diff --git a/src/Api/Controllers/TwoFactorController.cs b/src/Api/Controllers/TwoFactorController.cs index b0e22c9c8..ba3fd3ade 100644 --- a/src/Api/Controllers/TwoFactorController.cs +++ b/src/Api/Controllers/TwoFactorController.cs @@ -83,6 +83,24 @@ namespace Bit.Api.Controllers var response = new TwoFactorEmailResponseModel(user); return response; } + + [HttpPost("send-email")] + public async Task SendEmail([FromBody]TwoFactorEmailRequestModel 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.SendTwoFactorEmailAsync(user, model.Email); + } [HttpPut("email")] [HttpPost("email")] @@ -100,12 +118,18 @@ namespace Bit.Api.Controllers throw new BadRequestException("MasterPasswordHash", "Invalid password."); } - if(!await _userManager.VerifyTwoFactorTokenAsync(user, TwoFactorProviderType.Email.ToString(), model.Token)) + if(!await _userService.VerifyTwoFactorEmailAsync(user, model.Token, model.Email)) { await Task.Delay(2000); throw new BadRequestException("Token", "Invalid token."); } + var providers = user.GetTwoFactorProviders(); + providers[TwoFactorProviderType.Email] = new Core.Models.TwoFactorProvider + { + MetaData = new System.Collections.Generic.Dictionary { ["Email"] = model.Email } + }; + user.SetTwoFactorProviders(providers); await _userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.Email); var response = new TwoFactorEmailResponseModel(user); diff --git a/src/Core/Models/Api/Request/TwoFactorRequestModels.cs b/src/Core/Models/Api/Request/TwoFactorRequestModels.cs index f4c4742fc..a4a39b4e4 100644 --- a/src/Core/Models/Api/Request/TwoFactorRequestModels.cs +++ b/src/Core/Models/Api/Request/TwoFactorRequestModels.cs @@ -42,12 +42,16 @@ namespace Bit.Core.Models.Api } } - public class UpdateTwoFactorEmailRequestModel : TwoFactorRequestModel + public class TwoFactorEmailRequestModel : TwoFactorRequestModel { [Required] [EmailAddress] [StringLength(50)] public string Email { get; set; } + } + + public class UpdateTwoFactorEmailRequestModel : TwoFactorEmailRequestModel + { [Required] [StringLength(50)] public string Token { get; set; } diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 71dd21329..baf8e21fc 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -19,6 +19,8 @@ namespace Bit.Core.Services Task SaveUserAsync(User user); Task RegisterUserAsync(User user, string masterPassword); Task SendMasterPasswordHintAsync(string email); + Task SendTwoFactorEmailAsync(User user, string email = null); + Task VerifyTwoFactorEmailAsync(User user, string token, string email = null); Task InitiateEmailChangeAsync(User user, string newEmail); Task ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, string token, string key); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index bbe4ef01e..1f36e9cfc 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -182,6 +182,45 @@ namespace Bit.Core.Services await _mailService.SendMasterPasswordHintEmailAsync(email, user.MasterPasswordHint); } + public async Task SendTwoFactorEmailAsync(User user, string email = null) + { + if(string.IsNullOrWhiteSpace(email)) + { + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); + if(provider != null && provider.MetaData != null && provider.MetaData.ContainsKey("Email")) + { + email = provider.MetaData["Email"]; + } + } + + if(string.IsNullOrWhiteSpace(email)) + { + throw new ArgumentNullException(nameof(email)); + } + + var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider, "2faEmail:" + email); + await _mailService.SendChangeEmailEmailAsync(email, token); + } + + public async Task VerifyTwoFactorEmailAsync(User user, string token, string email = null) + { + if(string.IsNullOrWhiteSpace(email)) + { + var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); + if(provider != null && provider.MetaData != null && provider.MetaData.ContainsKey("Email")) + { + email = provider.MetaData["Email"]; + } + } + + if(string.IsNullOrWhiteSpace(email)) + { + throw new ArgumentNullException(nameof(email)); + } + + return await base.VerifyUserTokenAsync(user, TokenOptions.DefaultEmailProvider, "2faEmail:" + email, token); + } + public async Task InitiateEmailChangeAsync(User user, string newEmail) { var existingUser = await _userRepository.GetByEmailAsync(newEmail); @@ -329,6 +368,11 @@ namespace Bit.Core.Services return; } break; + case TwoFactorProviderType.Email: + case TwoFactorProviderType.U2F: + case TwoFactorProviderType.YubiKey: + case TwoFactorProviderType.Duo: + break; default: throw new ArgumentException(nameof(provider)); } @@ -356,6 +400,11 @@ namespace Bit.Core.Services var key = KeyGeneration.GenerateRandomKey(20); providerInfo.MetaData = new Dictionary { ["Key"] = Base32Encoding.ToString(key) }; break; + case TwoFactorProviderType.Email: + case TwoFactorProviderType.U2F: + case TwoFactorProviderType.YubiKey: + case TwoFactorProviderType.Duo: + break; default: throw new ArgumentException(nameof(provider)); }