From 951e8f562e367f153475559f56d58a6133702e54 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 19 Dec 2018 22:27:45 -0500 Subject: [PATCH] email token provider --- src/Api/Controllers/TwoFactorController.cs | 10 ++- src/Core/Identity/EmailTokenProvider.cs | 86 +++++++++++++++++++ .../ResourceOwnerPasswordValidator.cs | 49 ++--------- .../Implementations/OrganizationService.cs | 4 +- src/Core/Utilities/CoreHelpers.cs | 6 ++ .../Utilities/ServiceCollectionExtensions.cs | 17 ++-- 6 files changed, 121 insertions(+), 51 deletions(-) create mode 100644 src/Core/Identity/EmailTokenProvider.cs diff --git a/src/Api/Controllers/TwoFactorController.cs b/src/Api/Controllers/TwoFactorController.cs index e0828d217..b6d2d0d3a 100644 --- a/src/Api/Controllers/TwoFactorController.cs +++ b/src/Api/Controllers/TwoFactorController.cs @@ -11,6 +11,7 @@ using Bit.Core.Enums; using System.Linq; using Bit.Core; using Bit.Core.Repositories; +using Bit.Core.Utilities; namespace Bit.Api.Controllers { @@ -91,7 +92,8 @@ namespace Bit.Api.Controllers var user = await CheckAsync(model.MasterPasswordHash, false); model.ToUser(user); - if(!await _userManager.VerifyTwoFactorTokenAsync(user, TwoFactorProviderType.Authenticator.ToString(), model.Token)) + if(!await _userManager.VerifyTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(TwoFactorProviderType.Authenticator), model.Token)) { await Task.Delay(2000); throw new BadRequestException("Token", "Invalid token."); @@ -278,7 +280,8 @@ namespace Bit.Api.Controllers var user = await CheckAsync(model.MasterPasswordHash, false); model.ToUser(user); - if(!await _userService.VerifyTwoFactorEmailAsync(user, model.Token)) + if(!await _userManager.VerifyTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(TwoFactorProviderType.Email), model.Token)) { await Task.Delay(2000); throw new BadRequestException("Token", "Invalid token."); @@ -371,7 +374,8 @@ namespace Bit.Api.Controllers return; } - if(!await _userManager.VerifyTwoFactorTokenAsync(user, TwoFactorProviderType.YubiKey.ToString(), value)) + if(!await _userManager.VerifyTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey), value)) { await Task.Delay(2000); throw new BadRequestException(name, $"{name} is invalid."); diff --git a/src/Core/Identity/EmailTokenProvider.cs b/src/Core/Identity/EmailTokenProvider.cs new file mode 100644 index 000000000..c9b703bf6 --- /dev/null +++ b/src/Core/Identity/EmailTokenProvider.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Bit.Core.Models.Table; +using Bit.Core.Enums; +using Bit.Core.Services; +using Microsoft.Extensions.DependencyInjection; +using Bit.Core.Models; + +namespace Bit.Core.Identity +{ + public class EmailTokenProvider : IUserTwoFactorTokenProvider + { + private readonly IServiceProvider _serviceProvider; + + public EmailTokenProvider(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + { + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); + if(!HasProperMetaData(provider)) + { + return false; + } + + return await _serviceProvider.GetRequiredService(). + TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Email, user); + } + + public Task GenerateAsync(string purpose, UserManager manager, User user) + { + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); + if(!HasProperMetaData(provider)) + { + return null; + } + + return Task.FromResult(RedactEmail((string)provider.MetaData["Email"])); + } + + public Task ValidateAsync(string purpose, string token, UserManager manager, User user) + { + return _serviceProvider.GetRequiredService().VerifyTwoFactorEmailAsync(user, token); + } + + private bool HasProperMetaData(TwoFactorProvider provider) + { + return provider?.MetaData != null && provider.MetaData.ContainsKey("Email") && + !string.IsNullOrWhiteSpace((string)provider.MetaData["Email"]); + } + + private static string RedactEmail(string email) + { + var emailParts = email.Split('@'); + + string shownPart = null; + if(emailParts[0].Length > 2 && emailParts[0].Length <= 4) + { + shownPart = emailParts[0].Substring(0, 1); + } + else if(emailParts[0].Length > 4) + { + shownPart = emailParts[0].Substring(0, 2); + } + else + { + shownPart = string.Empty; + } + + string redactedPart = null; + if(emailParts[0].Length > 4) + { + redactedPart = new string('*', emailParts[0].Length - 2); + } + else + { + redactedPart = new string('*', emailParts[0].Length - shownPart.Length); + } + + return $"{shownPart}{redactedPart}@{emailParts[1]}"; + } + } +} diff --git a/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs b/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs index 6c02fc2ec..3c3b34c71 100644 --- a/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs +++ b/src/Core/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -14,6 +14,7 @@ using System.Linq; using Bit.Core.Models; using Bit.Core.Identity; using Bit.Core.Models.Data; +using Bit.Core.Utilities; namespace Bit.Core.IdentityServer { @@ -137,7 +138,7 @@ namespace Bit.Core.IdentityServer if(sendRememberToken) { var token = await _userManager.GenerateTwoFactorTokenAsync(user, - TwoFactorProviderType.Remember.ToString()); + CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember)); customResponse.Add("TwoFactorToken", token); } @@ -274,6 +275,7 @@ namespace Bit.Core.IdentityServer switch(type) { case TwoFactorProviderType.Authenticator: + case TwoFactorProviderType.Email: case TwoFactorProviderType.Duo: case TwoFactorProviderType.YubiKey: case TwoFactorProviderType.U2f: @@ -283,13 +285,8 @@ namespace Bit.Core.IdentityServer { return false; } - return await _userManager.VerifyTwoFactorTokenAsync(user, type.ToString(), token); - case TwoFactorProviderType.Email: - if(!(await _userService.TwoFactorProviderIsEnabledAsync(type, user))) - { - return false; - } - return await _userService.VerifyTwoFactorEmailAsync(user, token); + return await _userManager.VerifyTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(type), token); case TwoFactorProviderType.OrganizationDuo: if(!organization?.TwoFactorProviderIsEnabled(type) ?? true) { @@ -316,7 +313,8 @@ namespace Bit.Core.IdentityServer return null; } - var token = await _userManager.GenerateTwoFactorTokenAsync(user, type.ToString()); + var token = await _userManager.GenerateTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(type)); if(type == TwoFactorProviderType.Duo) { return new Dictionary @@ -339,7 +337,7 @@ namespace Bit.Core.IdentityServer { return new Dictionary { - ["Email"] = RedactEmail((string)provider.MetaData["Email"]) + ["Email"] = token }; } else if(type == TwoFactorProviderType.YubiKey) @@ -365,37 +363,6 @@ namespace Bit.Core.IdentityServer } } - private static string RedactEmail(string email) - { - var emailParts = email.Split('@'); - - string shownPart = null; - if(emailParts[0].Length > 2 && emailParts[0].Length <= 4) - { - shownPart = emailParts[0].Substring(0, 1); - } - else if(emailParts[0].Length > 4) - { - shownPart = emailParts[0].Substring(0, 2); - } - else - { - shownPart = string.Empty; - } - - string redactedPart = null; - if(emailParts[0].Length > 4) - { - redactedPart = new string('*', emailParts[0].Length - 2); - } - else - { - redactedPart = new string('*', emailParts[0].Length - shownPart.Length); - } - - return $"{shownPart}{redactedPart}@{emailParts[1]}"; - } - private async Task SaveDeviceAsync(User user, ResourceOwnerPasswordValidationContext context) { var device = GetDeviceFromRequest(context); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 6a122c6b3..8776b49db 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -844,7 +844,7 @@ namespace Bit.Core.Services public async Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type) { - if(!type.ToString().StartsWith("Organization")) + if(!type.ToString().Contains("Organization")) { throw new ArgumentException("Not an organization provider type."); } @@ -867,7 +867,7 @@ namespace Bit.Core.Services public async Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type) { - if(!type.ToString().StartsWith("Organization")) + if(!type.ToString().Contains("Organization")) { throw new ArgumentException("Not an organization provider type."); } diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index 7c5d0b128..c390a2acf 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -14,6 +14,7 @@ using Dapper; using System.Globalization; using System.Web; using Microsoft.AspNetCore.DataProtection; +using Bit.Core.Enums; namespace Bit.Core.Utilities { @@ -474,5 +475,10 @@ namespace Bit.Core.Utilities return !invalid; } + + public static string CustomProviderName(TwoFactorProviderType type) + { + return string.Concat("Custom_", type.ToString()); + } } } diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index 5efdd7081..3f8e04415 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -189,11 +189,18 @@ namespace Bit.Core.Utilities .AddUserStore() .AddRoleStore() .AddTokenProvider>(TokenOptions.DefaultProvider) - .AddTokenProvider(TwoFactorProviderType.Authenticator.ToString()) - .AddTokenProvider(TwoFactorProviderType.YubiKey.ToString()) - .AddTokenProvider(TwoFactorProviderType.Duo.ToString()) - .AddTokenProvider(TwoFactorProviderType.U2f.ToString()) - .AddTokenProvider(TwoFactorProviderType.Remember.ToString()) + .AddTokenProvider( + CoreHelpers.CustomProviderName(TwoFactorProviderType.Authenticator)) + .AddTokenProvider( + CoreHelpers.CustomProviderName(TwoFactorProviderType.Email)) + .AddTokenProvider( + CoreHelpers.CustomProviderName(TwoFactorProviderType.YubiKey)) + .AddTokenProvider( + CoreHelpers.CustomProviderName(TwoFactorProviderType.Duo)) + .AddTokenProvider( + CoreHelpers.CustomProviderName(TwoFactorProviderType.U2f)) + .AddTokenProvider( + CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember)) .AddTokenProvider>(TokenOptions.DefaultEmailProvider); return identityBuilder;