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:
Bjarke Berg
2022-01-19 09:21:50 +01:00
committed by GitHub
parent 229ca989eb
commit 029a261476
53 changed files with 1657 additions and 190 deletions

View File

@@ -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";

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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>

View 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);
}
}

View File

@@ -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>();

View File

@@ -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);

View File

@@ -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}");
}
}
}

View File

@@ -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();
}
}
}
}

View File

@@ -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>

View File

@@ -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,

View File

@@ -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));
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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);

View File

@@ -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)));
}

View File

@@ -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);
}
}
}
}

View File

@@ -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;

View File

@@ -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");
}

View File

@@ -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();
}
}

View File

@@ -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>()

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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; }
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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>&nbsp;{{login.caption}}&nbsp;<localize
key="defaultdialogs_account">account
</localize>

View File

@@ -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>

View File

@@ -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')" />

View File

@@ -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);
}
}
}
}
}
}
}

View File

@@ -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>

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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

View File

@@ -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")]

View File

@@ -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);

View File

@@ -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;
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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>();

View File

@@ -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);
}

View File

@@ -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 } };

View File

@@ -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]

View File

@@ -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>(