From 3b5b24531b4a339975cf34838df2c84fab55fdf2 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 19 Jun 2017 22:08:10 -0400 Subject: [PATCH] refactor to a new two-factor controller --- src/Api/Controllers/AccountsController.cs | 62 ------- src/Api/Controllers/TwoFactorController.cs | 166 ++++++++++++++++++ .../Accounts/RecoverTwoFactorRequestModel.cs | 18 -- .../Accounts/UpdateTwoFactorRequestModel.cs | 16 -- .../Api/Request/TwoFactorRequestModels.cs | 80 +++++++++ .../Api/Response/ProfileResponseModel.cs | 1 - .../TwoFactorAuthenticatorResponseModel.cs | 32 ++++ .../TwoFactor/TwoFactorEmailResponseModel.cs | 32 ++++ .../TwoFactorProviderResponseModel.cs | 23 +++ .../TwoFactorYubiKeyResponseModel.cs | 56 ++++++ .../Api/Response/TwoFactorResponseModel.cs | 45 ----- src/Core/Models/TwoFactorProvider.cs | 4 +- src/Core/Services/IUserService.cs | 1 + .../Services/Implementations/UserService.cs | 27 ++- 14 files changed, 411 insertions(+), 152 deletions(-) create mode 100644 src/Api/Controllers/TwoFactorController.cs delete mode 100644 src/Core/Models/Api/Request/Accounts/RecoverTwoFactorRequestModel.cs delete mode 100644 src/Core/Models/Api/Request/Accounts/UpdateTwoFactorRequestModel.cs create mode 100644 src/Core/Models/Api/Request/TwoFactorRequestModels.cs create mode 100644 src/Core/Models/Api/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs create mode 100644 src/Core/Models/Api/Response/TwoFactor/TwoFactorEmailResponseModel.cs create mode 100644 src/Core/Models/Api/Response/TwoFactor/TwoFactorProviderResponseModel.cs create mode 100644 src/Core/Models/Api/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs delete mode 100644 src/Core/Models/Api/Response/TwoFactorResponseModel.cs diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index f5d6f51dc4..558cafcd46 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -248,68 +248,6 @@ namespace Bit.Api.Controllers return revisionDate; } - [HttpGet("two-factor")] - public async Task GetTwoFactor(string masterPasswordHash, TwoFactorProviderType provider) - { - var user = await _userService.GetUserByPrincipalAsync(User); - if(user == null) - { - throw new UnauthorizedAccessException(); - } - - if(!await _userManager.CheckPasswordAsync(user, masterPasswordHash)) - { - await Task.Delay(2000); - throw new BadRequestException("MasterPasswordHash", "Invalid password."); - } - - await _userService.SetupTwoFactorAsync(user, provider); - - var response = new TwoFactorResponseModel(user); - return response; - } - - [Obsolete] - [HttpPut("two-factor")] - [HttpPost("two-factor")] - public async Task PutTwoFactor([FromBody]UpdateTwoFactorRequestModel model) - { - var user = await _userService.GetUserByPrincipalAsync(User); - if(user == null) - { - throw new UnauthorizedAccessException(); - } - - if(!await _userManager.CheckPasswordAsync(user, model.MasterPasswordHash)) - { - await Task.Delay(2000); - throw new BadRequestException("MasterPasswordHash", "Invalid password."); - } - - if(!await _userManager.VerifyTwoFactorTokenAsync(user, TwoFactorProviderType.Authenticator.ToString(), model.Token)) - { - await Task.Delay(2000); - throw new BadRequestException("Token", "Invalid token."); - } - - user.TwoFactorEnabled = model.Enabled.Value; - await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Authenticator); - - var response = new TwoFactorResponseModel(user); - return response; - } - - [HttpPost("two-factor-recover")] - [AllowAnonymous] - public async Task PostTwoFactorRecover([FromBody]RecoverTwoFactorRequestModel model) - { - if(!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode)) - { - await Task.Delay(2000); - throw new BadRequestException(string.Empty, "Invalid information. Try again."); - } - } - [HttpPut("keys")] [HttpPost("keys")] public async Task PutKeys([FromBody]KeysRequestModel model) diff --git a/src/Api/Controllers/TwoFactorController.cs b/src/Api/Controllers/TwoFactorController.cs new file mode 100644 index 0000000000..b0e22c9c8e --- /dev/null +++ b/src/Api/Controllers/TwoFactorController.cs @@ -0,0 +1,166 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Bit.Core.Models.Api; +using Bit.Core.Exceptions; +using Bit.Core.Services; +using Microsoft.AspNetCore.Identity; +using Bit.Core.Models.Table; +using Bit.Core.Enums; +using System.Linq; + +namespace Bit.Api.Controllers +{ + [Route("two-factor")] + [Authorize("Application")] + public class TwoFactorController : Controller + { + private readonly IUserService _userService; + private readonly UserManager _userManager; + + public TwoFactorController( + IUserService userService, + UserManager userManager) + { + _userService = userService; + _userManager = userManager; + } + + [HttpGet("")] + public async Task> Get() + { + var user = await _userService.GetUserByPrincipalAsync(User); + if(user == null) + { + throw new UnauthorizedAccessException(); + } + + var providers = user.GetTwoFactorProviders().Select(p => new TwoFactorProviderResponseModel(p.Key, p.Value)); + return new ListResponseModel(providers); + } + + [HttpPost("get-authenticator")] + public async Task GetAuthenticator([FromBody]TwoFactorRequestModel model) + { + var user = await GetProviderAsync(model, TwoFactorProviderType.Authenticator); + var response = new TwoFactorAuthenticatorResponseModel(user); + return response; + } + + [HttpPut("authenticator")] + [HttpPost("authenticator")] + public async Task PutAuthenticator( + [FromBody]UpdateTwoFactorAuthenticatorRequestModel model) + { + var user = await _userService.GetUserByPrincipalAsync(User); + if(user == null) + { + throw new UnauthorizedAccessException(); + } + + if(!await _userManager.CheckPasswordAsync(user, model.MasterPasswordHash)) + { + await Task.Delay(2000); + throw new BadRequestException("MasterPasswordHash", "Invalid password."); + } + + if(!await _userManager.VerifyTwoFactorTokenAsync(user, TwoFactorProviderType.Authenticator.ToString(), model.Token)) + { + await Task.Delay(2000); + throw new BadRequestException("Token", "Invalid token."); + } + + await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Authenticator); + var response = new TwoFactorAuthenticatorResponseModel(user); + return response; + } + + [HttpPost("get-email")] + public async Task GetEmail([FromBody]TwoFactorRequestModel model) + { + var user = await GetProviderAsync(model, TwoFactorProviderType.Email); + var response = new TwoFactorEmailResponseModel(user); + return response; + } + + [HttpPut("email")] + [HttpPost("email")] + public async Task PutEmail([FromBody]UpdateTwoFactorEmailRequestModel model) + { + var user = await _userService.GetUserByPrincipalAsync(User); + if(user == null) + { + throw new UnauthorizedAccessException(); + } + + if(!await _userManager.CheckPasswordAsync(user, model.MasterPasswordHash)) + { + await Task.Delay(2000); + throw new BadRequestException("MasterPasswordHash", "Invalid password."); + } + + if(!await _userManager.VerifyTwoFactorTokenAsync(user, TwoFactorProviderType.Email.ToString(), model.Token)) + { + await Task.Delay(2000); + throw new BadRequestException("Token", "Invalid token."); + } + + await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Email); + + var response = new TwoFactorEmailResponseModel(user); + return response; + } + + [HttpPut("disable")] + [HttpPost("disable")] + public async Task PutDisable([FromBody]TwoFactorProviderRequestModel model) + { + var user = await _userService.GetUserByPrincipalAsync(User); + if(user == null) + { + throw new UnauthorizedAccessException(); + } + + if(!await _userManager.CheckPasswordAsync(user, model.MasterPasswordHash)) + { + await Task.Delay(2000); + throw new BadRequestException("MasterPasswordHash", "Invalid password."); + } + + await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value); + + var response = new TwoFactorEmailResponseModel(user); + return response; + } + + [HttpPost("recover")] + [AllowAnonymous] + public async Task PostTwoFactorRecover([FromBody]TwoFactorRecoveryRequestModel model) + { + if(!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode)) + { + await Task.Delay(2000); + throw new BadRequestException(string.Empty, "Invalid information. Try again."); + } + } + + private async Task GetProviderAsync(TwoFactorRequestModel model, TwoFactorProviderType type) + { + var user = await _userService.GetUserByPrincipalAsync(User); + if(user == null) + { + throw new UnauthorizedAccessException(); + } + + if(!await _userManager.CheckPasswordAsync(user, model.MasterPasswordHash)) + { + await Task.Delay(2000); + throw new BadRequestException("MasterPasswordHash", "Invalid password."); + } + + await _userService.SetupTwoFactorAsync(user, type); + return user; + } + } +} diff --git a/src/Core/Models/Api/Request/Accounts/RecoverTwoFactorRequestModel.cs b/src/Core/Models/Api/Request/Accounts/RecoverTwoFactorRequestModel.cs deleted file mode 100644 index 6f227c084f..0000000000 --- a/src/Core/Models/Api/Request/Accounts/RecoverTwoFactorRequestModel.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace Bit.Core.Models.Api -{ - public class RecoverTwoFactorRequestModel - { - [Required] - [EmailAddress] - [StringLength(50)] - public string Email { get; set; } - [Required] - public string MasterPasswordHash { get; set; } - [Required] - [StringLength(32)] - public string RecoveryCode { get; set; } - } -} diff --git a/src/Core/Models/Api/Request/Accounts/UpdateTwoFactorRequestModel.cs b/src/Core/Models/Api/Request/Accounts/UpdateTwoFactorRequestModel.cs deleted file mode 100644 index ec085a94e2..0000000000 --- a/src/Core/Models/Api/Request/Accounts/UpdateTwoFactorRequestModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace Bit.Core.Models.Api -{ - public class UpdateTwoFactorRequestModel - { - [Required] - public string MasterPasswordHash { get; set; } - [Required] - public bool? Enabled { get; set; } - [Required] - [StringLength(50)] - public string Token { get; set; } - } -} diff --git a/src/Core/Models/Api/Request/TwoFactorRequestModels.cs b/src/Core/Models/Api/Request/TwoFactorRequestModels.cs new file mode 100644 index 0000000000..f4c4742fcf --- /dev/null +++ b/src/Core/Models/Api/Request/TwoFactorRequestModels.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class UpdateTwoFactorAuthenticatorRequestModel : TwoFactorRequestModel + { + [Required] + [StringLength(50)] + public string Token { get; set; } + } + + public class UpdateTwoFactorDuoRequestModel : TwoFactorRequestModel + { + [Required] + [StringLength(50)] + public string IntegrationKey { get; set; } + [Required] + [StringLength(50)] + public string SecretKey { get; set; } + [Required] + [StringLength(50)] + public string Host { get; set; } + } + + public class UpdateTwoFactorYubicoOtpRequestModel : TwoFactorRequestModel, IValidatableObject + { + public string Key1 { get; set; } + public string Key2 { get; set; } + public string Key3 { get; set; } + public string Key4 { get; set; } + public string Key5 { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + if(string.IsNullOrWhiteSpace(Key1) && string.IsNullOrWhiteSpace(Key2) && string.IsNullOrWhiteSpace(Key3) && + string.IsNullOrWhiteSpace(Key4) && string.IsNullOrWhiteSpace(Key5)) + { + yield return new ValidationResult("A Key is required.", new string[] { nameof(Key1) }); + } + } + } + + public class UpdateTwoFactorEmailRequestModel : TwoFactorRequestModel + { + [Required] + [EmailAddress] + [StringLength(50)] + public string Email { get; set; } + [Required] + [StringLength(50)] + public string Token { get; set; } + } + + public class TwoFactorProviderRequestModel : TwoFactorRequestModel + { + [Required] + public Enums.TwoFactorProviderType? Type { get; set; } + } + + public class TwoFactorRequestModel + { + [Required] + public string MasterPasswordHash { get; set; } + } + + public class TwoFactorRecoveryRequestModel + { + [Required] + [EmailAddress] + [StringLength(50)] + public string Email { get; set; } + [Required] + public string MasterPasswordHash { get; set; } + [Required] + [StringLength(32)] + public string RecoveryCode { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/ProfileResponseModel.cs b/src/Core/Models/Api/Response/ProfileResponseModel.cs index 2a12c23fbb..1dbbfca514 100644 --- a/src/Core/Models/Api/Response/ProfileResponseModel.cs +++ b/src/Core/Models/Api/Response/ProfileResponseModel.cs @@ -3,7 +3,6 @@ using Bit.Core.Models.Table; using System.Collections.Generic; using System.Linq; using Bit.Core.Models.Data; -using Bit.Core.Enums; namespace Bit.Core.Models.Api { diff --git a/src/Core/Models/Api/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs b/src/Core/Models/Api/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs new file mode 100644 index 0000000000..2b4f03ea7d --- /dev/null +++ b/src/Core/Models/Api/Response/TwoFactor/TwoFactorAuthenticatorResponseModel.cs @@ -0,0 +1,32 @@ +using System; +using Bit.Core.Enums; +using Bit.Core.Models.Table; + +namespace Bit.Core.Models.Api +{ + public class TwoFactorAuthenticatorResponseModel : ResponseModel + { + public TwoFactorAuthenticatorResponseModel(User user) + : base("twoFactorAuthenticator") + { + if(user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator); + if(provider?.MetaData?.ContainsKey("Key") ?? false) + { + Key = provider.MetaData["Key"]; + Enabled = provider.Enabled; + } + else + { + Enabled = false; + } + } + + public bool Enabled { get; set; } + public string Key { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/TwoFactor/TwoFactorEmailResponseModel.cs b/src/Core/Models/Api/Response/TwoFactor/TwoFactorEmailResponseModel.cs new file mode 100644 index 0000000000..dacccb2672 --- /dev/null +++ b/src/Core/Models/Api/Response/TwoFactor/TwoFactorEmailResponseModel.cs @@ -0,0 +1,32 @@ +using System; +using Bit.Core.Enums; +using Bit.Core.Models.Table; + +namespace Bit.Core.Models.Api +{ + public class TwoFactorEmailResponseModel : ResponseModel + { + public TwoFactorEmailResponseModel(User user) + : base("twoFactorEmail") + { + if(user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); + if(provider?.MetaData?.ContainsKey("Email") ?? false) + { + Email = provider.MetaData["Email"]; + Enabled = provider.Enabled; + } + else + { + Enabled = false; + } + } + + public bool Enabled { get; set; } + public string Email { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/TwoFactor/TwoFactorProviderResponseModel.cs b/src/Core/Models/Api/Response/TwoFactor/TwoFactorProviderResponseModel.cs new file mode 100644 index 0000000000..aabb308ff0 --- /dev/null +++ b/src/Core/Models/Api/Response/TwoFactor/TwoFactorProviderResponseModel.cs @@ -0,0 +1,23 @@ +using System; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Api +{ + public class TwoFactorProviderResponseModel : ResponseModel + { + public TwoFactorProviderResponseModel(TwoFactorProviderType type, TwoFactorProvider provider) + : base("twoFactorProvider") + { + if(provider == null) + { + throw new ArgumentNullException(nameof(provider)); + } + + Enabled = provider.Enabled; + Type = type; + } + + public bool Enabled { get; set; } + public TwoFactorProviderType Type { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs b/src/Core/Models/Api/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs new file mode 100644 index 0000000000..c2332aa597 --- /dev/null +++ b/src/Core/Models/Api/Response/TwoFactor/TwoFactorYubiKeyResponseModel.cs @@ -0,0 +1,56 @@ +using System; +using Bit.Core.Enums; +using Bit.Core.Models.Table; + +namespace Bit.Core.Models.Api +{ + public class TwoFactorYubiKeyResponseModel : ResponseModel + { + public TwoFactorYubiKeyResponseModel(User user) + : base("twoFactorYubiKey") + { + if(user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); + if(provider?.MetaData != null && provider.MetaData.Count > 0) + { + Enabled = provider.Enabled; + + if(provider.MetaData.ContainsKey("Key1")) + { + Key1 = provider.MetaData["Key1"]; + } + if(provider.MetaData.ContainsKey("Key2")) + { + Key2 = provider.MetaData["Key2"]; + } + if(provider.MetaData.ContainsKey("Key3")) + { + Key1 = provider.MetaData["Key3"]; + } + if(provider.MetaData.ContainsKey("Key4")) + { + Key4 = provider.MetaData["Key4"]; + } + if(provider.MetaData.ContainsKey("Key5")) + { + Key5 = provider.MetaData["Key5"]; + } + } + else + { + Enabled = false; + } + } + + public bool Enabled { get; set; } + public string Key1 { get; set; } + public string Key2 { get; set; } + public string Key3 { get; set; } + public string Key4 { get; set; } + public string Key5 { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/TwoFactorResponseModel.cs b/src/Core/Models/Api/Response/TwoFactorResponseModel.cs deleted file mode 100644 index 698e98e8eb..0000000000 --- a/src/Core/Models/Api/Response/TwoFactorResponseModel.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using Bit.Core.Models.Table; -using Bit.Core.Enums; - -namespace Bit.Core.Models.Api -{ - public class TwoFactorResponseModel : ResponseModel - { - public TwoFactorResponseModel(User user) - : base("twoFactor") - { - if(user == null) - { - throw new ArgumentNullException(nameof(user)); - } - - var providers = user.GetTwoFactorProviders(); - if(user.TwoFactorProvider.HasValue && providers.ContainsKey(user.TwoFactorProvider.Value)) - { - var provider = providers[user.TwoFactorProvider.Value]; - switch(user.TwoFactorProvider.Value) - { - case TwoFactorProviderType.Authenticator: - AuthenticatorKey = provider.MetaData["Key"]; - break; - default: - break; - } - } - else - { - TwoFactorEnabled = false; - } - - TwoFactorEnabled = user.TwoFactorIsEnabled(); - TwoFactorProvider = user.TwoFactorProvider; - TwoFactorRecoveryCode = user.TwoFactorRecoveryCode; - } - - public bool TwoFactorEnabled { get; set; } - public TwoFactorProviderType? TwoFactorProvider { get; set; } - public string AuthenticatorKey { get; set; } - public string TwoFactorRecoveryCode { get; set; } - } -} diff --git a/src/Core/Models/TwoFactorProvider.cs b/src/Core/Models/TwoFactorProvider.cs index 638164b613..5a2628b3d9 100644 --- a/src/Core/Models/TwoFactorProvider.cs +++ b/src/Core/Models/TwoFactorProvider.cs @@ -1,12 +1,10 @@ -using Bit.Core.Enums; -using System.Collections.Generic; +using System.Collections.Generic; namespace Bit.Core.Models { public class TwoFactorProvider { public bool Enabled { get; set; } - public bool Remember { get; set; } public Dictionary MetaData { get; set; } = new Dictionary(); } } diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 835e5b2b9e..71dd21329a 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -28,6 +28,7 @@ namespace Bit.Core.Services Task RefreshSecurityStampAsync(User user, string masterPasswordHash); Task SetupTwoFactorAsync(User user, TwoFactorProviderType provider); Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type); + Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type); Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode); Task GenerateUserTokenAsync(User user, string tokenProvider, string purpose); Task DeleteAsync(User user); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 7b2ca069cb..86145e6cae 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -319,8 +319,7 @@ namespace Bit.Core.Services public async Task SetupTwoFactorAsync(User user, TwoFactorProviderType provider) { var providers = user.GetTwoFactorProviders(); - if(providers != null && providers.ContainsKey(provider) && providers[provider].Enabled && - user.TwoFactorProvider.HasValue && user.TwoFactorProvider.Value == provider) + if(providers != null && providers.ContainsKey(provider) && providers[provider].MetaData != null) { switch(provider) { @@ -355,8 +354,7 @@ namespace Bit.Core.Services { case TwoFactorProviderType.Authenticator: var key = KeyGeneration.GenerateRandomKey(20); - providerInfo.MetaData["Key"] = Base32Encoding.ToString(key); - providerInfo.Remember = true; + providerInfo.MetaData = new Dictionary { ["Key"] = Base32Encoding.ToString(key) }; break; default: throw new ArgumentException(nameof(provider)); @@ -375,11 +373,26 @@ namespace Bit.Core.Services return; } - providers[type].Enabled = user.TwoFactorEnabled; + providers[type].Enabled = true; user.SetTwoFactorProviders(providers); - user.TwoFactorProvider = type; - user.TwoFactorRecoveryCode = user.TwoFactorIsEnabled() ? Guid.NewGuid().ToString("N") : null; + if(string.IsNullOrWhiteSpace(user.TwoFactorRecoveryCode)) + { + user.TwoFactorRecoveryCode = Guid.NewGuid().ToString("N"); + } + await SaveUserAsync(user); + } + + public async Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type) + { + var providers = user.GetTwoFactorProviders(); + if(!providers?.ContainsKey(type) ?? true) + { + return; + } + + providers.Remove(type); + user.SetTwoFactorProviders(providers); await SaveUserAsync(user); }