Member 2FA (#11889)
* 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 * Added 2fa for members * Change notification handler to be on deleted * Update src/Umbraco.Infrastructure/Security/MemberUserStore.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * updated snippets * 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. * Updated 2fa for members + publish notification when 2fa is requested. * Changed so only Members out of box supports 2fa * Cleanup * rollback of csproj file, that should not have been changed * Removed confirmed flag from db. It was not used. Handle case where a user is signed up for 2fa, but the provider do not exist anymore. Then it is just ignored until it shows up again Reintroduced ProviderName on interface, to ensure the class can be renamed safely * Bugfix * Registering DeleteTwoFactorLoginsOnMemberDeletedHandler * Rollback nuget packages added by mistake * Update src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Update src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TwoFactorLoginRepository.cs Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Added providername to snippet Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
This commit is contained in:
12
src/Umbraco.Core/Models/ITwoFactorLogin.cs
Normal file
12
src/Umbraco.Core/Models/ITwoFactorLogin.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models
|
||||
{
|
||||
public interface ITwoFactorLogin: IEntity, IRememberBeingDirty
|
||||
{
|
||||
string ProviderName { get; }
|
||||
string Secret { get; }
|
||||
Guid UserOrMemberKey { get; }
|
||||
}
|
||||
}
|
||||
13
src/Umbraco.Core/Models/TwoFactorLogin.cs
Normal file
13
src/Umbraco.Core/Models/TwoFactorLogin.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models
|
||||
{
|
||||
public class TwoFactorLogin : EntityBase, ITwoFactorLogin
|
||||
{
|
||||
public string ProviderName { get; set; }
|
||||
public string Secret { get; set; }
|
||||
public Guid UserOrMemberKey { get; set; }
|
||||
public bool Confirmed { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Cms.Core.Notifications
|
||||
{
|
||||
public class MemberTwoFactorRequestedNotification : INotification
|
||||
{
|
||||
public MemberTwoFactorRequestedNotification(Guid memberKey)
|
||||
{
|
||||
MemberKey = memberKey;
|
||||
}
|
||||
|
||||
public Guid MemberKey { get; }
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ namespace Umbraco.Cms.Core
|
||||
public const string UserGroup2Node = TableNamePrefix + "UserGroup2Node";
|
||||
public const string UserGroup2NodePermission = TableNamePrefix + "UserGroup2NodePermission";
|
||||
public const string ExternalLogin = TableNamePrefix + "ExternalLogin";
|
||||
public const string TwoFactorLogin = TableNamePrefix + "TwoFactorLogin";
|
||||
public const string ExternalLoginToken = TableNamePrefix + "ExternalLoginToken";
|
||||
|
||||
public const string Macro = /*TableNamePrefix*/ "cms" + "Macro";
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Persistence.Repositories
|
||||
{
|
||||
public interface ITwoFactorLoginRepository: IReadRepository<int, ITwoFactorLogin>, IWriteRepository<ITwoFactorLogin>
|
||||
{
|
||||
Task<bool> DeleteUserLoginsAsync(Guid userOrMemberKey);
|
||||
Task<bool> DeleteUserLoginsAsync(Guid userOrMemberKey, string providerName);
|
||||
|
||||
Task<IEnumerable<ITwoFactorLogin>> GetByUserOrMemberKeyAsync(Guid userOrMemberKey);
|
||||
}
|
||||
|
||||
}
|
||||
28
src/Umbraco.Core/Services/ITwoFactorLoginService.cs
Normal file
28
src/Umbraco.Core/Services/ITwoFactorLoginService.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services
|
||||
{
|
||||
public interface ITwoFactorLoginService : IService
|
||||
{
|
||||
/// <summary>
|
||||
/// Deletes all user logins - normally used when a member is deleted
|
||||
/// </summary>
|
||||
Task DeleteUserLoginsAsync(Guid userOrMemberKey);
|
||||
|
||||
Task<bool> IsTwoFactorEnabledAsync(Guid userKey);
|
||||
Task<string> GetSecretForUserAndProviderAsync(Guid userKey, string providerName);
|
||||
|
||||
Task<object> GetSetupInfoAsync(Guid userOrMemberKey, string providerName);
|
||||
|
||||
IEnumerable<string> GetAllProviderNames();
|
||||
Task<bool> DisableAsync(Guid userOrMemberKey, string providerName);
|
||||
|
||||
bool ValidateTwoFactorSetup(string providerName, string secret, string code);
|
||||
Task SaveAsync(TwoFactorLogin twoFactorLogin);
|
||||
Task<IEnumerable<string>> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -30,6 +31,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
|
||||
builder.Services.AddUnique<IDocumentTypeContainerRepository, DocumentTypeContainerRepository>();
|
||||
builder.Services.AddUnique<IDomainRepository, DomainRepository>();
|
||||
builder.Services.AddUnique<IEntityRepository, EntityRepository>();
|
||||
builder.Services.AddUnique<ITwoFactorLoginRepository, TwoFactorLoginRepository>();
|
||||
builder.Services.AddUnique<ExternalLoginRepository>();
|
||||
builder.Services.AddUnique<IExternalLoginRepository>(factory => factory.GetRequiredService<ExternalLoginRepository>());
|
||||
builder.Services.AddUnique<IExternalLoginWithKeyRepository>(factory => factory.GetRequiredService<ExternalLoginRepository>());
|
||||
|
||||
@@ -75,6 +75,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection
|
||||
));
|
||||
builder.Services.AddUnique<IExternalLoginService>(factory => factory.GetRequiredService<ExternalLoginService>());
|
||||
builder.Services.AddUnique<IExternalLoginWithKeyService>(factory => factory.GetRequiredService<ExternalLoginService>());
|
||||
builder.Services.AddUnique<ITwoFactorLoginService, TwoFactorLoginService>();
|
||||
builder.Services.AddUnique<IRedirectUrlService, RedirectUrlService>();
|
||||
builder.Services.AddUnique<IConsentService, ConsentService>();
|
||||
builder.Services.AddTransient(SourcesFactory);
|
||||
|
||||
@@ -60,6 +60,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
|
||||
typeof(CacheInstructionDto),
|
||||
typeof(ExternalLoginDto),
|
||||
typeof(ExternalLoginTokenDto),
|
||||
typeof(TwoFactorLoginDto),
|
||||
typeof(RedirectUrlDto),
|
||||
typeof(LockDto),
|
||||
typeof(UserGroupDto),
|
||||
|
||||
@@ -275,6 +275,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
|
||||
// TO 9.3.0
|
||||
To<MovePackageXMLToDb>("{A2F22F17-5870-4179-8A8D-2362AA4A0A5F}");
|
||||
To<UpdateExternalLoginToUseKeyInsteadOfId>("{CA7A1D9D-C9D4-4914-BC0A-459E7B9C3C8C}");
|
||||
To<AddTwoFactorLoginTable>("{0828F206-DCF7-4F73-ABBB-6792275532EB}");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_3_0
|
||||
{
|
||||
public class AddTwoFactorLoginTable : MigrationBase
|
||||
{
|
||||
public AddTwoFactorLoginTable(IMigrationContext context) : base(context)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Migrate()
|
||||
{
|
||||
IEnumerable<string> tables = SqlSyntax.GetTablesInSchema(Context.Database);
|
||||
if (tables.InvariantContains(TwoFactorLoginDto.TableName))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Create.Table<TwoFactorLoginDto>().Do();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
|
||||
{
|
||||
[TableName(TableName)]
|
||||
[ExplicitColumns]
|
||||
[PrimaryKey("Id")]
|
||||
internal class TwoFactorLoginDto
|
||||
{
|
||||
public const string TableName = Cms.Core.Constants.DatabaseSchema.Tables.TwoFactorLogin;
|
||||
|
||||
[Column("id")]
|
||||
[PrimaryKeyColumn]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Column("userOrMemberKey")]
|
||||
[Index(IndexTypes.NonClustered)]
|
||||
public Guid UserOrMemberKey { get; set; }
|
||||
|
||||
[Column("providerName")]
|
||||
[Length(400)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
[Index(IndexTypes.UniqueNonClustered, ForColumns = "providerName,userOrMemberKey", Name = "IX_" + TableName + "_ProviderName")]
|
||||
public string ProviderName { get; set; }
|
||||
|
||||
[Column("secret")]
|
||||
[Length(400)]
|
||||
[NullSetting(NullSetting = NullSettings.NotNull)]
|
||||
public string Secret { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Querying;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
{
|
||||
internal class TwoFactorLoginRepository : EntityRepositoryBase<int, ITwoFactorLogin>, ITwoFactorLoginRepository
|
||||
{
|
||||
public TwoFactorLoginRepository(IScopeAccessor scopeAccessor, AppCaches cache,
|
||||
ILogger<TwoFactorLoginRepository> logger)
|
||||
: base(scopeAccessor, cache, logger)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
protected override Sql<ISqlContext> GetBaseQuery(bool isCount)
|
||||
{
|
||||
var sql = SqlContext.Sql();
|
||||
|
||||
sql = isCount
|
||||
? sql.SelectCount()
|
||||
: sql.Select<TwoFactorLoginDto>();
|
||||
|
||||
sql.From<TwoFactorLoginDto>();
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
protected override string GetBaseWhereClause() =>
|
||||
Core.Constants.DatabaseSchema.Tables.TwoFactorLogin + ".id = @id";
|
||||
|
||||
protected override IEnumerable<string> GetDeleteClauses() => Enumerable.Empty<string>();
|
||||
|
||||
protected override ITwoFactorLogin PerformGet(int id)
|
||||
{
|
||||
var sql = GetBaseQuery(false).Where<TwoFactorLoginDto>(x => x.Id == id);
|
||||
var dto = Database.Fetch<TwoFactorLoginDto>(sql).FirstOrDefault();
|
||||
return dto == null ? null : Map(dto);
|
||||
}
|
||||
|
||||
protected override IEnumerable<ITwoFactorLogin> PerformGetAll(params int[] ids)
|
||||
{
|
||||
var sql = GetBaseQuery(false).WhereIn<TwoFactorLoginDto>(x => x.Id, ids);
|
||||
var dtos = Database.Fetch<TwoFactorLoginDto>(sql);
|
||||
return dtos.WhereNotNull().Select(Map);
|
||||
}
|
||||
|
||||
protected override IEnumerable<ITwoFactorLogin> PerformGetByQuery(IQuery<ITwoFactorLogin> query)
|
||||
{
|
||||
var sqlClause = GetBaseQuery(false);
|
||||
var translator = new SqlTranslator<ITwoFactorLogin>(sqlClause, query);
|
||||
var sql = translator.Translate();
|
||||
return Database.Fetch<TwoFactorLoginDto>(sql).Select(Map);
|
||||
}
|
||||
|
||||
protected override void PersistNewItem(ITwoFactorLogin entity)
|
||||
{
|
||||
var dto = Map(entity);
|
||||
Database.Insert(dto);
|
||||
}
|
||||
|
||||
protected override void PersistUpdatedItem(ITwoFactorLogin entity)
|
||||
{
|
||||
var dto = Map(entity);
|
||||
Database.Update(dto);
|
||||
}
|
||||
|
||||
private static TwoFactorLoginDto Map(ITwoFactorLogin entity)
|
||||
{
|
||||
if (entity == null) return null;
|
||||
|
||||
return new TwoFactorLoginDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
UserOrMemberKey = entity.UserOrMemberKey,
|
||||
ProviderName = entity.ProviderName,
|
||||
Secret = entity.Secret,
|
||||
};
|
||||
}
|
||||
|
||||
private static ITwoFactorLogin Map(TwoFactorLoginDto dto)
|
||||
{
|
||||
if (dto == null) return null;
|
||||
|
||||
return new TwoFactorLogin
|
||||
{
|
||||
Id = dto.Id,
|
||||
UserOrMemberKey = dto.UserOrMemberKey,
|
||||
ProviderName = dto.ProviderName,
|
||||
Secret = dto.Secret,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteUserLoginsAsync(Guid userOrMemberKey)
|
||||
{
|
||||
return await DeleteUserLoginsAsync(userOrMemberKey, null);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteUserLoginsAsync(Guid userOrMemberKey, string providerName)
|
||||
{
|
||||
var sql = Sql()
|
||||
.Delete()
|
||||
.From<TwoFactorLoginDto>()
|
||||
.Where<TwoFactorLoginDto>(x => x.UserOrMemberKey == userOrMemberKey);
|
||||
|
||||
if (providerName is not null)
|
||||
{
|
||||
sql = sql.Where<TwoFactorLoginDto>(x => x.ProviderName == providerName);
|
||||
}
|
||||
|
||||
var deletedRows = await Database.ExecuteAsync(sql);
|
||||
|
||||
return deletedRows > 0;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ITwoFactorLogin>> GetByUserOrMemberKeyAsync(Guid userOrMemberKey)
|
||||
{
|
||||
var sql = Sql()
|
||||
.Select<TwoFactorLoginDto>()
|
||||
.From<TwoFactorLoginDto>()
|
||||
.Where<TwoFactorLoginDto>(x => x.UserOrMemberKey == userOrMemberKey);
|
||||
var dtos = await Database.FetchAsync<TwoFactorLoginDto>(sql);
|
||||
return dtos.WhereNotNull().Select(Map);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,5 +159,24 @@ namespace Umbraco.Cms.Core.Security
|
||||
}
|
||||
|
||||
private static string UserIdToString(int userId) => string.Intern(userId.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
public Guid Key => UserIdToInt(Id).ToGuid();
|
||||
|
||||
|
||||
private static int UserIdToInt(string userId)
|
||||
{
|
||||
if(int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
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 two factor for the deleted members. This cannot be handled by the database as there is not foreign keys.
|
||||
/// </summary>
|
||||
public class DeleteTwoFactorLoginsOnMemberDeletedHandler : INotificationAsyncHandler<MemberDeletedNotification>
|
||||
{
|
||||
private readonly ITwoFactorLoginService _twoFactorLoginService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DeleteTwoFactorLoginsOnMemberDeletedHandler"/> class.
|
||||
/// </summary>
|
||||
public DeleteTwoFactorLoginsOnMemberDeletedHandler(ITwoFactorLoginService twoFactorLoginService)
|
||||
=> _twoFactorLoginService = twoFactorLoginService;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task HandleAsync(MemberDeletedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (IMember member in notification.DeletedEntities)
|
||||
{
|
||||
await _twoFactorLoginService.DeleteUserLoginsAsync(member.Key);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
22
src/Umbraco.Infrastructure/Security/ITwoFactorProvider.cs
Normal file
22
src/Umbraco.Infrastructure/Security/ITwoFactorProvider.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
public interface ITwoFactorProvider
|
||||
{
|
||||
string ProviderName { get; }
|
||||
|
||||
Task<object> GetSetupDataAsync(Guid userOrMemberKey, string secret);
|
||||
|
||||
bool ValidateTwoFactorPIN(string secret, string token);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <remarks>Called to confirm the setup of two factor on the user.</remarks>
|
||||
bool ValidateTwoFactorSetup(string secret, string token);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
63
src/Umbraco.Infrastructure/Security/MemberIdentityBuilder.cs
Normal file
63
src/Umbraco.Infrastructure/Security/MemberIdentityBuilder.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Umbraco.Cms.Core.Net;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
|
||||
public class MemberIdentityBuilder : IdentityBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemberIdentityBuilder"/> class.
|
||||
/// </summary>
|
||||
public MemberIdentityBuilder(IServiceCollection services)
|
||||
: base(typeof(MemberIdentityUser), services)
|
||||
=> InitializeServices(services);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemberIdentityBuilder"/> class.
|
||||
/// </summary>
|
||||
public MemberIdentityBuilder(Type role, IServiceCollection services)
|
||||
: base(typeof(MemberIdentityUser), role, services)
|
||||
=> InitializeServices(services);
|
||||
|
||||
private void InitializeServices(IServiceCollection services)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// override to add itself, by default identity only wants a single IdentityErrorDescriber
|
||||
public override IdentityBuilder AddErrorDescriber<TDescriber>()
|
||||
{
|
||||
if (!typeof(MembersErrorDescriber).IsAssignableFrom(typeof(TDescriber)))
|
||||
{
|
||||
throw new InvalidOperationException($"The type {typeof(TDescriber)} does not inherit from {typeof(MembersErrorDescriber)}");
|
||||
}
|
||||
|
||||
Services.AddScoped<TDescriber>();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a token provider for the <seealso cref="MemberIdentityBuilder"/>.
|
||||
/// </summary>
|
||||
/// <param name="providerName">The name of the provider to add.</param>
|
||||
/// <param name="provider">The type of the <see cref="IUserTwoFactorTokenProvider{MemberIdentityBuilder}"/> to add.</param>
|
||||
/// <returns>The current <see cref="IdentityBuilder"/> instance.</returns>
|
||||
public override IdentityBuilder AddTokenProvider(string providerName, Type provider)
|
||||
{
|
||||
if (!typeof(IUserTwoFactorTokenProvider<>).MakeGenericType(UserType).GetTypeInfo().IsAssignableFrom(provider.GetTypeInfo()))
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid Type for TokenProvider: {provider.FullName}");
|
||||
}
|
||||
|
||||
Services.Configure<IdentityOptions>(options => options.Tokens.ProviderMap[providerName] = new TokenProviderDescriptor(provider));
|
||||
Services.AddTransient(provider);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
private readonly IExternalLoginWithKeyService _externalLoginService;
|
||||
private readonly ITwoFactorLoginService _twoFactorLoginService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemberUserStore"/> class for the members identity store
|
||||
@@ -37,7 +38,9 @@ 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="publishedSnapshotAccessor">The published snapshot accessor</param>
|
||||
/// <param name="externalLoginService">The external login service</param>
|
||||
/// <param name="twoFactorLoginService">The two factor login service</param>
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public MemberUserStore(
|
||||
IMemberService memberService,
|
||||
@@ -45,7 +48,8 @@ namespace Umbraco.Cms.Core.Security
|
||||
IScopeProvider scopeProvider,
|
||||
IdentityErrorDescriber describer,
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IExternalLoginWithKeyService externalLoginService
|
||||
IExternalLoginWithKeyService externalLoginService,
|
||||
ITwoFactorLoginService twoFactorLoginService
|
||||
)
|
||||
: base(describer)
|
||||
{
|
||||
@@ -54,9 +58,10 @@ namespace Umbraco.Cms.Core.Security
|
||||
_scopeProvider = scopeProvider ?? throw new ArgumentNullException(nameof(scopeProvider));
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
_externalLoginService = externalLoginService;
|
||||
_twoFactorLoginService = twoFactorLoginService;
|
||||
}
|
||||
|
||||
[Obsolete("Use ctor with IExternalLoginWithKeyService param")]
|
||||
[Obsolete("Use ctor with IExternalLoginWithKeyService and ITwoFactorLoginService param")]
|
||||
public MemberUserStore(
|
||||
IMemberService memberService,
|
||||
IUmbracoMapper mapper,
|
||||
@@ -64,19 +69,19 @@ namespace Umbraco.Cms.Core.Security
|
||||
IdentityErrorDescriber describer,
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor,
|
||||
IExternalLoginService externalLoginService)
|
||||
: this(memberService, mapper, scopeProvider, describer, publishedSnapshotAccessor, StaticServiceProvider.Instance.GetRequiredService<IExternalLoginWithKeyService>())
|
||||
: this(memberService, mapper, scopeProvider, describer, publishedSnapshotAccessor, StaticServiceProvider.Instance.GetRequiredService<IExternalLoginWithKeyService>(), StaticServiceProvider.Instance.GetRequiredService<ITwoFactorLoginService>())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Obsolete("Use ctor with IExternalLoginWithKeyService param")]
|
||||
[Obsolete("Use ctor with IExternalLoginWithKeyService and ITwoFactorLoginService param")]
|
||||
public MemberUserStore(
|
||||
IMemberService memberService,
|
||||
IUmbracoMapper mapper,
|
||||
IScopeProvider scopeProvider,
|
||||
IdentityErrorDescriber describer,
|
||||
IPublishedSnapshotAccessor publishedSnapshotAccessor)
|
||||
: this(memberService, mapper, scopeProvider, describer, publishedSnapshotAccessor, StaticServiceProvider.Instance.GetRequiredService<IExternalLoginWithKeyService>())
|
||||
: this(memberService, mapper, scopeProvider, describer, publishedSnapshotAccessor, StaticServiceProvider.Instance.GetRequiredService<IExternalLoginWithKeyService>(), StaticServiceProvider.Instance.GetRequiredService<ITwoFactorLoginService>())
|
||||
{
|
||||
|
||||
}
|
||||
@@ -678,5 +683,34 @@ namespace Umbraco.Cms.Core.Security
|
||||
LoginOnly,
|
||||
FullSave
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overridden to support Umbraco's own data storage requirements
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The base class's implementation of this calls into FindTokenAsync, RemoveUserTokenAsync and AddUserTokenAsync, both methods will only work with ORMs that are change
|
||||
/// tracking ORMs like EFCore.
|
||||
/// </remarks>
|
||||
/// <inheritdoc />
|
||||
public override Task<string> GetTokenAsync(MemberIdentityUser user, string loginProvider, string name, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ThrowIfDisposed();
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
IIdentityUserToken token = user.LoginTokens.FirstOrDefault(x => x.LoginProvider.InvariantEquals(loginProvider) && x.Name.InvariantEquals(name));
|
||||
|
||||
return Task.FromResult(token?.Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<bool> GetTwoFactorEnabledAsync(MemberIdentityUser user,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
return await _twoFactorLoginService.IsTwoFactorEnabledAsync(user.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,5 +263,13 @@ namespace Umbraco.Cms.Core.Security
|
||||
|
||||
return await VerifyPasswordAsync(userPasswordStore, user, password) == PasswordVerificationResult.Success;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual async Task<IList<string>> GetValidTwoFactorProvidersAsync(TUser user)
|
||||
{
|
||||
var results = await base.GetValidTwoFactorProvidersAsync(user);
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services
|
||||
{
|
||||
public class TwoFactorLoginService : ITwoFactorLoginService
|
||||
{
|
||||
private readonly ITwoFactorLoginRepository _twoFactorLoginRepository;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IOptions<IdentityOptions> _identityOptions;
|
||||
private readonly IDictionary<string, ITwoFactorProvider> _twoFactorSetupGenerators;
|
||||
|
||||
public TwoFactorLoginService(
|
||||
ITwoFactorLoginRepository twoFactorLoginRepository,
|
||||
IScopeProvider scopeProvider,
|
||||
IEnumerable<ITwoFactorProvider> twoFactorSetupGenerators,
|
||||
IOptions<IdentityOptions> identityOptions)
|
||||
{
|
||||
_twoFactorLoginRepository = twoFactorLoginRepository;
|
||||
_scopeProvider = scopeProvider;
|
||||
_identityOptions = identityOptions;
|
||||
_twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x=>x.ProviderName);
|
||||
}
|
||||
|
||||
public async Task DeleteUserLoginsAsync(Guid userOrMemberKey)
|
||||
{
|
||||
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
|
||||
await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<string>> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey)
|
||||
{
|
||||
return await GetEnabledProviderNamesAsync(userOrMemberKey);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<string>> GetEnabledProviderNamesAsync(Guid userOrMemberKey)
|
||||
{
|
||||
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
|
||||
var providersOnUser = (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey))
|
||||
.Select(x => x.ProviderName).ToArray();
|
||||
|
||||
return providersOnUser.Where(x => _identityOptions.Value.Tokens.ProviderMap.ContainsKey(x));
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> IsTwoFactorEnabledAsync(Guid userOrMemberKey)
|
||||
{
|
||||
return (await GetEnabledProviderNamesAsync(userOrMemberKey)).Any();
|
||||
}
|
||||
|
||||
public async Task<string> GetSecretForUserAndProviderAsync(Guid userOrMemberKey, string providerName)
|
||||
{
|
||||
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
|
||||
return (await _twoFactorLoginRepository.GetByUserOrMemberKeyAsync(userOrMemberKey)).FirstOrDefault(x=>x.ProviderName == providerName)?.Secret;
|
||||
}
|
||||
|
||||
public async Task<object> GetSetupInfoAsync(Guid userOrMemberKey, string providerName)
|
||||
{
|
||||
var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName);
|
||||
|
||||
//Dont allow to generate a new secrets if user already has one
|
||||
if (!string.IsNullOrEmpty(secret))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
secret = GenerateSecret();
|
||||
|
||||
if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider generator))
|
||||
{
|
||||
throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}");
|
||||
}
|
||||
|
||||
return await generator.GetSetupDataAsync(userOrMemberKey, secret);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetAllProviderNames() => _twoFactorSetupGenerators.Keys;
|
||||
public async Task<bool> DisableAsync(Guid userOrMemberKey, string providerName)
|
||||
{
|
||||
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
|
||||
return (await _twoFactorLoginRepository.DeleteUserLoginsAsync(userOrMemberKey, providerName));
|
||||
|
||||
}
|
||||
|
||||
public bool ValidateTwoFactorSetup(string providerName, string secret, string code)
|
||||
{
|
||||
if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider generator))
|
||||
{
|
||||
throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}");
|
||||
}
|
||||
|
||||
return generator.ValidateTwoFactorSetup(secret, code);
|
||||
}
|
||||
|
||||
public Task SaveAsync(TwoFactorLogin twoFactorLogin)
|
||||
{
|
||||
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
|
||||
_twoFactorLoginRepository.Save(twoFactorLogin);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new random unique secret.
|
||||
/// </summary>
|
||||
/// <returns>The random secret</returns>
|
||||
protected virtual string GenerateSecret() => Guid.NewGuid().ToString();
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,8 @@ namespace Umbraco.Extensions
|
||||
factory.GetRequiredService<IScopeProvider>(),
|
||||
factory.GetRequiredService<IdentityErrorDescriber>(),
|
||||
factory.GetRequiredService<IPublishedSnapshotAccessor>(),
|
||||
factory.GetRequiredService<IExternalLoginWithKeyService>()
|
||||
factory.GetRequiredService<IExternalLoginWithKeyService>(),
|
||||
factory.GetRequiredService<ITwoFactorLoginService>()
|
||||
))
|
||||
.AddRoleStore<MemberRoleStore>()
|
||||
.AddRoleManager<IMemberRoleManager, MemberRoleManager>()
|
||||
@@ -63,6 +64,7 @@ namespace Umbraco.Extensions
|
||||
|
||||
|
||||
builder.AddNotificationHandler<MemberDeletedNotification, DeleteExternalLoginsOnMemberDeletedHandler>();
|
||||
builder.AddNotificationAsyncHandler<MemberDeletedNotification, DeleteTwoFactorLoginsOnMemberDeletedHandler>();
|
||||
services.ConfigureOptions<ConfigureMemberIdentityOptions>();
|
||||
|
||||
services.AddScoped<IMemberUserStore>(x => (IMemberUserStore)x.GetRequiredService<IUserStore<MemberIdentityUser>>());
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Infrastructure.Security;
|
||||
|
||||
namespace Umbraco.Extensions
|
||||
{
|
||||
@@ -59,5 +60,14 @@ namespace Umbraco.Extensions
|
||||
identityBuilder.Services.AddScoped(typeof(TInterface), implementationFactory);
|
||||
return identityBuilder;
|
||||
}
|
||||
|
||||
public static MemberIdentityBuilder AddTwoFactorProvider<T>(this MemberIdentityBuilder identityBuilder, string providerName) where T : class, ITwoFactorProvider
|
||||
{
|
||||
identityBuilder.Services.AddSingleton<ITwoFactorProvider, T>();
|
||||
identityBuilder.Services.AddSingleton<T>();
|
||||
identityBuilder.AddTokenProvider<TwoFactorMemberValidationProvider<T>>(providerName);
|
||||
|
||||
return identityBuilder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,33 @@ namespace Umbraco.Extensions
|
||||
builder.Services.Replace(ServiceDescriptor.Scoped(userManagerType, customType));
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IUmbracoBuilder SetBackOfficeUserStore<TUserStore>(this IUmbracoBuilder builder)
|
||||
where TUserStore : BackOfficeUserStore
|
||||
{
|
||||
Type customType = typeof(TUserStore);
|
||||
builder.Services.Replace(ServiceDescriptor.Scoped(typeof(IUserStore<>).MakeGenericType(typeof(BackOfficeIdentityUser)), customType));
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IUmbracoBuilder SetMemberManager<TUserManager>(this IUmbracoBuilder builder)
|
||||
where TUserManager : UserManager<MemberIdentityUser>, IMemberManager
|
||||
{
|
||||
|
||||
Type customType = typeof(TUserManager);
|
||||
Type userManagerType = typeof(UserManager<MemberIdentityUser>);
|
||||
builder.Services.Replace(ServiceDescriptor.Scoped(typeof(IMemberManager), customType));
|
||||
builder.Services.AddScoped(customType, services => services.GetRequiredService(userManagerType));
|
||||
builder.Services.Replace(ServiceDescriptor.Scoped(userManagerType, customType));
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static IUmbracoBuilder SetMemberUserStore<TUserStore>(this IUmbracoBuilder builder)
|
||||
where TUserStore : MemberUserStore
|
||||
{
|
||||
Type customType = typeof(TUserStore);
|
||||
builder.Services.Replace(ServiceDescriptor.Scoped(typeof(IUserStore<>).MakeGenericType(typeof(MemberIdentityUser)), customType));
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Semver;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
@@ -16,6 +18,7 @@ namespace Umbraco.Extensions
|
||||
public const string TokenUmbracoVersion = "UmbracoVersion";
|
||||
public const string TokenExternalSignInError = "ExternalSignInError";
|
||||
public const string TokenPasswordResetCode = "PasswordResetCode";
|
||||
public const string TokenTwoFactorRequired = "TwoFactorRequired";
|
||||
|
||||
public static bool FromTempData(this ViewDataDictionary viewData, ITempDataDictionary tempData, string token)
|
||||
{
|
||||
@@ -135,5 +138,16 @@ namespace Umbraco.Extensions
|
||||
{
|
||||
viewData[TokenPasswordResetCode] = value;
|
||||
}
|
||||
|
||||
public static void SetTwoFactorProviderNames(this ViewDataDictionary viewData, IEnumerable<string> providerNames)
|
||||
{
|
||||
viewData[TokenTwoFactorRequired] = providerNames;
|
||||
}
|
||||
|
||||
public static bool TryGetTwoFactorProviderNames(this ViewDataDictionary viewData, out IEnumerable<string> providerNames)
|
||||
{
|
||||
providerNames = viewData[TokenTwoFactorRequired] as IEnumerable<string>;
|
||||
return providerNames is not null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Security
|
||||
{
|
||||
@@ -12,5 +13,7 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null);
|
||||
Task<IdentityResult> UpdateExternalAuthenticationTokensAsync(ExternalLoginInfo externalLogin);
|
||||
Task<SignInResult> ExternalLoginSignInAsync(ExternalLoginInfo loginInfo, bool isPersistent, bool bypassTwoFactor = false);
|
||||
Task<MemberIdentityUser> GetTwoFactorAuthenticationUserAsync();
|
||||
Task<SignInResult> TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool SupportsUserTwoFactor => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> IsMemberAuthorizedAsync(IEnumerable<string> allowTypes = null, IEnumerable<string> allowGroups = null, IEnumerable<int> allowMembers = null)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Principal;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -9,6 +10,8 @@ using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
using Umbraco.Extensions;
|
||||
@@ -22,6 +25,7 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
public class MemberSignInManager : UmbracoSignInManager<MemberIdentityUser>, IMemberSignInManagerExternalLogins
|
||||
{
|
||||
private readonly IMemberExternalLoginProviders _memberExternalLoginProviders;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
|
||||
public MemberSignInManager(
|
||||
UserManager<MemberIdentityUser> memberManager,
|
||||
@@ -31,10 +35,12 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
ILogger<SignInManager<MemberIdentityUser>> logger,
|
||||
IAuthenticationSchemeProvider schemes,
|
||||
IUserConfirmation<MemberIdentityUser> confirmation,
|
||||
IMemberExternalLoginProviders memberExternalLoginProviders) :
|
||||
IMemberExternalLoginProviders memberExternalLoginProviders,
|
||||
IEventAggregator eventAggregator) :
|
||||
base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
|
||||
{
|
||||
_memberExternalLoginProviders = memberExternalLoginProviders;
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
[Obsolete("Use ctor with all params")]
|
||||
@@ -46,7 +52,9 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
ILogger<SignInManager<MemberIdentityUser>> logger,
|
||||
IAuthenticationSchemeProvider schemes,
|
||||
IUserConfirmation<MemberIdentityUser> confirmation) :
|
||||
this(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation, StaticServiceProvider.Instance.GetRequiredService<IMemberExternalLoginProviders>())
|
||||
this(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IMemberExternalLoginProviders>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IEventAggregator>())
|
||||
{ }
|
||||
|
||||
// use default scheme for members
|
||||
@@ -61,30 +69,6 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
// use default scheme for members
|
||||
protected override string TwoFactorRememberMeAuthenticationType => IdentityConstants.TwoFactorRememberMeScheme;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<MemberIdentityUser> GetTwoFactorAuthenticationUserAsync()
|
||||
=> throw new NotImplementedException("Two factor is not yet implemented for members");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<SignInResult> TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient)
|
||||
=> throw new NotImplementedException("Two factor is not yet implemented for members");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<bool> IsTwoFactorClientRememberedAsync(MemberIdentityUser user)
|
||||
=> throw new NotImplementedException("Two factor is not yet implemented for members");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task RememberTwoFactorClientAsync(MemberIdentityUser user)
|
||||
=> throw new NotImplementedException("Two factor is not yet implemented for members");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task ForgetTwoFactorClientAsync()
|
||||
=> throw new NotImplementedException("Two factor is not yet implemented for members");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Task<SignInResult> TwoFactorRecoveryCodeSignInAsync(string recoveryCode)
|
||||
=> throw new NotImplementedException("Two factor is not yet implemented for members");
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
|
||||
{
|
||||
@@ -369,6 +353,29 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
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);
|
||||
|
||||
protected override async Task<SignInResult> SignInOrTwoFactorAsync(MemberIdentityUser user, bool isPersistent,
|
||||
string loginProvider = null, bool bypassTwoFactor = false)
|
||||
{
|
||||
var result = await base.SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor);
|
||||
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
NotifyRequiresTwoFactor(user);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected void NotifyRequiresTwoFactor(MemberIdentityUser user) => Notify(user,
|
||||
(currentUser) => new MemberTwoFactorRequestedNotification(currentUser.Key)
|
||||
);
|
||||
|
||||
private T Notify<T>(MemberIdentityUser currentUser, Func<MemberIdentityUser, T> createNotification) where T : INotification
|
||||
{
|
||||
|
||||
var notification = createNotification(currentUser);
|
||||
_eventAggregator.Publish(notification);
|
||||
return notification;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Security
|
||||
{
|
||||
public class TwoFactorBackOfficeValidationProvider<TTwoFactorSetupGenerator> : TwoFactorValidationProvider<BackOfficeIdentityUser, TTwoFactorSetupGenerator>
|
||||
where TTwoFactorSetupGenerator : ITwoFactorProvider
|
||||
{
|
||||
protected TwoFactorBackOfficeValidationProvider(IDataProtectionProvider dataProtectionProvider, IOptions<DataProtectionTokenProviderOptions> options, ILogger<TwoFactorBackOfficeValidationProvider<TTwoFactorSetupGenerator>> logger, ITwoFactorLoginService twoFactorLoginService, TTwoFactorSetupGenerator generator) : base(dataProtectionProvider, options, logger, twoFactorLoginService, generator)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class TwoFactorMemberValidationProvider<TTwoFactorSetupGenerator> : TwoFactorValidationProvider<MemberIdentityUser, TTwoFactorSetupGenerator>
|
||||
where TTwoFactorSetupGenerator : ITwoFactorProvider
|
||||
{
|
||||
public TwoFactorMemberValidationProvider(IDataProtectionProvider dataProtectionProvider, IOptions<DataProtectionTokenProviderOptions> options, ILogger<TwoFactorMemberValidationProvider<TTwoFactorSetupGenerator>> logger, ITwoFactorLoginService twoFactorLoginService, TTwoFactorSetupGenerator generator) : base(dataProtectionProvider, options, logger, twoFactorLoginService, generator)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class TwoFactorValidationProvider<TUmbracoIdentityUser, TTwoFactorSetupGenerator>
|
||||
: DataProtectorTokenProvider<TUmbracoIdentityUser>
|
||||
where TUmbracoIdentityUser : UmbracoIdentityUser
|
||||
where TTwoFactorSetupGenerator : ITwoFactorProvider
|
||||
{
|
||||
private readonly ITwoFactorLoginService _twoFactorLoginService;
|
||||
private readonly TTwoFactorSetupGenerator _generator;
|
||||
|
||||
protected TwoFactorValidationProvider(
|
||||
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
IOptions<DataProtectionTokenProviderOptions> options,
|
||||
ILogger<TwoFactorValidationProvider<TUmbracoIdentityUser, TTwoFactorSetupGenerator>> logger,
|
||||
ITwoFactorLoginService twoFactorLoginService,
|
||||
TTwoFactorSetupGenerator generator)
|
||||
: base(dataProtectionProvider, options, logger)
|
||||
{
|
||||
_twoFactorLoginService = twoFactorLoginService;
|
||||
_generator = generator;
|
||||
}
|
||||
|
||||
public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUmbracoIdentityUser> manager,
|
||||
TUmbracoIdentityUser user) => Task.FromResult(_generator is not null);
|
||||
|
||||
public override async Task<bool> ValidateAsync(string purpose, string token,
|
||||
UserManager<TUmbracoIdentityUser> manager, TUmbracoIdentityUser user)
|
||||
{
|
||||
var secret =
|
||||
await _twoFactorLoginService.GetSecretForUserAndProviderAsync(GetUserKey(user), _generator.ProviderName);
|
||||
|
||||
if (secret is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var validToken = _generator.ValidateTwoFactorPIN(secret, token);
|
||||
|
||||
|
||||
return validToken;
|
||||
}
|
||||
|
||||
protected Guid GetUserKey(TUmbracoIdentityUser user)
|
||||
{
|
||||
|
||||
switch (user)
|
||||
{
|
||||
case MemberIdentityUser memberIdentityUser:
|
||||
return memberIdentityUser.Key;
|
||||
case BackOfficeIdentityUser backOfficeIdentityUser:
|
||||
return backOfficeIdentityUser.Key;
|
||||
default:
|
||||
throw new NotSupportedException(
|
||||
"Current we only support MemberIdentityUser and BackOfficeIdentityUser");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
@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
|
||||
@@ -11,6 +8,7 @@
|
||||
@inject IMemberExternalLoginProviders memberExternalLoginProviders
|
||||
@inject IExternalLoginWithKeyService externalLoginWithKeyService
|
||||
@{
|
||||
|
||||
// Build a profile model to edit
|
||||
var profileModel = await memberModelBuilderFactory
|
||||
.CreateProfileModel()
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
@inherits Umbraco.Cms.Web.Common.Macros.PartialViewMacroPage
|
||||
|
||||
@using Umbraco.Cms.Web.Common.Models
|
||||
@using Umbraco.Cms.Web.Common.Security
|
||||
@using Umbraco.Cms.Web.Website.Controllers
|
||||
@using Umbraco.Cms.Core.Services
|
||||
@using Umbraco.Extensions
|
||||
@inject IMemberExternalLoginProviders memberExternalLoginProviders
|
||||
@inject ITwoFactorLoginService twoFactorLoginService
|
||||
@{
|
||||
var loginModel = new LoginModel();
|
||||
// You can modify this to redirect to a different URL instead of the current one
|
||||
@@ -14,6 +17,33 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.0/jquery.validate.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"></script>
|
||||
|
||||
|
||||
@if (ViewData.TryGetTwoFactorProviderNames(out var providerNames))
|
||||
{
|
||||
|
||||
foreach (var providerName in providerNames)
|
||||
{
|
||||
<div class="2fa-form">
|
||||
<h4>Two factor with @providerName.</h4>
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
@using (Html.BeginUmbracoForm<UmbTwoFactorLoginController>(nameof(UmbTwoFactorLoginController.Verify2FACode)))
|
||||
{
|
||||
|
||||
<text>
|
||||
<input type="hidden" name="provider" value="@providerName"/>
|
||||
Input security code: <input name="code" value=""/><br/>
|
||||
<button type="submit" class="btn btn-primary">Validate</button>
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
</text>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
|
||||
<div class="login-form">
|
||||
|
||||
@using (Html.BeginUmbracoForm<UmbLoginController>(
|
||||
@@ -74,3 +104,4 @@
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ 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 Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -27,9 +28,12 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
public class UmbExternalLoginController : SurfaceController
|
||||
{
|
||||
private readonly IMemberManager _memberManager;
|
||||
private readonly ITwoFactorLoginService _twoFactorLoginService;
|
||||
private readonly ILogger<UmbExternalLoginController> _logger;
|
||||
private readonly IMemberSignInManagerExternalLogins _memberSignInManager;
|
||||
|
||||
public UmbExternalLoginController(
|
||||
ILogger<UmbExternalLoginController> logger,
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
ServiceContext services,
|
||||
@@ -37,7 +41,8 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
IProfilingLogger profilingLogger,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IMemberSignInManagerExternalLogins memberSignInManager,
|
||||
IMemberManager memberManager)
|
||||
IMemberManager memberManager,
|
||||
ITwoFactorLoginService twoFactorLoginService)
|
||||
: base(
|
||||
umbracoContextAccessor,
|
||||
databaseFactory,
|
||||
@@ -46,8 +51,10 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
profilingLogger,
|
||||
publishedUrlProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_memberSignInManager = memberSignInManager;
|
||||
_memberManager = memberManager;
|
||||
_twoFactorLoginService = twoFactorLoginService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -108,14 +115,12 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
$"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;
|
||||
var providerNames = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(attemptedUser.Key);
|
||||
ViewData.SetTwoFactorProviderNames(providerNames);
|
||||
|
||||
return CurrentUmbracoPage();
|
||||
|
||||
}
|
||||
|
||||
if (result == SignInResult.LockedOut)
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
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.DependencyInjection;
|
||||
using Umbraco.Cms.Web.Common.Filters;
|
||||
using Umbraco.Cms.Web.Common.Models;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
@@ -20,7 +26,29 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
public class UmbLoginController : SurfaceController
|
||||
{
|
||||
private readonly IMemberSignInManager _signInManager;
|
||||
private readonly IMemberManager _memberManager;
|
||||
private readonly ITwoFactorLoginService _twoFactorLoginService;
|
||||
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public UmbLoginController(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
ServiceContext services,
|
||||
AppCaches appCaches,
|
||||
IProfilingLogger profilingLogger,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IMemberSignInManager signInManager,
|
||||
IMemberManager memberManager,
|
||||
ITwoFactorLoginService twoFactorLoginService)
|
||||
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
_memberManager = memberManager;
|
||||
_twoFactorLoginService = twoFactorLoginService;
|
||||
}
|
||||
|
||||
[Obsolete("Use ctor with all params")]
|
||||
public UmbLoginController(
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
@@ -29,9 +57,11 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
IProfilingLogger profilingLogger,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IMemberSignInManager signInManager)
|
||||
: base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider)
|
||||
: this(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider, signInManager,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IMemberManager>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<ITwoFactorLoginService>())
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -74,15 +104,28 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
throw new NotImplementedException("Two factor support is not supported for Umbraco members yet");
|
||||
MemberIdentityUser attemptedUser = await _memberManager.FindByNameAsync(model.Username);
|
||||
if (attemptedUser == null)
|
||||
{
|
||||
return new ValidationErrorResult(
|
||||
$"No local member found for username {model.Username}");
|
||||
}
|
||||
|
||||
var providerNames = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(attemptedUser.Key);
|
||||
ViewData.SetTwoFactorProviderNames(providerNames);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
ModelState.AddModelError("loginModel", "Member is locked out");
|
||||
}
|
||||
else if (result.IsNotAllowed)
|
||||
{
|
||||
ModelState.AddModelError("loginModel", "Member is not allowed");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError("loginModel", "Invalid username or password");
|
||||
}
|
||||
|
||||
// TODO: We can check for these and respond differently if we think it's important
|
||||
// result.IsLockedOut
|
||||
// result.IsNotAllowed
|
||||
|
||||
// Don't add a field level error, just model level.
|
||||
ModelState.AddModelError("loginModel", "Invalid username or password");
|
||||
return CurrentUmbracoPage();
|
||||
}
|
||||
|
||||
@@ -97,5 +140,7 @@ namespace Umbraco.Cms.Web.Website.Controllers
|
||||
model.RedirectUrl = redirectUrl.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
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 UmbTwoFactorLoginController : SurfaceController
|
||||
{
|
||||
private readonly IMemberManager _memberManager;
|
||||
private readonly ITwoFactorLoginService _twoFactorLoginService;
|
||||
private readonly ILogger<UmbTwoFactorLoginController> _logger;
|
||||
private readonly IMemberSignInManagerExternalLogins _memberSignInManager;
|
||||
|
||||
public UmbTwoFactorLoginController(
|
||||
ILogger<UmbTwoFactorLoginController> logger,
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
IUmbracoDatabaseFactory databaseFactory,
|
||||
ServiceContext services,
|
||||
AppCaches appCaches,
|
||||
IProfilingLogger profilingLogger,
|
||||
IPublishedUrlProvider publishedUrlProvider,
|
||||
IMemberSignInManagerExternalLogins memberSignInManager,
|
||||
IMemberManager memberManager,
|
||||
ITwoFactorLoginService twoFactorLoginService)
|
||||
: base(
|
||||
umbracoContextAccessor,
|
||||
databaseFactory,
|
||||
services,
|
||||
appCaches,
|
||||
profilingLogger,
|
||||
publishedUrlProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_memberSignInManager = memberSignInManager;
|
||||
_memberManager = memberManager;
|
||||
_twoFactorLoginService = twoFactorLoginService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to retrieve the 2FA providers for code submission
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<IEnumerable<string>>> Get2FAProviders()
|
||||
{
|
||||
var user = await _memberSignInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("Get2FAProviders :: No verified member found, returning 404");
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var userFactors = await _memberManager.GetValidTwoFactorProvidersAsync(user);
|
||||
return new ObjectResult(userFactors);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> Verify2FACode(Verify2FACodeModel model, string returnUrl = null)
|
||||
{
|
||||
var user = await _memberSignInManager.GetTwoFactorAuthenticationUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("PostVerify2FACode :: No verified member found, returning 404");
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var result = await _memberSignInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.IsPersistent, model.RememberClient);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Member is locked out");
|
||||
}
|
||||
else if (result.IsNotAllowed)
|
||||
{
|
||||
ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Member is not allowed");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Invalid code");
|
||||
}
|
||||
}
|
||||
|
||||
//We need to set this, to ensure we show the 2fa login page
|
||||
var providerNames = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(user.Key);
|
||||
ViewData.SetTwoFactorProviderNames(providerNames);
|
||||
return CurrentUmbracoPage();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> ValidateAndSaveSetup(string providerName, string secret, string code, string returnUrl = null)
|
||||
{
|
||||
var member = await _memberManager.GetCurrentMemberAsync();
|
||||
|
||||
var isValid = _twoFactorLoginService.ValidateTwoFactorSetup(providerName, secret, code);
|
||||
|
||||
if (isValid == false)
|
||||
{
|
||||
ModelState.AddModelError(nameof(code), "Invalid Code");
|
||||
|
||||
return CurrentUmbracoPage();
|
||||
}
|
||||
|
||||
var twoFactorLogin = new TwoFactorLogin()
|
||||
{
|
||||
Confirmed = true,
|
||||
Secret = secret,
|
||||
UserOrMemberKey = member.Key,
|
||||
ProviderName = providerName
|
||||
};
|
||||
|
||||
await _twoFactorLoginService.SaveAsync(twoFactorLogin);
|
||||
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Disable(string providerName, string returnUrl = null)
|
||||
{
|
||||
var member = await _memberManager.GetCurrentMemberAsync();
|
||||
|
||||
var success = await _twoFactorLoginService.DisableAsync(member.Key, providerName);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return CurrentUmbracoPage();
|
||||
}
|
||||
|
||||
return RedirectToLocal(returnUrl);
|
||||
}
|
||||
|
||||
private IActionResult RedirectToLocal(string returnUrl) =>
|
||||
Url.IsLocalUrl(returnUrl) ? Redirect(returnUrl) : RedirectToCurrentUmbracoPage();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user