Improve functionality for external member logins (#11855)
* Bugfix - Take ufprt from form data if the request has form content type, otherwise fallback to use the query * External linking for members * Changed migration to reuse old table * removed unnecessary web.config files * Cleanup * Extracted class to own file * Clean up * Rollback changes to Umbraco.Web.UI.csproj * Fixed migration for SqlCE * Change notification handler to be on deleted * Update src/Umbraco.Infrastructure/Security/MemberUserStore.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Fixed issue with errors not shown on member linking * fixed issue with errors * clean up * Fix issue where external logins could not be used to upgrade Umbraco, because the externalLogin table was expected to look different. (Like after the migration) * Fixed issue in Ignore legacy column now using result column. Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
This commit is contained in:
@@ -50,6 +50,7 @@ namespace Umbraco.Cms.Core
|
||||
/// providers need to be setup differently and each auth type for the back office will be prefixed with this value
|
||||
/// </remarks>
|
||||
public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco.";
|
||||
public const string MemberExternalAuthenticationTypePrefix = "UmbracoMembers.";
|
||||
|
||||
public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode";
|
||||
public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode";
|
||||
|
||||
@@ -62,6 +62,19 @@ namespace Umbraco.Extensions
|
||||
return username;
|
||||
}
|
||||
|
||||
public static string GetEmail(this IIdentity identity)
|
||||
{
|
||||
if (identity == null) throw new ArgumentNullException(nameof(identity));
|
||||
|
||||
string email = null;
|
||||
if (identity is ClaimsIdentity claimsIdentity)
|
||||
{
|
||||
email = claimsIdentity.FindFirstValue(ClaimTypes.Email);
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first claim value found in the <see cref="ClaimsIdentity"/> for the given claimType
|
||||
/// </summary>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Core.Persistence.Repositories
|
||||
{
|
||||
|
||||
public interface IExternalLoginRepository : IReadWriteQueryRepository<int, IIdentityUserLogin>, IQueryRepository<IIdentityUserToken>
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all external login providers for the user
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="logins"></param>
|
||||
[Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")]
|
||||
void Save(int userId, IEnumerable<IExternalLogin> logins);
|
||||
|
||||
/// <summary>
|
||||
@@ -17,8 +21,9 @@ namespace Umbraco.Cms.Core.Persistence.Repositories
|
||||
/// </summary>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="tokens"></param>
|
||||
[Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")]
|
||||
void Save(int userId, IEnumerable<IExternalLoginToken> tokens);
|
||||
|
||||
[Obsolete("Use method that takes guid as param from IExternalLoginWithKeyRepository")]
|
||||
void DeleteUserLogins(int memberId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Core.Persistence.Repositories
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Repository for external logins with Guid as key, so it can be shared for members and users
|
||||
/// </summary>
|
||||
public interface IExternalLoginWithKeyRepository : IReadWriteQueryRepository<int, IIdentityUserLogin>, IQueryRepository<IIdentityUserToken>
|
||||
{
|
||||
/// <summary>
|
||||
/// Replaces all external login providers for the user/member key
|
||||
/// </summary>
|
||||
void Save(Guid userOrMemberKey, IEnumerable<IExternalLogin> logins);
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all external login provider tokens for the providers specified for the user/member key
|
||||
/// </summary>
|
||||
void Save(Guid userOrMemberKey, IEnumerable<IExternalLoginToken> tokens);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all external logins for the specified the user/member key
|
||||
/// </summary>
|
||||
void DeleteUserLogins(Guid userOrMemberKey);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
string ProviderKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets user Id for the user who owns this login
|
||||
/// Gets or sets user or member key (Guid) for the user/member who owns this login
|
||||
/// </summary>
|
||||
string UserId { get; set; } // TODO: This should be able to be used by both users and members
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
@@ -6,6 +7,7 @@ namespace Umbraco.Cms.Core.Services
|
||||
/// <summary>
|
||||
/// Used to store the external login info
|
||||
/// </summary>
|
||||
[Obsolete("Use IExternalLoginServiceWithKey. This will be removed in Umbraco 10")]
|
||||
public interface IExternalLoginService : IService
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
54
src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs
Normal file
54
src/Umbraco.Core/Services/IExternalLoginWithKeyService.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services
|
||||
{
|
||||
public interface IExternalLoginWithKeyService : IService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all user logins assigned
|
||||
/// </summary>
|
||||
IEnumerable<IIdentityUserLogin> GetExternalLogins(Guid userOrMemberKey);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all user login tokens assigned
|
||||
/// </summary>
|
||||
IEnumerable<IIdentityUserToken> GetExternalLoginTokens(Guid userOrMemberKey);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all logins matching the login info - generally there should only be one but in some cases
|
||||
/// there might be more than one depending on if an administrator has been editing/removing members
|
||||
/// </summary>
|
||||
IEnumerable<IIdentityUserLogin> Find(string loginProvider, string providerKey);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the external logins associated with the user
|
||||
/// </summary>
|
||||
/// <param name="userOrMemberKey">
|
||||
/// The user or member key associated with the logins
|
||||
/// </param>
|
||||
/// <param name="logins"></param>
|
||||
/// <remarks>
|
||||
/// This will replace all external login provider information for the user
|
||||
/// </remarks>
|
||||
void Save(Guid userOrMemberKey, IEnumerable<IExternalLogin> logins);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the external login tokens associated with the user
|
||||
/// </summary>
|
||||
/// <param name="userId">
|
||||
/// The user or member key associated with the logins
|
||||
/// </param>
|
||||
/// <param name="tokens"></param>
|
||||
/// <remarks>
|
||||
/// This will replace all external login tokens for the user
|
||||
/// </remarks>
|
||||
void Save(Guid userOrMemberKey,IEnumerable<IExternalLoginToken> tokens);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all user logins - normally used when a member is deleted
|
||||
/// </summary>
|
||||
void DeleteUserLogins(Guid userOrMemberKey);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
|
||||
@@ -29,7 +30,9 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
|
||||
builder.Services.AddUnique<IDocumentTypeContainerRepository, DocumentTypeContainerRepository>();
|
||||
builder.Services.AddUnique<IDomainRepository, DomainRepository>();
|
||||
builder.Services.AddUnique<IEntityRepository, EntityRepository>();
|
||||
builder.Services.AddUnique<IExternalLoginRepository, ExternalLoginRepository>();
|
||||
builder.Services.AddUnique<ExternalLoginRepository>();
|
||||
builder.Services.AddUnique<IExternalLoginRepository>(factory => factory.GetRequiredService<ExternalLoginRepository>());
|
||||
builder.Services.AddUnique<IExternalLoginWithKeyRepository>(factory => factory.GetRequiredService<ExternalLoginRepository>());
|
||||
builder.Services.AddUnique<ILanguageRepository, LanguageRepository>();
|
||||
builder.Services.AddUnique<IMacroRepository, MacroRepository>();
|
||||
builder.Services.AddUnique<IMediaRepository, MediaRepository>();
|
||||
|
||||
@@ -8,10 +8,13 @@ using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Packaging;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.Implement;
|
||||
using Umbraco.Cms.Infrastructure.Packaging;
|
||||
@@ -64,7 +67,14 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
|
||||
builder.Services.AddUnique<IMemberTypeService, MemberTypeService>();
|
||||
builder.Services.AddUnique<IMemberGroupService, MemberGroupService>();
|
||||
builder.Services.AddUnique<INotificationService, NotificationService>();
|
||||
builder.Services.AddUnique<IExternalLoginService, ExternalLoginService>();
|
||||
builder.Services.AddUnique<ExternalLoginService>(factory => new ExternalLoginService(
|
||||
factory.GetRequiredService<IScopeProvider>(),
|
||||
factory.GetRequiredService<ILoggerFactory>(),
|
||||
factory.GetRequiredService<IEventMessagesFactory>(),
|
||||
factory.GetRequiredService<IExternalLoginWithKeyRepository>()
|
||||
));
|
||||
builder.Services.AddUnique<IExternalLoginService>(factory => factory.GetRequiredService<ExternalLoginService>());
|
||||
builder.Services.AddUnique<IExternalLoginWithKeyService>(factory => factory.GetRequiredService<ExternalLoginService>());
|
||||
builder.Services.AddUnique<IRedirectUrlService, RedirectUrlService>();
|
||||
builder.Services.AddUnique<IConsentService, ConsentService>();
|
||||
builder.Services.AddTransient(SourcesFactory);
|
||||
|
||||
@@ -15,6 +15,7 @@ using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_9_0;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_0_0;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_1_0;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_2_0;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
|
||||
@@ -269,6 +270,11 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
|
||||
To<AddUserGroup2NodeTable>("{0571C395-8F0B-44E9-8E3F-47BDD08D817B}");
|
||||
To<AddDefaultForNotificationsToggle>("{AD3D3B7F-8E74-45A4-85DB-7FFAD57F9243}");
|
||||
To<MovePackageXMLToDb>("{A2F22F17-5870-4179-8A8D-2362AA4A0A5F}");
|
||||
|
||||
|
||||
// TO 9.3.0
|
||||
To<UpdateExternalLoginToUseKeyInsteadOfId>("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0
|
||||
{
|
||||
public class UpdateExternalLoginToUseKeyInsteadOfId : MigrationBase
|
||||
{
|
||||
public UpdateExternalLoginToUseKeyInsteadOfId(IMigrationContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Migrate()
|
||||
{
|
||||
if (!ColumnExists(ExternalLoginDto.TableName, "userOrMemberKey"))
|
||||
{
|
||||
var indexNameToRecreate = "IX_" + ExternalLoginDto.TableName + "_LoginProvider";
|
||||
var indexNameToDelete = "IX_" + ExternalLoginDto.TableName + "_userId";
|
||||
|
||||
if (IndexExists(indexNameToRecreate))
|
||||
{
|
||||
// drop it since the previous migration index was wrong, and we
|
||||
// need to modify a column that belons to it
|
||||
Delete.Index(indexNameToRecreate).OnTable(ExternalLoginDto.TableName).Do();
|
||||
}
|
||||
|
||||
if (IndexExists(indexNameToDelete))
|
||||
{
|
||||
// drop it since the previous migration index was wrong, and we
|
||||
// need to modify a column that belons to it
|
||||
Delete.Index(indexNameToDelete).OnTable(ExternalLoginDto.TableName).Do();
|
||||
}
|
||||
|
||||
//special trick to add the column without constraints and return the sql to add them later
|
||||
AddColumn<ExternalLoginDto>("userOrMemberKey", out var sqls);
|
||||
|
||||
|
||||
if (DatabaseType.IsSqlCe())
|
||||
{
|
||||
var userIds = Database.Fetch<int>(Sql().Select("userId").From<ExternalLoginDto>());
|
||||
|
||||
foreach (int userId in userIds)
|
||||
{
|
||||
Execute.Sql($"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = '{userId.ToGuid()}' WHERE userId = {userId}").Do();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//populate the new columns with the userId as a Guid. Same method as IntExtensions.ToGuid.
|
||||
Execute.Sql($"UPDATE {ExternalLoginDto.TableName} SET userOrMemberKey = CAST(CONVERT(char(8), CONVERT(BINARY(4), userId), 2) + '-0000-0000-0000-000000000000' AS UNIQUEIDENTIFIER)").Do();
|
||||
|
||||
}
|
||||
|
||||
//now apply constraints (NOT NULL) to new table
|
||||
foreach (var sql in sqls) Execute.Sql(sql).Do();
|
||||
|
||||
//now remove these old columns
|
||||
Delete.Column("userId").FromTable(ExternalLoginDto.TableName).Do();
|
||||
|
||||
// create index with the correct definition
|
||||
Create
|
||||
.Index(indexNameToRecreate)
|
||||
.OnTable(ExternalLoginDto.TableName)
|
||||
.OnColumn("loginProvider").Ascending()
|
||||
.OnColumn("userOrMemberKey").Ascending()
|
||||
.WithOptions()
|
||||
.Unique()
|
||||
.WithOptions()
|
||||
.NonClustered()
|
||||
.Do();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
|
||||
[PrimaryKeyColumn]
|
||||
public int Id { get; set; }
|
||||
|
||||
// TODO: This is completely missing a FK!!? ... IIRC that is because we want to change this to a GUID
|
||||
// to support both members and users for external logins and that will not have any referential integrity
|
||||
// This should be part of the members task for enabling external logins.
|
||||
[Obsolete("This only exists to ensure you can upgrade using external logins from umbraco version where this was used to the new where it is not used")]
|
||||
[ResultColumn("userId")]
|
||||
public int? UserId { get; set; }
|
||||
|
||||
[Column("userId")]
|
||||
[Column("userOrMemberKey")]
|
||||
[Index(IndexTypes.NonClustered)]
|
||||
public int UserId { get; set; }
|
||||
public Guid UserOrMemberKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to store the name of the provider (i.e. Facebook, Google)
|
||||
@@ -30,7 +30,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
|
||||
[Column("loginProvider")]
|
||||
[Length(400)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userId", Name = "IX_" + TableName + "_LoginProvider")]
|
||||
[Index(IndexTypes.UniqueNonClustered, ForColumns = "loginProvider,userOrMemberKey", Name = "IX_" + TableName + "_LoginProvider")]
|
||||
public string LoginProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Globalization;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Factories
|
||||
{
|
||||
@@ -9,7 +10,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
|
||||
{
|
||||
public static IIdentityUserToken BuildEntity(ExternalLoginTokenDto dto)
|
||||
{
|
||||
var entity = new IdentityUserToken(dto.Id, dto.ExternalLoginDto.LoginProvider, dto.Name, dto.Value, dto.ExternalLoginDto.UserId.ToString(CultureInfo.InvariantCulture), dto.CreateDate);
|
||||
var entity = new IdentityUserToken(dto.Id, dto.ExternalLoginDto.LoginProvider, dto.Name, dto.Value, dto.ExternalLoginDto.UserOrMemberKey.ToString(), dto.CreateDate);
|
||||
|
||||
// reset dirty initial properties (U4-1946)
|
||||
entity.ResetDirtyProperties(false);
|
||||
@@ -18,7 +19,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
|
||||
|
||||
public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto)
|
||||
{
|
||||
var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId.ToString(CultureInfo.InvariantCulture), dto.CreateDate)
|
||||
|
||||
//If there exists a UserId - this means the database is still not migrated. E.g on the upgrade state.
|
||||
//At this point we have to manually set the key, to ensure external logins can be used to upgrade
|
||||
var key = dto.UserId.HasValue ? dto.UserId.Value.ToGuid().ToString() : dto.UserOrMemberKey.ToString();
|
||||
|
||||
var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, key, dto.CreateDate)
|
||||
{
|
||||
UserData = dto.UserData
|
||||
};
|
||||
@@ -36,19 +42,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
|
||||
CreateDate = entity.CreateDate,
|
||||
LoginProvider = entity.LoginProvider,
|
||||
ProviderKey = entity.ProviderKey,
|
||||
UserId = int.Parse(entity.UserId, CultureInfo.InvariantCulture), // TODO: This is temp until we change the ext logins to use GUIDs
|
||||
UserOrMemberKey = entity.Key,
|
||||
UserData = entity.UserData
|
||||
};
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
public static ExternalLoginDto BuildDto(int userId, IExternalLogin entity, int? id = null)
|
||||
public static ExternalLoginDto BuildDto(Guid userOrMemberKey, IExternalLogin entity, int? id = null)
|
||||
{
|
||||
var dto = new ExternalLoginDto
|
||||
{
|
||||
Id = id ?? default,
|
||||
UserId = userId,
|
||||
UserOrMemberKey = userOrMemberKey,
|
||||
LoginProvider = entity.LoginProvider,
|
||||
ProviderKey = entity.ProviderKey,
|
||||
UserData = entity.UserData,
|
||||
|
||||
@@ -18,7 +18,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers
|
||||
DefineMap<IdentityUserLogin, ExternalLoginDto>(nameof(IdentityUserLogin.CreateDate), nameof(ExternalLoginDto.CreateDate));
|
||||
DefineMap<IdentityUserLogin, ExternalLoginDto>(nameof(IdentityUserLogin.LoginProvider), nameof(ExternalLoginDto.LoginProvider));
|
||||
DefineMap<IdentityUserLogin, ExternalLoginDto>(nameof(IdentityUserLogin.ProviderKey), nameof(ExternalLoginDto.ProviderKey));
|
||||
DefineMap<IdentityUserLogin, ExternalLoginDto>(nameof(IdentityUserLogin.UserId), nameof(ExternalLoginDto.UserId));
|
||||
DefineMap<IdentityUserLogin, ExternalLoginDto>(nameof(IdentityUserLogin.Key), nameof(ExternalLoginDto.UserOrMemberKey));
|
||||
DefineMap<IdentityUserLogin, ExternalLoginDto>(nameof(IdentityUserLogin.UserData), nameof(ExternalLoginDto.UserData));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers
|
||||
DefineMap<IdentityUserToken, ExternalLoginTokenDto>(nameof(IdentityUserToken.Name), nameof(ExternalLoginTokenDto.Name));
|
||||
DefineMap<IdentityUserToken, ExternalLoginTokenDto>(nameof(IdentityUserToken.Value), nameof(ExternalLoginTokenDto.Value));
|
||||
// separate table
|
||||
DefineMap<IdentityUserToken, ExternalLoginDto>(nameof(IdentityUserToken.UserId), nameof(ExternalLoginDto.UserId));
|
||||
DefineMap<IdentityUserLogin, ExternalLoginDto>(nameof(IdentityUserLogin.Key), nameof(ExternalLoginDto.UserOrMemberKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ using NPoco;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Persistence;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
@@ -18,22 +17,34 @@ using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
{
|
||||
// TODO: We should update this to support both users and members. It means we would remove referential integrity from users
|
||||
// and the user/member key would be a GUID (we also need to add a GUID to users)
|
||||
internal class ExternalLoginRepository : EntityRepositoryBase<int, IIdentityUserLogin>, IExternalLoginRepository
|
||||
internal class ExternalLoginRepository : EntityRepositoryBase<int, IIdentityUserLogin>, IExternalLoginRepository, IExternalLoginWithKeyRepository
|
||||
{
|
||||
public ExternalLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger<ExternalLoginRepository> logger)
|
||||
: base(scopeAccessor, cache, logger)
|
||||
{ }
|
||||
|
||||
public void DeleteUserLogins(int memberId) => Database.Delete<ExternalLoginDto>("WHERE userId=@userId", new { userId = memberId });
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Use method that takes guid as param")]
|
||||
public void DeleteUserLogins(int memberId) => DeleteUserLogins(memberId.ToGuid());
|
||||
|
||||
public void Save(int userId, IEnumerable<IExternalLogin> logins)
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Use method that takes guid as param")]
|
||||
public void Save(int userId, IEnumerable<IExternalLogin> logins) => Save(userId.ToGuid(), logins);
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Use method that takes guid as param")]
|
||||
public void Save(int userId, IEnumerable<IExternalLoginToken> tokens) => Save(userId.ToGuid(), tokens);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteUserLogins(Guid userOrMemberKey) => Database.Delete<ExternalLoginDto>("WHERE userOrMemberKey=@userOrMemberKey", new { userOrMemberKey });
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save(Guid userOrMemberKey, IEnumerable<IExternalLogin> logins)
|
||||
{
|
||||
var sql = Sql()
|
||||
.Select<ExternalLoginDto>()
|
||||
.From<ExternalLoginDto>()
|
||||
.Where<ExternalLoginDto>(x => x.UserId == userId)
|
||||
.Where<ExternalLoginDto>(x => x.UserOrMemberKey == userOrMemberKey)
|
||||
.ForUpdate();
|
||||
|
||||
// deduplicate the logins
|
||||
@@ -71,10 +82,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
|
||||
foreach (var u in toUpdate)
|
||||
{
|
||||
Database.Update(ExternalLoginFactory.BuildDto(userId, u.Value, u.Key));
|
||||
Database.Update(ExternalLoginFactory.BuildDto(userOrMemberKey, u.Value, u.Key));
|
||||
}
|
||||
|
||||
Database.InsertBulk(toInsert.Select(i => ExternalLoginFactory.BuildDto(userId, i)));
|
||||
Database.InsertBulk(toInsert.Select(i => ExternalLoginFactory.BuildDto(userOrMemberKey, i)));
|
||||
}
|
||||
|
||||
protected override IIdentityUserLogin PerformGet(int id)
|
||||
@@ -217,11 +228,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
return Database.ExecuteScalar<int>(sql);
|
||||
}
|
||||
|
||||
public void Save(int userId, IEnumerable<IExternalLoginToken> tokens)
|
||||
/// <inheritdoc />
|
||||
public void Save(Guid userOrMemberKey, IEnumerable<IExternalLoginToken> tokens)
|
||||
{
|
||||
// get the existing logins (provider + id)
|
||||
var existingUserLogins = Database
|
||||
.Fetch<ExternalLoginDto>(GetBaseQuery(false).Where<ExternalLoginDto>(x => x.UserId == userId))
|
||||
.Fetch<ExternalLoginDto>(GetBaseQuery(false).Where<ExternalLoginDto>(x => x.UserOrMemberKey == userOrMemberKey))
|
||||
.ToDictionary(x => x.LoginProvider, x => x.Id);
|
||||
|
||||
// deduplicate the tokens
|
||||
@@ -231,7 +243,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
|
||||
Sql<ISqlContext> sql = GetBaseTokenQuery(true)
|
||||
.WhereIn<ExternalLoginDto>(x => x.LoginProvider, providers)
|
||||
.Where<ExternalLoginDto>(x => x.UserId == userId);
|
||||
.Where<ExternalLoginDto>(x => x.UserOrMemberKey == userOrMemberKey);
|
||||
|
||||
var toUpdate = new Dictionary<int, (IExternalLoginToken externalLoginToken, int externalLoginId)>();
|
||||
var toDelete = new List<int>();
|
||||
@@ -289,7 +301,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
.On<ExternalLoginTokenDto, ExternalLoginDto>(x => x.ExternalLoginId, x => x.Id)
|
||||
: Sql()
|
||||
.Select<ExternalLoginTokenDto>()
|
||||
.AndSelect<ExternalLoginDto>(x => x.LoginProvider, x => x.UserId)
|
||||
.AndSelect<ExternalLoginDto>(x => x.LoginProvider, x => x.UserOrMemberKey)
|
||||
.From<ExternalLoginTokenDto>()
|
||||
.InnerJoin<ExternalLoginDto>()
|
||||
.On<ExternalLoginTokenDto, ExternalLoginDto>(x => x.ExternalLoginId, x => x.Id);
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
@@ -16,6 +17,7 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Security
|
||||
@@ -29,7 +31,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly IExternalLoginService _externalLoginService;
|
||||
private readonly IExternalLoginWithKeyService _externalLoginService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
private readonly AppCaches _appCaches;
|
||||
@@ -37,11 +39,12 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BackOfficeUserStore"/> class.
|
||||
/// </summary>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public BackOfficeUserStore(
|
||||
IScopeProvider scopeProvider,
|
||||
IUserService userService,
|
||||
IEntityService entityService,
|
||||
IExternalLoginService externalLoginService,
|
||||
IExternalLoginWithKeyService externalLoginService,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IUmbracoMapper mapper,
|
||||
BackOfficeErrorDescriber describer,
|
||||
@@ -59,6 +62,29 @@ namespace Umbraco.Cms.Core.Security
|
||||
_externalLoginService = externalLoginService;
|
||||
}
|
||||
|
||||
[Obsolete("Use ctor injecting IExternalLoginWithKeyService ")]
|
||||
public BackOfficeUserStore(
|
||||
IScopeProvider scopeProvider,
|
||||
IUserService userService,
|
||||
IEntityService entityService,
|
||||
IExternalLoginService externalLoginService,
|
||||
IOptions<GlobalSettings> globalSettings,
|
||||
IUmbracoMapper mapper,
|
||||
BackOfficeErrorDescriber describer,
|
||||
AppCaches appCaches)
|
||||
: this(
|
||||
scopeProvider,
|
||||
userService,
|
||||
entityService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IExternalLoginWithKeyService>(),
|
||||
globalSettings,
|
||||
mapper,
|
||||
describer,
|
||||
appCaches)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IdentityResult> CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -104,7 +130,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
if (isLoginsPropertyDirty)
|
||||
{
|
||||
_externalLoginService.Save(
|
||||
userEntity.Id,
|
||||
userEntity.Key,
|
||||
user.Logins.Select(x => new ExternalLogin(
|
||||
x.LoginProvider,
|
||||
x.ProviderKey,
|
||||
@@ -114,7 +140,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
if (isTokensPropertyDirty)
|
||||
{
|
||||
_externalLoginService.Save(
|
||||
userEntity.Id,
|
||||
userEntity.Key,
|
||||
user.LoginTokens.Select(x => new ExternalLoginToken(
|
||||
x.LoginProvider,
|
||||
x.Name,
|
||||
@@ -156,7 +182,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
if (isLoginsPropertyDirty)
|
||||
{
|
||||
_externalLoginService.Save(
|
||||
found.Id,
|
||||
found.Key,
|
||||
user.Logins.Select(x => new ExternalLogin(
|
||||
x.LoginProvider,
|
||||
x.ProviderKey,
|
||||
@@ -166,7 +192,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
if (isTokensPropertyDirty)
|
||||
{
|
||||
_externalLoginService.Save(
|
||||
found.Id,
|
||||
found.Key,
|
||||
user.LoginTokens.Select(x => new ExternalLoginToken(
|
||||
x.LoginProvider,
|
||||
x.Name,
|
||||
@@ -190,13 +216,14 @@ namespace Umbraco.Cms.Core.Security
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
IUser found = _userService.GetUserById(UserIdToInt(user.Id));
|
||||
var userId = UserIdToInt(user.Id);
|
||||
IUser found = _userService.GetUserById(userId);
|
||||
if (found != null)
|
||||
{
|
||||
_userService.Delete(found);
|
||||
}
|
||||
|
||||
_externalLoginService.DeleteUserLogins(UserIdToInt(user.Id));
|
||||
_externalLoginService.DeleteUserLogins(userId.ToGuid());
|
||||
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
}
|
||||
@@ -414,7 +441,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
if (user != null)
|
||||
{
|
||||
var userId = UserIdToInt(user.Id);
|
||||
var userId = UserIdToInt(user.Id).ToGuid();
|
||||
user.SetLoginsCallback(new Lazy<IEnumerable<IIdentityUserLogin>>(() => _externalLoginService.GetExternalLogins(userId)));
|
||||
user.SetTokensCallback(new Lazy<IEnumerable<IIdentityUserToken>>(() => _externalLoginService.GetExternalLoginTokens(userId)));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Deletes the external logins for the deleted members. This cannot be handled by the database as there is not foreign keys.
|
||||
/// </summary>
|
||||
public class DeleteExternalLoginsOnMemberDeletedHandler : INotificationHandler<MemberDeletedNotification>
|
||||
{
|
||||
private readonly IExternalLoginWithKeyService _externalLoginWithKeyService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeleteExternalLoginsOnMemberDeletingHandler"/> class.
|
||||
/// </summary>
|
||||
public DeleteExternalLoginsOnMemberDeletedHandler(IExternalLoginWithKeyService externalLoginWithKeyService)
|
||||
=> _externalLoginWithKeyService = externalLoginWithKeyService;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(MemberDeletedNotification notification)
|
||||
{
|
||||
foreach (IMember member in notification.DeletedEntities)
|
||||
{
|
||||
_externalLoginWithKeyService.DeleteUserLogins(member.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,14 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Security
|
||||
@@ -26,6 +28,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
private readonly IUmbracoMapper _mapper;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IExternalLoginWithKeyService _externalLoginService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemberUserStore"/> class for the members identity store
|
||||
@@ -34,18 +37,48 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// <param name="mapper">The mapper for properties</param>
|
||||
/// <param name="scopeProvider">The scope provider</param>
|
||||
/// <param name="describer">The error describer</param>
|
||||
/// <param name="externalLoginService">The external login service</param>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public MemberUserStore(
|
||||
IMemberService memberService,
|
||||
IUmbracoMapper mapper,
|
||||
IScopeProvider scopeProvider,
|
||||
IdentityErrorDescriber describer,
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor)
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IExternalLoginWithKeyService externalLoginService
|
||||
)
|
||||
: base(describer)
|
||||
{
|
||||
_memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
|
||||
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
|
||||
_scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_externalLoginService = externalLoginService;
|
||||
}
|
||||
|
||||
[Obsolete("Use ctor with IExternalLoginWithKeyService param")]
|
||||
public MemberUserStore(
|
||||
IMemberService memberService,
|
||||
IUmbracoMapper mapper,
|
||||
IScopeProvider scopeProvider,
|
||||
IdentityErrorDescriber describer,
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IExternalLoginService externalLoginService)
|
||||
: this(memberService, mapper, scopeProvider, describer, publishedSnapshotAccessor, StaticServiceProvider.Instance.GetRequiredService<IExternalLoginWithKeyService>())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Obsolete("Use ctor with IExternalLoginWithKeyService param")]
|
||||
public MemberUserStore(
|
||||
IMemberService memberService,
|
||||
IUmbracoMapper mapper,
|
||||
IScopeProvider scopeProvider,
|
||||
IdentityErrorDescriber describer,
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor)
|
||||
: this(memberService, mapper, scopeProvider, describer, publishedSnapshotAccessor, StaticServiceProvider.Instance.GetRequiredService<IExternalLoginWithKeyService>())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -83,18 +116,29 @@ namespace Umbraco.Cms.Core.Security
|
||||
user.Id = UserIdToString(memberEntity.Id);
|
||||
user.Key = memberEntity.Key;
|
||||
|
||||
// [from backofficeuser] we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
|
||||
// var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MembersIdentityUser.Logins));
|
||||
// TODO: confirm re externallogins implementation
|
||||
//if (isLoginsPropertyDirty)
|
||||
//{
|
||||
// _externalLoginService.Save(
|
||||
// user.Id,
|
||||
// user.Logins.Select(x => new ExternalLogin(
|
||||
// x.LoginProvider,
|
||||
// x.ProviderKey,
|
||||
// x.UserData)));
|
||||
//}
|
||||
// we have to remember whether Logins property is dirty, since the UpdateMemberProperties will reset it.
|
||||
var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.Logins));
|
||||
var isTokensPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.LoginTokens));
|
||||
|
||||
if (isLoginsPropertyDirty)
|
||||
{
|
||||
_externalLoginService.Save(
|
||||
memberEntity.Key,
|
||||
user.Logins.Select(x => new ExternalLogin(
|
||||
x.LoginProvider,
|
||||
x.ProviderKey,
|
||||
x.UserData)));
|
||||
}
|
||||
|
||||
if (isTokensPropertyDirty)
|
||||
{
|
||||
_externalLoginService.Save(
|
||||
memberEntity.Key,
|
||||
user.LoginTokens.Select(x => new ExternalLoginToken(
|
||||
x.LoginProvider,
|
||||
x.Name,
|
||||
x.Value)));
|
||||
}
|
||||
|
||||
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
@@ -142,17 +186,15 @@ namespace Umbraco.Cms.Core.Security
|
||||
_memberService.SetLastLogin(found.Username, DateTime.Now);
|
||||
}
|
||||
|
||||
// TODO: when to implement external login service?
|
||||
|
||||
//if (isLoginsPropertyDirty)
|
||||
//{
|
||||
// _externalLoginService.Save(
|
||||
// found.Id,
|
||||
// user.Logins.Select(x => new ExternalLogin(
|
||||
// x.LoginProvider,
|
||||
// x.ProviderKey,
|
||||
// x.UserData)));
|
||||
//}
|
||||
if (isLoginsPropertyDirty)
|
||||
{
|
||||
_externalLoginService.Save(
|
||||
found.Key,
|
||||
user.Logins.Select(x => new ExternalLogin(
|
||||
x.LoginProvider,
|
||||
x.ProviderKey,
|
||||
x.UserData)));
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
@@ -181,8 +223,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
_memberService.Delete(found);
|
||||
}
|
||||
|
||||
// TODO: when to implement external login service?
|
||||
//_externalLoginService.DeleteUserLogins(UserIdToInt(user.Id));
|
||||
_externalLoginService.DeleteUserLogins(user.Key);
|
||||
|
||||
return Task.FromResult(IdentityResult.Success);
|
||||
}
|
||||
@@ -203,7 +244,8 @@ namespace Umbraco.Cms.Core.Security
|
||||
throw new ArgumentNullException(nameof(userId));
|
||||
}
|
||||
|
||||
IMember user = _memberService.GetById(UserIdToInt(userId));
|
||||
|
||||
IMember user = Guid.TryParse(userId, out var key) ? _memberService.GetByKey(key) : _memberService.GetById(UserIdToInt(userId));
|
||||
if (user == null)
|
||||
{
|
||||
return Task.FromResult((MemberIdentityUser)null);
|
||||
@@ -375,10 +417,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
throw new ArgumentNullException(nameof(providerKey));
|
||||
}
|
||||
|
||||
var logins = new List<IIdentityUserLogin>();
|
||||
|
||||
// TODO: external login needed
|
||||
//_externalLoginService.Find(loginProvider, providerKey).ToList();
|
||||
var logins = _externalLoginService.Find(loginProvider, providerKey).ToList();
|
||||
if (logins.Count == 0)
|
||||
{
|
||||
return Task.FromResult((IdentityUserLogin<string>)null);
|
||||
@@ -492,8 +531,8 @@ namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
if (user != null)
|
||||
{
|
||||
//TODO: implement
|
||||
//user.SetLoginsCallback(new Lazy<IEnumerable<IIdentityUserLogin>>(() => _externalLoginService.GetAll(UserIdToInt(user.Id))));
|
||||
user.SetLoginsCallback(new Lazy<IEnumerable<IIdentityUserLogin>>(() => _externalLoginService.GetExternalLogins(user.Key)));
|
||||
user.SetTokensCallback(new Lazy<IEnumerable<IIdentityUserToken>>(() => _externalLoginService.GetExternalLoginTokens(user.Key)));
|
||||
}
|
||||
|
||||
return user;
|
||||
|
||||
@@ -34,6 +34,12 @@ namespace Umbraco.Cms.Core.Security
|
||||
return result;
|
||||
}
|
||||
|
||||
if(Guid.TryParse(userId, out var key))
|
||||
{
|
||||
// Reverse the IntExtensions.ToGuid
|
||||
return BitConverter.ToInt32(key.ToByteArray(), 0);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Implement
|
||||
{
|
||||
public class ExternalLoginService : RepositoryService, IExternalLoginService
|
||||
public class ExternalLoginService : RepositoryService, IExternalLoginService, IExternalLoginWithKeyService
|
||||
{
|
||||
private readonly IExternalLoginRepository _externalLoginRepository;
|
||||
private readonly IExternalLoginWithKeyRepository _externalLoginRepository;
|
||||
|
||||
public ExternalLoginService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
|
||||
IExternalLoginRepository externalLoginRepository)
|
||||
IExternalLoginWithKeyRepository externalLoginRepository)
|
||||
: base(provider, loggerFactory, eventMessagesFactory)
|
||||
{
|
||||
_externalLoginRepository = externalLoginRepository;
|
||||
}
|
||||
|
||||
[Obsolete("Use ctor injecting IExternalLoginWithKeyRepository")]
|
||||
public ExternalLoginService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory,
|
||||
IExternalLoginRepository externalLoginRepository)
|
||||
: this(provider, loggerFactory, eventMessagesFactory, StaticServiceProvider.Instance.GetRequiredService<IExternalLoginWithKeyRepository>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Use overload that takes a user/member key (Guid).")]
|
||||
public IEnumerable<IIdentityUserLogin> GetExternalLogins(int userId)
|
||||
=> GetExternalLogins(userId.ToGuid());
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Use overload that takes a user/member key (Guid).")]
|
||||
public IEnumerable<IIdentityUserToken> GetExternalLoginTokens(int userId) =>
|
||||
GetExternalLoginTokens(userId.ToGuid());
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Use overload that takes a user/member key (Guid).")]
|
||||
public void Save(int userId, IEnumerable<IExternalLogin> logins)
|
||||
=> Save(userId.ToGuid(), logins);
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Use overload that takes a user/member key (Guid).")]
|
||||
public void Save(int userId, IEnumerable<IExternalLoginToken> tokens)
|
||||
=> Save(userId.ToGuid(), tokens);
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("Use overload that takes a user/member key (Guid).")]
|
||||
public void DeleteUserLogins(int userId)
|
||||
=> DeleteUserLogins(userId.ToGuid());
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IIdentityUserLogin> GetExternalLogins(Guid userOrMemberKey)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
// TODO: This is temp until we update the external service to support guids for both users and members
|
||||
var asString = userId.ToString(CultureInfo.InvariantCulture);
|
||||
return _externalLoginRepository.Get(Query<IIdentityUserLogin>().Where(x => x.UserId == asString))
|
||||
return _externalLoginRepository.Get(Query<IIdentityUserLogin>().Where(x => x.Key == userOrMemberKey))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<IIdentityUserToken> GetExternalLoginTokens(int userId)
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<IIdentityUserToken> GetExternalLoginTokens(Guid userOrMemberKey)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
|
||||
{
|
||||
// TODO: This is temp until we update the external service to support guids for both users and members
|
||||
var asString = userId.ToString(CultureInfo.InvariantCulture);
|
||||
return _externalLoginRepository.Get(Query<IIdentityUserToken>().Where(x => x.UserId == asString))
|
||||
return _externalLoginRepository.Get(Query<IIdentityUserToken>().Where(x => x.Key == userOrMemberKey))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -55,30 +88,31 @@ namespace Umbraco.Cms.Core.Services.Implement
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save(int userId, IEnumerable<IExternalLogin> logins)
|
||||
public void Save(Guid userOrMemberKey, IEnumerable<IExternalLogin> logins)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
_externalLoginRepository.Save(userId, logins);
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
public void Save(int userId, IEnumerable<IExternalLoginToken> tokens)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
_externalLoginRepository.Save(userId, tokens);
|
||||
_externalLoginRepository.Save(userOrMemberKey, logins);
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteUserLogins(int userId)
|
||||
public void Save(Guid userOrMemberKey, IEnumerable<IExternalLoginToken> tokens)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
_externalLoginRepository.DeleteUserLogins(userId);
|
||||
_externalLoginRepository.Save(userOrMemberKey, tokens);
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteUserLogins(Guid userOrMemberKey)
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
_externalLoginRepository.DeleteUserLogins(userOrMemberKey);
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,15 @@ using System;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Net;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.BackOffice.Security;
|
||||
using Umbraco.Cms.Web.Common.AspNetCore;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
@@ -27,7 +33,16 @@ namespace Umbraco.Extensions
|
||||
|
||||
builder.BuildUmbracoBackOfficeIdentity()
|
||||
.AddDefaultTokenProviders()
|
||||
.AddUserStore<BackOfficeUserStore>()
|
||||
.AddUserStore<IUserStore<BackOfficeIdentityUser>, BackOfficeUserStore>(factory => new BackOfficeUserStore(
|
||||
factory.GetRequiredService<IScopeProvider>(),
|
||||
factory.GetRequiredService<IUserService>(),
|
||||
factory.GetRequiredService<IEntityService>(),
|
||||
factory.GetRequiredService<IExternalLoginWithKeyService>(),
|
||||
factory.GetRequiredService<IOptions<GlobalSettings>>(),
|
||||
factory.GetRequiredService<IUmbracoMapper>(),
|
||||
factory.GetRequiredService<BackOfficeErrorDescriber>(),
|
||||
factory.GetRequiredService<AppCaches>()
|
||||
))
|
||||
.AddUserManager<IBackOfficeUserManager, BackOfficeUserManager>()
|
||||
.AddSignInManager<IBackOfficeSignInManager, BackOfficeSignInManager>()
|
||||
.AddClaimsPrincipalFactory<BackOfficeClaimsPrincipalFactory>()
|
||||
|
||||
@@ -62,6 +62,11 @@ namespace Umbraco.Cms.Web.BackOffice.Security
|
||||
{
|
||||
public void PostConfigure(string name, TOptions options)
|
||||
{
|
||||
if (!name.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
options.SignInScheme = Constants.Security.BackOfficeExternalAuthenticationType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Runtime.Serialization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
using SecurityConstants = Umbraco.Cms.Core.Constants.Security;
|
||||
|
||||
namespace Umbraco.Cms.Web.BackOffice.Security
|
||||
@@ -13,11 +14,12 @@ namespace Umbraco.Cms.Web.BackOffice.Security
|
||||
public class ExternalSignInAutoLinkOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ExternalSignInAutoLinkOptions"/> instance
|
||||
/// Initializes a new instance of the <see cref="ExternalSignInAutoLinkOptions"/> class.
|
||||
/// </summary>
|
||||
/// <param name="autoLinkExternalAccount"></param>
|
||||
/// <param name="defaultUserGroups">If null, the default will be the 'editor' group</param>
|
||||
/// <param name="defaultCulture"></param>
|
||||
/// <param name="allowManualLinking"></param>
|
||||
public ExternalSignInAutoLinkOptions(
|
||||
bool autoLinkExternalAccount = false,
|
||||
string[] defaultUserGroups = null,
|
||||
@@ -30,12 +32,6 @@ namespace Umbraco.Cms.Web.BackOffice.Security
|
||||
_defaultCulture = defaultCulture;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// By default this is true which allows the user to manually link and unlink the external provider, if set to false the back office user
|
||||
/// will not see and cannot perform manual linking or unlinking of the external provider.
|
||||
/// </summary>
|
||||
public bool AllowManualLinking { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A callback executed during account auto-linking and before the user is persisted
|
||||
/// </summary>
|
||||
@@ -50,10 +46,16 @@ namespace Umbraco.Cms.Web.BackOffice.Security
|
||||
public Func<BackOfficeIdentityUser, ExternalLoginInfo, bool> OnExternalLogin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Flag indicating if logging in with the external provider should auto-link/create a local user
|
||||
/// Gets a value indicating whether flag indicating if logging in with the external provider should auto-link/create a local user
|
||||
/// </summary>
|
||||
public bool AutoLinkExternalAccount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// By default this is true which allows the user to manually link and unlink the external provider, if set to false the back office user
|
||||
/// will not see and cannot perform manual linking or unlinking of the external provider.
|
||||
/// </summary>
|
||||
public bool AllowManualLinking { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// The default user groups to assign to the created local user linked
|
||||
/// </summary>
|
||||
@@ -64,7 +66,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security
|
||||
/// <summary>
|
||||
/// The default Culture to use for auto-linking users
|
||||
/// </summary>
|
||||
// TODO: Should we use IDefaultCultureAccessor here intead?
|
||||
// TODO: Should we use IDefaultCultureAccessor here instead?
|
||||
public string GetUserAutoLinkCulture(GlobalSettings globalSettings) => _defaultCulture ?? globalSettings.DefaultUILanguage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Security;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
|
||||
namespace Umbraco.Extensions
|
||||
@@ -34,14 +45,24 @@ namespace Umbraco.Extensions
|
||||
|
||||
services.AddIdentity<MemberIdentityUser, UmbracoIdentityRole>()
|
||||
.AddDefaultTokenProviders()
|
||||
.AddUserStore<MemberUserStore>()
|
||||
.AddUserStore<IUserStore<MemberIdentityUser>, MemberUserStore>(factory => new MemberUserStore(
|
||||
factory.GetRequiredService<IMemberService>(),
|
||||
factory.GetRequiredService<IUmbracoMapper>(),
|
||||
factory.GetRequiredService<IScopeProvider>(),
|
||||
factory.GetRequiredService<IdentityErrorDescriber>(),
|
||||
factory.GetRequiredService<IPublishedSnapshotAccessor>(),
|
||||
factory.GetRequiredService<IExternalLoginWithKeyService>()
|
||||
))
|
||||
.AddRoleStore<MemberRoleStore>()
|
||||
.AddRoleManager<IMemberRoleManager, MemberRoleManager>()
|
||||
.AddMemberManager<IMemberManager, MemberManager>()
|
||||
.AddSignInManager<IMemberSignInManager, MemberSignInManager>()
|
||||
.AddSignInManager<IMemberSignInManagerExternalLogins, MemberSignInManager>()
|
||||
.AddErrorDescriber<MembersErrorDescriber>()
|
||||
.AddUserConfirmation<UmbracoUserConfirmation<MemberIdentityUser>>();
|
||||
|
||||
|
||||
builder.AddNotificationHandler<MemberDeletedNotification, DeleteExternalLoginsOnMemberDeletedHandler>();
|
||||
services.ConfigureOptions<ConfigureMemberIdentityOptions>();
|
||||
|
||||
services.AddScoped<IMemberUserStore>(x => (IMemberUserStore)x.GetRequiredService<IUserStore<MemberIdentityUser>>());
|
||||
@@ -50,6 +71,8 @@ namespace Umbraco.Extensions
|
||||
services.ConfigureOptions<ConfigureSecurityStampOptions>();
|
||||
services.ConfigureOptions<ConfigureMemberCookieOptions>();
|
||||
|
||||
services.AddUnique<IMemberExternalLoginProviders, MemberExternalLoginProviders>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
@@ -50,5 +51,13 @@ namespace Umbraco.Extensions
|
||||
identityBuilder.Services.AddScoped(typeof(TInterface), typeof(TSignInManager));
|
||||
return identityBuilder;
|
||||
}
|
||||
|
||||
|
||||
public static IdentityBuilder AddUserStore<TInterface, TStore>(this IdentityBuilder identityBuilder, Func<IServiceProvider, TStore> implementationFactory)
|
||||
where TStore : class, TInterface
|
||||
{
|
||||
identityBuilder.Services.AddScoped(typeof(TInterface), implementationFactory);
|
||||
return identityBuilder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Security
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Service to return <see cref="MemberExternalLoginProvider"/> instances
|
||||
/// </summary>
|
||||
public interface IMemberExternalLoginProviders
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the <see cref="BackOfficeExternalLoginProvider"/> for the specified scheme
|
||||
/// </summary>
|
||||
/// <param name="authenticationType"></param>
|
||||
/// <returns></returns>
|
||||
Task<MemberExternalLoginProviderScheme> GetAsync(string authenticationType);
|
||||
|
||||
/// <summary>
|
||||
/// Get all registered <see cref="BackOfficeExternalLoginProvider"/>
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<IEnumerable<MemberExternalLoginProviderScheme>> GetMemberProvidersAsync();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Security
|
||||
{
|
||||
public interface IMemberSignInManager
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Security
|
||||
{
|
||||
[Obsolete("This interface will be merged with IMemberSignInManager in Umbraco 10")]
|
||||
public interface IMemberSignInManagerExternalLogins : IMemberSignInManager
|
||||
{
|
||||
AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null);
|
||||
Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null);
|
||||
Task<IdentityResult> UpdateExternalAuthenticationTokensAsync(ExternalLoginInfo externalLogin);
|
||||
Task<SignInResult> ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// An external login (OAuth) provider for the members
|
||||
/// </summary>
|
||||
public class MemberExternalLoginProvider : IEquatable<MemberExternalLoginProvider>
|
||||
{
|
||||
public MemberExternalLoginProvider(
|
||||
string authenticationType,
|
||||
IOptionsMonitor<MemberExternalLoginProviderOptions> properties)
|
||||
{
|
||||
if (properties is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(properties));
|
||||
}
|
||||
|
||||
AuthenticationType = authenticationType ?? throw new ArgumentNullException(nameof(authenticationType));
|
||||
Options = properties.Get(authenticationType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The authentication "Scheme"
|
||||
/// </summary>
|
||||
public string AuthenticationType { get; }
|
||||
|
||||
public MemberExternalLoginProviderOptions Options { get; }
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as MemberExternalLoginProvider);
|
||||
public bool Equals(MemberExternalLoginProvider other) => other != null && AuthenticationType == other.AuthenticationType;
|
||||
public override int GetHashCode() => HashCode.Combine(AuthenticationType);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Umbraco.Cms.Web.Common.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Options used to configure member external login providers
|
||||
/// </summary>
|
||||
public class MemberExternalLoginProviderOptions
|
||||
{
|
||||
public MemberExternalLoginProviderOptions(
|
||||
MemberExternalSignInAutoLinkOptions autoLinkOptions = null,
|
||||
bool autoRedirectLoginToExternalProvider = false,
|
||||
string customBackOfficeView = null)
|
||||
{
|
||||
AutoLinkOptions = autoLinkOptions ?? new MemberExternalSignInAutoLinkOptions();
|
||||
}
|
||||
|
||||
public MemberExternalLoginProviderOptions()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options used to control how users can be auto-linked/created/updated based on the external login provider
|
||||
/// </summary>
|
||||
public MemberExternalSignInAutoLinkOptions AutoLinkOptions { get; set; } = new MemberExternalSignInAutoLinkOptions();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Security
|
||||
{
|
||||
public class MemberExternalLoginProviderScheme
|
||||
{
|
||||
public MemberExternalLoginProviderScheme(
|
||||
MemberExternalLoginProvider externalLoginProvider,
|
||||
AuthenticationScheme authenticationScheme)
|
||||
{
|
||||
ExternalLoginProvider = externalLoginProvider ?? throw new ArgumentNullException(nameof(externalLoginProvider));
|
||||
AuthenticationScheme = authenticationScheme ?? throw new ArgumentNullException(nameof(authenticationScheme));
|
||||
}
|
||||
|
||||
public MemberExternalLoginProvider ExternalLoginProvider { get; }
|
||||
public AuthenticationScheme AuthenticationScheme { get; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Security
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public class MemberExternalLoginProviders : IMemberExternalLoginProviders
|
||||
{
|
||||
private readonly Dictionary<string, MemberExternalLoginProvider> _externalLogins;
|
||||
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
|
||||
|
||||
public MemberExternalLoginProviders(
|
||||
IEnumerable<MemberExternalLoginProvider> externalLogins,
|
||||
IAuthenticationSchemeProvider authenticationSchemeProvider)
|
||||
{
|
||||
_externalLogins = externalLogins.ToDictionary(x => x.AuthenticationType);
|
||||
_authenticationSchemeProvider = authenticationSchemeProvider;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<MemberExternalLoginProviderScheme> GetAsync(string authenticationType)
|
||||
{
|
||||
var schemaName =
|
||||
authenticationType.EnsureStartsWith(Core.Constants.Security.MemberExternalAuthenticationTypePrefix);
|
||||
|
||||
if (!_externalLogins.TryGetValue(schemaName, out MemberExternalLoginProvider provider))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// get the associated scheme
|
||||
AuthenticationScheme associatedScheme = await _authenticationSchemeProvider.GetSchemeAsync(provider.AuthenticationType);
|
||||
|
||||
if (associatedScheme == null)
|
||||
{
|
||||
throw new InvalidOperationException("No authentication scheme registered for " + provider.AuthenticationType);
|
||||
}
|
||||
|
||||
return new MemberExternalLoginProviderScheme(provider, associatedScheme);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IEnumerable<MemberExternalLoginProviderScheme>> GetMemberProvidersAsync()
|
||||
{
|
||||
var providersWithSchemes = new List<MemberExternalLoginProviderScheme>();
|
||||
foreach (MemberExternalLoginProvider login in _externalLogins.Values)
|
||||
{
|
||||
// get the associated scheme
|
||||
AuthenticationScheme associatedScheme = await _authenticationSchemeProvider.GetSchemeAsync(login.AuthenticationType);
|
||||
|
||||
providersWithSchemes.Add(new MemberExternalLoginProviderScheme(login, associatedScheme));
|
||||
}
|
||||
|
||||
return providersWithSchemes;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using SecurityConstants = Umbraco.Cms.Core.Constants.Security;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Options used to configure auto-linking external OAuth providers
|
||||
/// </summary>
|
||||
public class MemberExternalSignInAutoLinkOptions
|
||||
{
|
||||
private readonly string _defaultCulture;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemberExternalSignInAutoLinkOptions" /> class.
|
||||
/// </summary>
|
||||
public MemberExternalSignInAutoLinkOptions(
|
||||
bool autoLinkExternalAccount = false,
|
||||
bool defaultIsApproved = true,
|
||||
string defaultMemberTypeAlias = Core.Constants.Conventions.MemberTypes.DefaultAlias,
|
||||
string defaultCulture = null,
|
||||
IEnumerable<string> defaultMemberGroups = null)
|
||||
{
|
||||
AutoLinkExternalAccount = autoLinkExternalAccount;
|
||||
DefaultIsApproved = defaultIsApproved;
|
||||
DefaultMemberTypeAlias = defaultMemberTypeAlias;
|
||||
_defaultCulture = defaultCulture;
|
||||
DefaultMemberGroups = defaultMemberGroups ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A callback executed during account auto-linking and before the user is persisted
|
||||
/// </summary>
|
||||
[IgnoreDataMember]
|
||||
public Action<MemberIdentityUser, ExternalLoginInfo> OnAutoLinking { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A callback executed during every time a user authenticates using an external login.
|
||||
/// returns a boolean indicating if sign in should continue or not.
|
||||
/// </summary>
|
||||
[IgnoreDataMember]
|
||||
public Func<MemberIdentityUser, ExternalLoginInfo, bool> OnExternalLogin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether flag indicating if logging in with the external provider should auto-link/create a
|
||||
/// local user
|
||||
/// </summary>
|
||||
public bool AutoLinkExternalAccount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the member type alias that auto linked members are created as
|
||||
/// </summary>
|
||||
public string DefaultMemberTypeAlias { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the IsApproved value for auto linked members.
|
||||
/// </summary>
|
||||
public bool DefaultIsApproved { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default member groups to add the user in.
|
||||
/// </summary>
|
||||
public IEnumerable<string> DefaultMemberGroups { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The default Culture to use for auto-linking users
|
||||
/// </summary>
|
||||
// TODO: Should we use IDefaultCultureAccessor here instead?
|
||||
public string GetUserAutoLinkCulture(GlobalSettings globalSettings) =>
|
||||
_defaultCulture ?? globalSettings.DefaultUILanguage;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Security
|
||||
{
|
||||
@@ -14,8 +19,25 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
/// <summary>
|
||||
/// The sign in manager for members
|
||||
/// </summary>
|
||||
public class MemberSignInManager : UmbracoSignInManager<MemberIdentityUser>, IMemberSignInManager
|
||||
public class MemberSignInManager : UmbracoSignInManager<MemberIdentityUser>, IMemberSignInManagerExternalLogins
|
||||
{
|
||||
private readonly IMemberExternalLoginProviders _memberExternalLoginProviders;
|
||||
|
||||
public MemberSignInManager(
|
||||
UserManager<MemberIdentityUser> memberManager,
|
||||
IHttpContextAccessor contextAccessor,
|
||||
IUserClaimsPrincipalFactory<MemberIdentityUser> claimsFactory,
|
||||
IOptions<IdentityOptions> optionsAccessor,
|
||||
ILogger<SignInManager<MemberIdentityUser>> logger,
|
||||
IAuthenticationSchemeProvider schemes,
|
||||
IUserConfirmation<MemberIdentityUser> confirmation,
|
||||
IMemberExternalLoginProviders memberExternalLoginProviders) :
|
||||
base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
|
||||
{
|
||||
_memberExternalLoginProviders = memberExternalLoginProviders;
|
||||
}
|
||||
|
||||
[Obsolete("Use ctor with all params")]
|
||||
public MemberSignInManager(
|
||||
UserManager<MemberIdentityUser> memberManager,
|
||||
IHttpContextAccessor contextAccessor,
|
||||
@@ -24,7 +46,7 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
ILogger<SignInManager<MemberIdentityUser>> logger,
|
||||
IAuthenticationSchemeProvider schemes,
|
||||
IUserConfirmation<MemberIdentityUser> confirmation) :
|
||||
base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
|
||||
this(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation, StaticServiceProvider.Instance.GetRequiredService<IMemberExternalLoginProviders>())
|
||||
{ }
|
||||
|
||||
// use default scheme for members
|
||||
@@ -64,16 +86,289 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
=> throw new NotImplementedException("Two factor is not yet implemented for members");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
|
||||
=> throw new NotImplementedException("External login is not yet implemented for members");
|
||||
public override async Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
|
||||
{
|
||||
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422
|
||||
// to replace the auth scheme
|
||||
|
||||
var auth = await Context.AuthenticateAsync(ExternalAuthenticationType);
|
||||
var items = auth?.Properties?.Items;
|
||||
if (auth?.Principal == null || items == null)
|
||||
{
|
||||
Logger.LogDebug(auth?.Failure ?? new NullReferenceException("Context.AuthenticateAsync(ExternalAuthenticationType) is null"),
|
||||
"The external login authentication failed. No user Principal or authentication items was resolved.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!items.ContainsKey(UmbracoSignInMgrLoginProviderKey))
|
||||
{
|
||||
throw new InvalidOperationException($"The external login authenticated successfully but the key {UmbracoSignInMgrLoginProviderKey} was not found in the authentication properties. Ensure you call SignInManager.ConfigureExternalAuthenticationProperties before issuing a ChallengeResult.");
|
||||
}
|
||||
|
||||
if (expectedXsrf != null)
|
||||
{
|
||||
if (!items.ContainsKey(UmbracoSignInMgrXsrfKey))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var userId = items[UmbracoSignInMgrXsrfKey];
|
||||
if (userId != expectedXsrf)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (providerKey == null || items[UmbracoSignInMgrLoginProviderKey] is not string provider)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? provider;
|
||||
return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName)
|
||||
{
|
||||
AuthenticationTokens = auth.Properties.GetTokens(),
|
||||
AuthenticationProperties = auth.Properties
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom ExternalLoginSignInAsync overload for handling external sign in with auto-linking
|
||||
/// </summary>
|
||||
public async Task<SignInResult> ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false)
|
||||
{
|
||||
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
|
||||
// to be able to deal with auto-linking and reduce duplicate lookups
|
||||
|
||||
var autoLinkOptions = (await _memberExternalLoginProviders.GetAsync(loginInfo.LoginProvider))?.ExternalLoginProvider?.Options?.AutoLinkOptions;
|
||||
var user = await UserManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
|
||||
if (user == null)
|
||||
{
|
||||
// user doesn't exist so see if we can auto link
|
||||
return await AutoLinkAndSignInExternalAccount(loginInfo, autoLinkOptions);
|
||||
}
|
||||
|
||||
if (autoLinkOptions != null && autoLinkOptions.OnExternalLogin != null)
|
||||
{
|
||||
var shouldSignIn = autoLinkOptions.OnExternalLogin(user, loginInfo);
|
||||
if (shouldSignIn == false)
|
||||
{
|
||||
LogFailedExternalLogin(loginInfo, user);
|
||||
return ExternalLoginSignInResult.NotAllowed;
|
||||
}
|
||||
}
|
||||
|
||||
var error = await PreSignInCheck(user);
|
||||
if (error != null)
|
||||
{
|
||||
return error;
|
||||
}
|
||||
return await SignInOrTwoFactorAsync(user, isPersistent, loginInfo.LoginProvider, bypassTwoFactor);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Used for auto linking/creating user accounts for external logins
|
||||
/// </summary>
|
||||
/// <param name="loginInfo"></param>
|
||||
/// <param name="autoLinkOptions"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<SignInResult> AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo, MemberExternalSignInAutoLinkOptions autoLinkOptions)
|
||||
{
|
||||
// If there are no autolink options then the attempt is failed (user does not exist)
|
||||
if (autoLinkOptions == null || !autoLinkOptions.AutoLinkExternalAccount)
|
||||
{
|
||||
return SignInResult.Failed;
|
||||
}
|
||||
|
||||
var email = loginInfo.Principal.FindFirstValue(ClaimTypes.Email);
|
||||
|
||||
//we are allowing auto-linking/creating of local accounts
|
||||
if (email.IsNullOrWhiteSpace())
|
||||
{
|
||||
return AutoLinkSignInResult.FailedNoEmail;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Now we need to perform the auto-link, so first we need to lookup/create a user with the email address
|
||||
var autoLinkUser = await UserManager.FindByEmailAsync(email);
|
||||
if (autoLinkUser != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
//call the callback if one is assigned
|
||||
autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider);
|
||||
return AutoLinkSignInResult.FailedException(ex.Message);
|
||||
}
|
||||
|
||||
var shouldLinkUser = autoLinkOptions.OnExternalLogin == null || autoLinkOptions.OnExternalLogin(autoLinkUser, loginInfo);
|
||||
if (shouldLinkUser)
|
||||
{
|
||||
return await LinkUser(autoLinkUser, loginInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogFailedExternalLogin(loginInfo, autoLinkUser);
|
||||
return ExternalLoginSignInResult.NotAllowed;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var name = loginInfo.Principal?.Identity?.Name;
|
||||
if (name.IsNullOrWhiteSpace()) throw new InvalidOperationException("The Name value cannot be null");
|
||||
|
||||
autoLinkUser = MemberIdentityUser.CreateNew(email, email, autoLinkOptions.DefaultMemberTypeAlias, autoLinkOptions.DefaultIsApproved, name);
|
||||
|
||||
foreach (var userGroup in autoLinkOptions.DefaultMemberGroups)
|
||||
{
|
||||
autoLinkUser.AddRole(userGroup);
|
||||
}
|
||||
|
||||
//call the callback if one is assigned
|
||||
try
|
||||
{
|
||||
autoLinkOptions.OnAutoLinking?.Invoke(autoLinkUser, loginInfo);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Could not link login provider {LoginProvider}.", loginInfo.LoginProvider);
|
||||
return AutoLinkSignInResult.FailedException(ex.Message);
|
||||
}
|
||||
|
||||
var userCreationResult = await UserManager.CreateAsync(autoLinkUser);
|
||||
|
||||
if (!userCreationResult.Succeeded)
|
||||
{
|
||||
return AutoLinkSignInResult.FailedCreatingUser(userCreationResult.Errors.Select(x => x.Description).ToList());
|
||||
}
|
||||
else
|
||||
{
|
||||
var shouldLinkUser = autoLinkOptions.OnExternalLogin == null || autoLinkOptions.OnExternalLogin(autoLinkUser, loginInfo);
|
||||
if (shouldLinkUser)
|
||||
{
|
||||
return await LinkUser(autoLinkUser, loginInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogFailedExternalLogin(loginInfo, autoLinkUser);
|
||||
return ExternalLoginSignInResult.NotAllowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO in v10 we can share this with backoffice by moving the backoffice into common.
|
||||
public class ExternalLoginSignInResult : SignInResult
|
||||
{
|
||||
public static ExternalLoginSignInResult NotAllowed { get; } = new ExternalLoginSignInResult()
|
||||
{
|
||||
Succeeded = false
|
||||
};
|
||||
}
|
||||
// TODO in v10 we can share this with backoffice by moving the backoffice into common.
|
||||
public class AutoLinkSignInResult : SignInResult
|
||||
{
|
||||
public static AutoLinkSignInResult FailedNotLinked { get; } = new AutoLinkSignInResult()
|
||||
{
|
||||
Succeeded = false
|
||||
};
|
||||
|
||||
public static AutoLinkSignInResult FailedNoEmail { get; } = new AutoLinkSignInResult()
|
||||
{
|
||||
Succeeded = false
|
||||
};
|
||||
|
||||
public static AutoLinkSignInResult FailedException(string error) => new AutoLinkSignInResult(new[] { error })
|
||||
{
|
||||
Succeeded = false
|
||||
};
|
||||
|
||||
public static AutoLinkSignInResult FailedCreatingUser(IReadOnlyCollection<string> errors) => new AutoLinkSignInResult(errors)
|
||||
{
|
||||
Succeeded = false
|
||||
};
|
||||
|
||||
public static AutoLinkSignInResult FailedLinkingUser(IReadOnlyCollection<string> errors) => new AutoLinkSignInResult(errors)
|
||||
{
|
||||
Succeeded = false
|
||||
};
|
||||
|
||||
public AutoLinkSignInResult(IReadOnlyCollection<string> errors)
|
||||
{
|
||||
Errors = errors ?? throw new ArgumentNullException(nameof(errors));
|
||||
}
|
||||
|
||||
public AutoLinkSignInResult()
|
||||
{
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<string> Errors { get; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null)
|
||||
=> throw new NotImplementedException("External login is not yet implemented for members");
|
||||
{
|
||||
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
|
||||
// to be able to use our own XsrfKey/LoginProviderKey because the default is private :/
|
||||
|
||||
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
|
||||
properties.Items[UmbracoSignInMgrLoginProviderKey] = provider;
|
||||
if (userId != null)
|
||||
{
|
||||
properties.Items[UmbracoSignInMgrXsrfKey] = userId;
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<IEnumerable<AuthenticationScheme>> GetExternalAuthenticationSchemesAsync()
|
||||
=> throw new NotImplementedException("External login is not yet implemented for members");
|
||||
{
|
||||
// That can be done by either checking the scheme (maybe) or comparing it to what we have registered in the collection of BackOfficeExternalLoginProvider
|
||||
return base.GetExternalAuthenticationSchemesAsync();
|
||||
}
|
||||
|
||||
private async Task<SignInResult> LinkUser(MemberIdentityUser autoLinkUser, ExternalLoginInfo loginInfo)
|
||||
{
|
||||
var existingLogins = await UserManager.GetLoginsAsync(autoLinkUser);
|
||||
var exists = existingLogins.FirstOrDefault(x => x.LoginProvider == loginInfo.LoginProvider && x.ProviderKey == loginInfo.ProviderKey);
|
||||
|
||||
// if it already exists (perhaps it was added in the AutoLink callbak) then we just continue
|
||||
if (exists != null)
|
||||
{
|
||||
//sign in
|
||||
return await SignInOrTwoFactorAsync(autoLinkUser, isPersistent: false, loginInfo.LoginProvider);
|
||||
}
|
||||
|
||||
var linkResult = await UserManager.AddLoginAsync(autoLinkUser, loginInfo);
|
||||
if (linkResult.Succeeded)
|
||||
{
|
||||
//we're good! sign in
|
||||
return await SignInOrTwoFactorAsync(autoLinkUser, isPersistent: false, loginInfo.LoginProvider);
|
||||
}
|
||||
|
||||
//If this fails, we should really delete the user since it will be in an inconsistent state!
|
||||
var deleteResult = await UserManager.DeleteAsync(autoLinkUser);
|
||||
if (deleteResult.Succeeded)
|
||||
{
|
||||
var errors = linkResult.Errors.Select(x => x.Description).ToList();
|
||||
return AutoLinkSignInResult.FailedLinkingUser(errors);
|
||||
}
|
||||
else
|
||||
{
|
||||
//DOH! ... this isn't good, combine all errors to be shown
|
||||
var errors = linkResult.Errors.Concat(deleteResult.Errors).Select(x => x.Description).ToList();
|
||||
return AutoLinkSignInResult.FailedLinkingUser(errors);
|
||||
}
|
||||
}
|
||||
|
||||
private void LogFailedExternalLogin(ExternalLoginInfo loginInfo, MemberIdentityUser user) =>
|
||||
Logger.LogWarning("The AutoLinkOptions of the external authentication provider '{LoginProvider}' have refused the login based on the OnExternalLogin method. Affected user id: '{UserId}'", loginInfo.LoginProvider, user.Id);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<staticContent>
|
||||
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
|
||||
</staticContent>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
@@ -69,7 +69,7 @@
|
||||
ng-click="unlink($event, login.authType, login.linkedProviderKey)"
|
||||
ng-if="login.linkedProviderKey != undefined"
|
||||
value="{{login.authType}}">
|
||||
<umb-icon icon="fa {{login.properties.SocialIcon}}"></umb-icon>
|
||||
<umb-icon icon="{{login.properties.Icon}}" style="height:100%;"></umb-icon>
|
||||
<localize key="defaultdialogs_unLinkYour">Un-link your</localize> {{login.caption}} <localize
|
||||
key="defaultdialogs_account">account
|
||||
</localize>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0"?>
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<staticContent>
|
||||
<clientCache cacheControlCustom="private" cacheControlMode="UseMaxAge" cacheControlMaxAge="3.00:00:00" />
|
||||
</staticContent>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
@@ -102,8 +102,6 @@
|
||||
<Message Text="Generate the appsettings json schema." Importance="High" Condition="!Exists('$(JsonSchemaPath)') and '$(UmbracoBuild)' == ''" />
|
||||
|
||||
<CallTarget Targets="JsonSchemaBuild" Condition="!Exists('$(JsonSchemaPath)') and '$(UmbracoBuild)' == ''" />
|
||||
|
||||
|
||||
<CallTarget Targets="AppsettingsBuild" Condition="!Exists('appsettings.json')" />
|
||||
<CallTarget Targets="AppsettingsDevBuild" Condition="!Exists('appsettings.Development.json')" />
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
@inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage
|
||||
|
||||
@using Umbraco.Cms.Core
|
||||
@using Umbraco.Cms.Core.Security
|
||||
@using Umbraco.Cms.Core.Services
|
||||
@using Umbraco.Cms.Web.Common.Security
|
||||
@using Umbraco.Cms.Web.Website.Controllers
|
||||
@using Umbraco.Cms.Web.Website.Models
|
||||
@using Umbraco.Extensions
|
||||
@inject MemberModelBuilderFactory memberModelBuilderFactory;
|
||||
|
||||
@inject IMemberExternalLoginProviders memberExternalLoginProviders
|
||||
@inject IExternalLoginWithKeyService externalLoginWithKeyService
|
||||
@{
|
||||
// Build a profile model to edit
|
||||
var profileModel = await memberModelBuilderFactory
|
||||
@@ -17,6 +21,13 @@
|
||||
.BuildForCurrentMemberAsync();
|
||||
|
||||
var success = TempData["FormSuccess"] != null;
|
||||
|
||||
var loginProviders = await memberExternalLoginProviders.GetMemberProvidersAsync();
|
||||
var externalSignInError = ViewData.GetExternalSignInProviderErrors();
|
||||
|
||||
var currentExternalLogin = profileModel is null
|
||||
? new Dictionary<string, string>()
|
||||
: externalLoginWithKeyService.GetExternalLogins(profileModel.Key).ToDictionary(x=>x.LoginProvider, x=>x.ProviderKey);
|
||||
}
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
|
||||
@@ -70,5 +81,50 @@
|
||||
}
|
||||
|
||||
<button type="submit" class="btn btn-primary">Update</button>
|
||||
|
||||
if (loginProviders.Any())
|
||||
{
|
||||
<hr/>
|
||||
<h4>Link external accounts</h4>
|
||||
|
||||
if (externalSignInError?.AuthenticationType is null && externalSignInError?.Errors.Any() == true)
|
||||
{
|
||||
@Html.DisplayFor(x => externalSignInError.Errors);
|
||||
}
|
||||
|
||||
@foreach (var login in loginProviders)
|
||||
{
|
||||
if (currentExternalLogin.TryGetValue(login.ExternalLoginProvider.AuthenticationType, out var providerKey))
|
||||
{
|
||||
@using (Html.BeginUmbracoForm<UmbExternalLoginController>(nameof(UmbExternalLoginController.Disassociate)))
|
||||
{
|
||||
<input type="hidden" name="providerKey" value="@providerKey"/>
|
||||
<button type="submit" name="provider" value="@login.ExternalLoginProvider.AuthenticationType">
|
||||
Un-Link your @login.AuthenticationScheme.DisplayName account
|
||||
</button>
|
||||
|
||||
if (externalSignInError?.AuthenticationType == login.ExternalLoginProvider.AuthenticationType)
|
||||
{
|
||||
@Html.DisplayFor(x => externalSignInError.Errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@using (Html.BeginUmbracoForm<UmbExternalLoginController>(nameof(UmbExternalLoginController.LinkLogin)))
|
||||
{
|
||||
<button type="submit" name="provider" value="@login.ExternalLoginProvider.AuthenticationType">
|
||||
Link your @login.AuthenticationScheme.DisplayName account
|
||||
</button>
|
||||
|
||||
if (externalSignInError?.AuthenticationType == login.ExternalLoginProvider.AuthenticationType)
|
||||
{
|
||||
@Html.DisplayFor(x => externalSignInError.Errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
@inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage
|
||||
@using Microsoft.AspNetCore.Http.Extensions
|
||||
@using Umbraco.Cms.Web.Common.Models
|
||||
@using Umbraco.Cms.Web.Common.Security
|
||||
@using Umbraco.Cms.Web.Website.Controllers
|
||||
@using Umbraco.Extensions
|
||||
|
||||
@inject IMemberExternalLoginProviders memberExternalLoginProviders
|
||||
@{
|
||||
var loginModel = new LoginModel();
|
||||
// You can modify this to redirect to a different URL instead of the current one
|
||||
@@ -40,6 +40,37 @@
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Log in</button>
|
||||
|
||||
|
||||
}
|
||||
@{
|
||||
var loginProviders = await memberExternalLoginProviders.GetMemberProvidersAsync();
|
||||
var externalSignInError = ViewData.GetExternalSignInProviderErrors();
|
||||
|
||||
if (loginProviders.Any())
|
||||
{
|
||||
<hr/>
|
||||
<h4>Or using external providers</h4>
|
||||
if (externalSignInError?.AuthenticationType is null && externalSignInError?.Errors.Any() == true)
|
||||
{
|
||||
@Html.DisplayFor(x => externalSignInError.Errors);
|
||||
}
|
||||
|
||||
@foreach (var login in await memberExternalLoginProviders.GetMemberProvidersAsync())
|
||||
{
|
||||
|
||||
@using (Html.BeginUmbracoForm<UmbExternalLoginController>(nameof(UmbExternalLoginController.ExternalLogin)))
|
||||
{
|
||||
<button type="submit" name="provider" value="@login.ExternalLoginProvider.AuthenticationType">
|
||||
Sign in with @login.AuthenticationScheme.DisplayName
|
||||
</button>
|
||||
|
||||
if (externalSignInError?.AuthenticationType == login.ExternalLoginProvider.AuthenticationType)
|
||||
{
|
||||
@Html.DisplayFor(x => externalSignInError.Errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Web.Common.ActionsResults;
|
||||
using Umbraco.Cms.Web.Common.Filters;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
using Umbraco.Extensions;
|
||||
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
|
||||
namespace Umbraco.Cms.Web.Website.Controllers
|
||||
{
|
||||
[UmbracoMemberAuthorize]
|
||||
public class UmbExternalLoginController : SurfaceController
|
||||
{
|
||||
private readonly IMemberManager _memberManager;
|
||||
private readonly IMemberSignInManagerExternalLogins _memberSignInManager;
|
||||
|
||||
public UmbExternalLoginController(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
ServiceContext services,
|
||||
AppCaches appCaches,
|
||||
IProfilingLogger profilingLogger,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IMemberSignInManagerExternalLogins memberSignInManager,
|
||||
IMemberManager memberManager)
|
||||
: base(
|
||||
umbracoContextAccessor,
|
||||
databaseFactory,
|
||||
services,
|
||||
appCaches,
|
||||
profilingLogger,
|
||||
publishedUrlProvider)
|
||||
{
|
||||
_memberSignInManager = memberSignInManager;
|
||||
_memberManager = memberManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint used to redirect to a specific login provider. This endpoint is used from the Login Macro snippet.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public ActionResult ExternalLogin(string provider, string returnUrl = null)
|
||||
{
|
||||
if (returnUrl.IsNullOrWhiteSpace())
|
||||
{
|
||||
returnUrl = Request.GetEncodedPathAndQuery();
|
||||
}
|
||||
|
||||
var wrappedReturnUrl =
|
||||
Url.SurfaceAction(nameof(ExternalLoginCallback), this.GetControllerName(), new { returnUrl });
|
||||
|
||||
AuthenticationProperties properties =
|
||||
_memberSignInManager.ConfigureExternalAuthenticationProperties(provider, wrappedReturnUrl);
|
||||
|
||||
return Challenge(properties, provider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Endpoint used my the login provider to call back to our solution.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ExternalLoginCallback(string returnUrl)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
ExternalLoginInfo loginInfo = await _memberSignInManager.GetExternalLoginInfoAsync();
|
||||
if (loginInfo is null)
|
||||
{
|
||||
errors.Add("Invalid response from the login provider");
|
||||
}
|
||||
else
|
||||
{
|
||||
SignInResult result = await _memberSignInManager.ExternalLoginSignInAsync(loginInfo, false);
|
||||
|
||||
if (result == SignInResult.Success)
|
||||
{
|
||||
// Update any authentication tokens if succeeded
|
||||
await _memberSignInManager.UpdateExternalAuthenticationTokensAsync(loginInfo);
|
||||
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
if (result == SignInResult.TwoFactorRequired)
|
||||
{
|
||||
MemberIdentityUser attemptedUser =
|
||||
await _memberManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
|
||||
if (attemptedUser == null)
|
||||
{
|
||||
return new ValidationErrorResult(
|
||||
$"No local user found for the login provider {loginInfo.LoginProvider} - {loginInfo.ProviderKey}");
|
||||
}
|
||||
|
||||
// create a with information to display a custom two factor send code view
|
||||
var verifyResponse =
|
||||
new ObjectResult(new { userId = attemptedUser.Id })
|
||||
{
|
||||
StatusCode = StatusCodes.Status402PaymentRequired
|
||||
};
|
||||
|
||||
return verifyResponse;
|
||||
}
|
||||
|
||||
if (result == SignInResult.LockedOut)
|
||||
{
|
||||
errors.Add(
|
||||
$"The local member {loginInfo.Principal.Identity.Name} for the external provider {loginInfo.ProviderDisplayName} is locked out.");
|
||||
}
|
||||
else if (result == SignInResult.NotAllowed)
|
||||
{
|
||||
// This occurs when SignInManager.CanSignInAsync fails which is when RequireConfirmedEmail , RequireConfirmedPhoneNumber or RequireConfirmedAccount fails
|
||||
// however since we don't enforce those rules (yet) this shouldn't happen.
|
||||
errors.Add(
|
||||
$"The member {loginInfo.Principal.Identity.Name} for the external provider {loginInfo.ProviderDisplayName} has not confirmed their details and cannot sign in.");
|
||||
}
|
||||
else if (result == SignInResult.Failed)
|
||||
{
|
||||
// Failed only occurs when the user does not exist
|
||||
errors.Add("The requested provider (" + loginInfo.LoginProvider +
|
||||
") has not been linked to an account, the provider must be linked before it can be used.");
|
||||
}
|
||||
else if (result == MemberSignInManager.ExternalLoginSignInResult.NotAllowed)
|
||||
{
|
||||
// This occurs when the external provider has approved the login but custom logic in OnExternalLogin has denined it.
|
||||
errors.Add(
|
||||
$"The user {loginInfo.Principal.Identity.Name} for the external provider {loginInfo.ProviderDisplayName} has not been accepted and cannot sign in.");
|
||||
}
|
||||
else if (result == MemberSignInManager.AutoLinkSignInResult.FailedNotLinked)
|
||||
{
|
||||
errors.Add("The requested provider (" + loginInfo.LoginProvider +
|
||||
") has not been linked to an account, the provider must be linked from the back office.");
|
||||
}
|
||||
else if (result == MemberSignInManager.AutoLinkSignInResult.FailedNoEmail)
|
||||
{
|
||||
errors.Add(
|
||||
$"The requested provider ({loginInfo.LoginProvider}) has not provided the email claim {ClaimTypes.Email}, the account cannot be linked.");
|
||||
}
|
||||
else if (result is MemberSignInManager.AutoLinkSignInResult autoLinkSignInResult &&
|
||||
autoLinkSignInResult.Errors.Count > 0)
|
||||
{
|
||||
errors.AddRange(autoLinkSignInResult.Errors);
|
||||
}
|
||||
else if (!result.Succeeded)
|
||||
{
|
||||
// this shouldn't occur, the above should catch the correct error but we'll be safe just in case
|
||||
errors.Add($"An unknown error with the requested provider ({loginInfo.LoginProvider}) occurred.");
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
ViewData.SetExternalSignInProviderErrors(
|
||||
new BackOfficeExternalLoginProviderErrors(
|
||||
loginInfo?.LoginProvider,
|
||||
errors));
|
||||
|
||||
return CurrentUmbracoPage();
|
||||
}
|
||||
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
private void AddModelErrors(IdentityResult result, string prefix = "")
|
||||
{
|
||||
foreach (IdentityError error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(prefix, error.Description);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public IActionResult LinkLogin(string provider, string returnUrl = null)
|
||||
{
|
||||
if (returnUrl.IsNullOrWhiteSpace())
|
||||
{
|
||||
returnUrl = Request.GetEncodedPathAndQuery();
|
||||
}
|
||||
|
||||
var wrappedReturnUrl =
|
||||
Url.SurfaceAction(nameof(ExternalLinkLoginCallback), this.GetControllerName(), new { returnUrl });
|
||||
|
||||
// Configures the redirect URL and user identifier for the specified external login including xsrf data
|
||||
AuthenticationProperties properties =
|
||||
_memberSignInManager.ConfigureExternalAuthenticationProperties(provider, wrappedReturnUrl,
|
||||
_memberManager.GetUserId(User));
|
||||
|
||||
return Challenge(properties, provider);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ExternalLinkLoginCallback(string returnUrl)
|
||||
{
|
||||
MemberIdentityUser user = await _memberManager.GetUserAsync(User);
|
||||
string loginProvider = null;
|
||||
var errors = new List<string>();
|
||||
if (user == null)
|
||||
{
|
||||
// ... this should really not happen
|
||||
errors.Add("Local user does not exist");
|
||||
}
|
||||
else
|
||||
{
|
||||
ExternalLoginInfo info =
|
||||
await _memberSignInManager.GetExternalLoginInfoAsync(await _memberManager.GetUserIdAsync(user));
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
//Add error and redirect for it to be displayed
|
||||
errors.Add( "An error occurred, could not get external login info");
|
||||
}
|
||||
else
|
||||
{
|
||||
loginProvider = info.LoginProvider;
|
||||
IdentityResult addLoginResult = await _memberManager.AddLoginAsync(user, info);
|
||||
if (addLoginResult.Succeeded)
|
||||
{
|
||||
// Update any authentication tokens if succeeded
|
||||
await _memberSignInManager.UpdateExternalAuthenticationTokensAsync(info);
|
||||
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
//Add errors and redirect for it to be displayed
|
||||
errors.AddRange(addLoginResult.Errors.Select(x => x.Description));
|
||||
}
|
||||
}
|
||||
|
||||
ViewData.SetExternalSignInProviderErrors(
|
||||
new BackOfficeExternalLoginProviderErrors(
|
||||
loginProvider,
|
||||
errors));
|
||||
return CurrentUmbracoPage();
|
||||
}
|
||||
|
||||
private IActionResult RedirectToLocal(string returnUrl) =>
|
||||
Url.IsLocalUrl(returnUrl) ? Redirect(returnUrl) : RedirectToCurrentUmbracoPage();
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Disassociate(string provider, string providerKey, string returnUrl = null)
|
||||
{
|
||||
if (returnUrl.IsNullOrWhiteSpace())
|
||||
{
|
||||
returnUrl = Request.GetEncodedPathAndQuery();
|
||||
}
|
||||
|
||||
MemberIdentityUser user = await _memberManager.FindByIdAsync(User.Identity.GetUserId());
|
||||
|
||||
IdentityResult result = await _memberManager.RemoveLoginAsync(user, provider, providerKey);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
await _memberSignInManager.SignInAsync(user, false);
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
AddModelErrors(result);
|
||||
return CurrentUmbracoPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Web.Website.Security;
|
||||
|
||||
|
||||
namespace Umbraco.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="IUmbracoBuilder"/> for the Umbraco back office
|
||||
/// </summary>
|
||||
public static partial class UmbracoBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds support for external login providers in Umbraco
|
||||
/// </summary>
|
||||
public static IUmbracoBuilder AddMemberExternalLogins(this IUmbracoBuilder umbracoBuilder, Action<MemberExternalLoginsBuilder> builder)
|
||||
{
|
||||
builder(new MemberExternalLoginsBuilder(umbracoBuilder.Services));
|
||||
return umbracoBuilder;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ namespace Umbraco.Extensions
|
||||
/// <summary>
|
||||
/// <see cref="IUmbracoBuilder"/> extensions for umbraco front-end website
|
||||
/// </summary>
|
||||
public static class UmbracoBuilderExtensions
|
||||
public static partial class UmbracoBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Add services for the umbraco front-end website
|
||||
|
||||
@@ -12,6 +12,10 @@ namespace Umbraco.Cms.Web.Website.Models
|
||||
/// </summary>
|
||||
public class ProfileModel : PostRedirectModel
|
||||
{
|
||||
|
||||
[ReadOnly(true)]
|
||||
public Guid Key { get; set; }
|
||||
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
[Display(Name = "Email")]
|
||||
|
||||
@@ -68,7 +68,8 @@ namespace Umbraco.Cms.Web.Website.Models
|
||||
CreatedDate = member.CreatedDateUtc.ToLocalTime(),
|
||||
LastLoginDate = member.LastLoginDateUtc?.ToLocalTime(),
|
||||
LastPasswordChangedDate = member.LastPasswordChangeDateUtc?.ToLocalTime(),
|
||||
RedirectUrl = _redirectUrl
|
||||
RedirectUrl = _redirectUrl,
|
||||
Key = member.Key
|
||||
};
|
||||
|
||||
IMemberType memberType = MemberTypeService.Get(member.MemberTypeAlias);
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
using Umbraco.Extensions;
|
||||
using Constants = Umbraco.Cms.Core.Constants;
|
||||
|
||||
namespace Umbraco.Cms.Web.Website.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom <see cref="AuthenticationBuilder"/> used to associate external logins with umbraco external login options
|
||||
/// </summary>
|
||||
public class MemberAuthenticationBuilder : AuthenticationBuilder
|
||||
{
|
||||
private readonly Action<MemberExternalLoginProviderOptions> _loginProviderOptions;
|
||||
|
||||
public MemberAuthenticationBuilder(
|
||||
IServiceCollection services,
|
||||
Action<MemberExternalLoginProviderOptions> loginProviderOptions = null)
|
||||
: base(services)
|
||||
=> _loginProviderOptions = loginProviderOptions ?? (x => { });
|
||||
|
||||
public string SchemeForMembers(string scheme)
|
||||
=> scheme?.EnsureStartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix);
|
||||
|
||||
/// <summary>
|
||||
/// Overridden to track the final authenticationScheme being registered for the external login
|
||||
/// </summary>
|
||||
/// <typeparam name="TOptions"></typeparam>
|
||||
/// <typeparam name="THandler"></typeparam>
|
||||
/// <param name="authenticationScheme"></param>
|
||||
/// <param name="displayName"></param>
|
||||
/// <param name="configureOptions"></param>
|
||||
/// <returns></returns>
|
||||
public override AuthenticationBuilder AddRemoteScheme<TOptions, THandler>(string authenticationScheme, string displayName, Action<TOptions> configureOptions)
|
||||
{
|
||||
// Validate that the prefix is set
|
||||
if (!authenticationScheme.StartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix))
|
||||
{
|
||||
throw new InvalidOperationException($"The {nameof(authenticationScheme)} is not prefixed with {Constants.Security.BackOfficeExternalAuthenticationTypePrefix}. The scheme must be created with a call to the method {nameof(SchemeForMembers)}");
|
||||
}
|
||||
|
||||
// add our login provider to the container along with a custom options configuration
|
||||
Services.Configure(authenticationScheme, _loginProviderOptions);
|
||||
base.Services.AddSingleton(services =>
|
||||
{
|
||||
return new MemberExternalLoginProvider(
|
||||
authenticationScheme,
|
||||
services.GetRequiredService<IOptionsMonitor<MemberExternalLoginProviderOptions>>());
|
||||
});
|
||||
Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, EnsureMemberScheme<TOptions>>());
|
||||
|
||||
return base.AddRemoteScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
|
||||
}
|
||||
|
||||
// Ensures that the sign in scheme is always the Umbraco member external type
|
||||
private class EnsureMemberScheme<TOptions> : IPostConfigureOptions<TOptions> where TOptions : RemoteAuthenticationOptions
|
||||
{
|
||||
public void PostConfigure(string name, TOptions options)
|
||||
{
|
||||
if (!name.StartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
options.SignInScheme = IdentityConstants.ExternalScheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
|
||||
namespace Umbraco.Cms.Web.Website.Security
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to add back office login providers
|
||||
/// </summary>
|
||||
public class MemberExternalLoginsBuilder
|
||||
{
|
||||
public MemberExternalLoginsBuilder(IServiceCollection services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
private readonly IServiceCollection _services;
|
||||
|
||||
/// <summary>
|
||||
/// Add a back office login provider with options
|
||||
/// </summary>
|
||||
/// <param name="loginProviderOptions"></param>
|
||||
/// <param name="build"></param>
|
||||
/// <returns></returns>
|
||||
public MemberExternalLoginsBuilder AddMemberLogin(
|
||||
Action<MemberAuthenticationBuilder> build,
|
||||
Action<MemberExternalLoginProviderOptions> loginProviderOptions = null)
|
||||
{
|
||||
build(new MemberAuthenticationBuilder(_services, loginProviderOptions));
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security
|
||||
{
|
||||
private IUserService UserService => GetRequiredService<IUserService>();
|
||||
private IEntityService EntityService => GetRequiredService<IEntityService>();
|
||||
private IExternalLoginService ExternalLoginService => GetRequiredService<IExternalLoginService>();
|
||||
private IExternalLoginWithKeyService ExternalLoginService => GetRequiredService<IExternalLoginWithKeyService>();
|
||||
private IUmbracoMapper UmbracoMapper => GetRequiredService<IUmbracoMapper>();
|
||||
private ILocalizedTextService TextService => GetRequiredService<ILocalizedTextService>();
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
|
||||
{
|
||||
private IUserService UserService => GetRequiredService<IUserService>();
|
||||
|
||||
private IExternalLoginService ExternalLoginService => (IExternalLoginService)GetRequiredService<IExternalLoginService>();
|
||||
private IExternalLoginWithKeyService ExternalLoginService => GetRequiredService<IExternalLoginWithKeyService>();
|
||||
|
||||
[Test]
|
||||
[Ignore("We don't support duplicates anymore, this removing on save was a breaking change work around, this needs to be ported to a migration")]
|
||||
@@ -38,14 +38,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
|
||||
// insert duplicates manuall
|
||||
scope.Database.Insert(new ExternalLoginDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
UserOrMemberKey = user.Key,
|
||||
LoginProvider = "test1",
|
||||
ProviderKey = providerKey,
|
||||
CreateDate = latest
|
||||
});
|
||||
scope.Database.Insert(new ExternalLoginDto
|
||||
{
|
||||
UserId = user.Id,
|
||||
UserOrMemberKey = user.Key,
|
||||
LoginProvider = "test1",
|
||||
ProviderKey = providerKey,
|
||||
CreateDate = oldest
|
||||
@@ -60,9 +60,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
|
||||
new ExternalLogin("test1", providerKey)
|
||||
};
|
||||
|
||||
ExternalLoginService.Save(user.Id, externalLogins);
|
||||
ExternalLoginService.Save(user.Key, externalLogins);
|
||||
|
||||
var logins = ExternalLoginService.GetExternalLogins(user.Id).ToList();
|
||||
var logins = ExternalLoginService.GetExternalLogins(user.Key).ToList();
|
||||
|
||||
// duplicates will be removed, keeping the latest entries
|
||||
Assert.AreEqual(2, logins.Count);
|
||||
@@ -84,9 +84,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
|
||||
new ExternalLogin("test1", providerKey)
|
||||
};
|
||||
|
||||
ExternalLoginService.Save(user.Id, externalLogins);
|
||||
ExternalLoginService.Save(user.Key, externalLogins);
|
||||
|
||||
var logins = ExternalLoginService.GetExternalLogins(user.Id).ToList();
|
||||
var logins = ExternalLoginService.GetExternalLogins(user.Key).ToList();
|
||||
Assert.AreEqual(1, logins.Count);
|
||||
}
|
||||
|
||||
@@ -103,16 +103,16 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
|
||||
new ExternalLogin("test1", providerKey1, "hello"),
|
||||
new ExternalLogin("test2", providerKey2, "world")
|
||||
};
|
||||
ExternalLoginService.Save(user.Id, extLogins);
|
||||
ExternalLoginService.Save(user.Key, extLogins);
|
||||
|
||||
extLogins = new[]
|
||||
{
|
||||
new ExternalLogin("test1", providerKey1, "123456"),
|
||||
new ExternalLogin("test2", providerKey2, "987654")
|
||||
};
|
||||
ExternalLoginService.Save(user.Id, extLogins);
|
||||
ExternalLoginService.Save(user.Key, extLogins);
|
||||
|
||||
var found = ExternalLoginService.GetExternalLogins(user.Id).OrderBy(x => x.LoginProvider).ToList();
|
||||
var found = ExternalLoginService.GetExternalLogins(user.Key).OrderBy(x => x.LoginProvider).ToList();
|
||||
Assert.AreEqual(2, found.Count);
|
||||
Assert.AreEqual("123456", found[0].UserData);
|
||||
Assert.AreEqual("987654", found[1].UserData);
|
||||
@@ -131,7 +131,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
|
||||
new ExternalLogin("test1", providerKey1, "hello"),
|
||||
new ExternalLogin("test2", providerKey2, "world")
|
||||
};
|
||||
ExternalLoginService.Save(user.Id, extLogins);
|
||||
ExternalLoginService.Save(user.Key, extLogins);
|
||||
|
||||
var found = ExternalLoginService.Find("test2", providerKey2).ToList();
|
||||
Assert.AreEqual(1, found.Count);
|
||||
@@ -151,9 +151,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
|
||||
new ExternalLogin("test2", Guid.NewGuid().ToString("N"))
|
||||
};
|
||||
|
||||
ExternalLoginService.Save(user.Id, externalLogins);
|
||||
ExternalLoginService.Save(user.Key, externalLogins);
|
||||
|
||||
var logins = ExternalLoginService.GetExternalLogins(user.Id).OrderBy(x => x.LoginProvider).ToList();
|
||||
var logins = ExternalLoginService.GetExternalLogins(user.Key).OrderBy(x => x.LoginProvider).ToList();
|
||||
Assert.AreEqual(2, logins.Count);
|
||||
for (int i = 0; i < logins.Count; i++)
|
||||
{
|
||||
@@ -173,7 +173,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
|
||||
new ExternalLogin("test1", Guid.NewGuid().ToString("N"))
|
||||
};
|
||||
|
||||
ExternalLoginService.Save(user.Id, externalLogins);
|
||||
ExternalLoginService.Save(user.Key, externalLogins);
|
||||
|
||||
ExternalLoginToken[] externalTokens = new[]
|
||||
{
|
||||
@@ -181,9 +181,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
|
||||
new ExternalLoginToken(externalLogins[0].LoginProvider, "hello2", "world2")
|
||||
};
|
||||
|
||||
ExternalLoginService.Save(user.Id, externalTokens);
|
||||
ExternalLoginService.Save(user.Key, externalTokens);
|
||||
|
||||
var tokens = ExternalLoginService.GetExternalLoginTokens(user.Id).ToList();
|
||||
var tokens = ExternalLoginService.GetExternalLoginTokens(user.Key).ToList();
|
||||
Assert.AreEqual(2, tokens.Count);
|
||||
}
|
||||
|
||||
@@ -201,18 +201,18 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
|
||||
new ExternalLogin("test4", Guid.NewGuid().ToString("N"))
|
||||
};
|
||||
|
||||
ExternalLoginService.Save(user.Id, externalLogins);
|
||||
ExternalLoginService.Save(user.Key, externalLogins);
|
||||
|
||||
var logins = ExternalLoginService.GetExternalLogins(user.Id).OrderBy(x => x.LoginProvider).ToList();
|
||||
var logins = ExternalLoginService.GetExternalLogins(user.Key).OrderBy(x => x.LoginProvider).ToList();
|
||||
|
||||
logins.RemoveAt(0); // remove the first one
|
||||
logins.Add(new IdentityUserLogin("test5", Guid.NewGuid().ToString("N"), user.Id.ToString())); // add a new one
|
||||
logins[0].ProviderKey = "abcd123"; // update
|
||||
|
||||
// save new list
|
||||
ExternalLoginService.Save(user.Id, logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey)));
|
||||
ExternalLoginService.Save(user.Key, logins.Select(x => new ExternalLogin(x.LoginProvider, x.ProviderKey)));
|
||||
|
||||
var updatedLogins = ExternalLoginService.GetExternalLogins(user.Id).OrderBy(x => x.LoginProvider).ToList();
|
||||
var updatedLogins = ExternalLoginService.GetExternalLogins(user.Key).OrderBy(x => x.LoginProvider).ToList();
|
||||
Assert.AreEqual(4, updatedLogins.Count);
|
||||
for (int i = 0; i < updatedLogins.Count; i++)
|
||||
{
|
||||
@@ -233,7 +233,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
|
||||
new ExternalLogin("test2", Guid.NewGuid().ToString("N"))
|
||||
};
|
||||
|
||||
ExternalLoginService.Save(user.Id, externalLogins);
|
||||
ExternalLoginService.Save(user.Key, externalLogins);
|
||||
|
||||
ExternalLoginToken[] externalTokens = new[]
|
||||
{
|
||||
@@ -243,18 +243,18 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
|
||||
new ExternalLoginToken(externalLogins[1].LoginProvider, "hello2a", "world2a")
|
||||
};
|
||||
|
||||
ExternalLoginService.Save(user.Id, externalTokens);
|
||||
ExternalLoginService.Save(user.Key, externalTokens);
|
||||
|
||||
var tokens = ExternalLoginService.GetExternalLoginTokens(user.Id).OrderBy(x => x.LoginProvider).ToList();
|
||||
var tokens = ExternalLoginService.GetExternalLoginTokens(user.Key).OrderBy(x => x.LoginProvider).ToList();
|
||||
|
||||
tokens.RemoveAt(0); // remove the first one
|
||||
tokens.Add(new IdentityUserToken(externalLogins[1].LoginProvider, "hello2b", "world2b", user.Id.ToString())); // add a new one
|
||||
tokens[0].Value = "abcd123"; // update
|
||||
|
||||
// save new list
|
||||
ExternalLoginService.Save(user.Id, tokens.Select(x => new ExternalLoginToken(x.LoginProvider, x.Name, x.Value)));
|
||||
ExternalLoginService.Save(user.Key, tokens.Select(x => new ExternalLoginToken(x.LoginProvider, x.Name, x.Value)));
|
||||
|
||||
var updatedTokens = ExternalLoginService.GetExternalLoginTokens(user.Id).OrderBy(x => x.LoginProvider).ToList();
|
||||
var updatedTokens = ExternalLoginService.GetExternalLoginTokens(user.Key).OrderBy(x => x.LoginProvider).ToList();
|
||||
Assert.AreEqual(4, updatedTokens.Count);
|
||||
for (int i = 0; i < updatedTokens.Count; i++)
|
||||
{
|
||||
@@ -275,9 +275,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
|
||||
new ExternalLogin("test1", Guid.NewGuid().ToString("N"), "hello world")
|
||||
};
|
||||
|
||||
ExternalLoginService.Save(user.Id, externalLogins);
|
||||
ExternalLoginService.Save(user.Key, externalLogins);
|
||||
|
||||
var logins = ExternalLoginService.GetExternalLogins(user.Id).ToList();
|
||||
var logins = ExternalLoginService.GetExternalLogins(user.Key).ToList();
|
||||
|
||||
Assert.AreEqual("hello world", logins[0].UserData);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security
|
||||
new UmbracoMapper(new MapDefinitionCollection(() => mapDefinitions), scopeProvider),
|
||||
scopeProvider,
|
||||
new IdentityErrorDescriber(),
|
||||
Mock.Of<IPublishedSnapshotAccessor>());
|
||||
Mock.Of<IPublishedSnapshotAccessor>(), Mock.Of<IExternalLoginWithKeyService>());
|
||||
|
||||
_mockIdentityOptions = new Mock<IOptions<IdentityOptions>>();
|
||||
var idOptions = new IdentityOptions { Lockout = { AllowedForNewUsers = false } };
|
||||
|
||||
@@ -37,7 +37,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Security
|
||||
new UmbracoMapper(new MapDefinitionCollection(() => new List<IMapDefinition>()), mockScopeProvider.Object),
|
||||
mockScopeProvider.Object,
|
||||
new IdentityErrorDescriber(),
|
||||
Mock.Of<IPublishedSnapshotAccessor>());
|
||||
Mock.Of<IPublishedSnapshotAccessor>(),
|
||||
Mock.Of<IExternalLoginWithKeyService>()
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -37,7 +37,17 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security
|
||||
serviceCollection
|
||||
.AddLogging()
|
||||
.AddAuthentication()
|
||||
.AddCookie(IdentityConstants.ApplicationScheme);
|
||||
.AddCookie(IdentityConstants.ApplicationScheme)
|
||||
.AddCookie(IdentityConstants.ExternalScheme, o =>
|
||||
{
|
||||
o.Cookie.Name = IdentityConstants.ExternalScheme;
|
||||
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
|
||||
})
|
||||
.AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
|
||||
{
|
||||
o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
|
||||
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
|
||||
});
|
||||
IServiceProvider serviceProvider = serviceProviderFactory.CreateServiceProvider(serviceCollection);
|
||||
var httpContextFactory = new DefaultHttpContextFactory(serviceProvider);
|
||||
IFeatureCollection features = new DefaultHttpContext().Features;
|
||||
@@ -55,7 +65,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security
|
||||
Mock.Of<IOptions<IdentityOptions>>(),
|
||||
_mockLogger.Object,
|
||||
Mock.Of<IAuthenticationSchemeProvider>(),
|
||||
Mock.Of<IUserConfirmation<MemberIdentityUser>>());
|
||||
Mock.Of<IUserConfirmation<MemberIdentityUser>>(),
|
||||
Mock.Of<IMemberExternalLoginProviders>()
|
||||
);
|
||||
}
|
||||
private static Mock<MemberManager> MockMemberManager()
|
||||
=> new Mock<MemberManager>(
|
||||
|
||||
Reference in New Issue
Block a user