diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index 721587cb43..9740a3cda1 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -194,7 +194,7 @@ namespace Bit.Api.Controllers throw new BadRequestException("MasterPasswordHash", "Invalid password."); } - if(model.Enabled.Value && !await _userManager.VerifyTwoFactorTokenAsync(user, "Authenticator", model.Token)) + if(!await _userManager.VerifyTwoFactorTokenAsync(user, "Authenticator", model.Token)) { await Task.Delay(2000); throw new BadRequestException("Token", "Invalid token."); @@ -202,12 +202,66 @@ namespace Bit.Api.Controllers user.TwoFactorProvider = TwoFactorProvider.Authenticator; user.TwoFactorEnabled = model.Enabled.Value; + user.TwoFactorRecoveryCode = user.TwoFactorEnabled ? Guid.NewGuid().ToString("N") : null; await _userService.SaveUserAsync(user); var response = new TwoFactorResponseModel(user); return response; } + [HttpPut("two-factor-recover")] + [HttpPost("two-factor-recover")] + public async Task PutTwoFactorRecover([FromBody]RecoverTwoFactorRequestModel model) + { + var user = _currentContext.User; + if(!await _userManager.CheckPasswordAsync(user, model.MasterPasswordHash)) + { + await Task.Delay(2000); + throw new BadRequestException("MasterPasswordHash", "Invalid password."); + } + + if(string.Compare(user.TwoFactorRecoveryCode, model.RecoveryCode, true) != 0) + { + await Task.Delay(2000); + throw new BadRequestException("RecoveryCode", "Invalid recovery code."); + } + + user.TwoFactorProvider = TwoFactorProvider.Authenticator; + user.TwoFactorEnabled = false; + user.TwoFactorRecoveryCode = null; + await _userService.SaveUserAsync(user); + + var response = new TwoFactorResponseModel(user); + return response; + } + + [HttpPut("two-factor-regenerate")] + [HttpPost("two-factor-regenerate")] + public async Task PutTwoFactorRegenerate([FromBody]RegenerateTwoFactorRequestModel model) + { + var user = _currentContext.User; + if(!await _userManager.CheckPasswordAsync(user, model.MasterPasswordHash)) + { + await Task.Delay(2000); + throw new BadRequestException("MasterPasswordHash", "Invalid password."); + } + + if(!await _userManager.VerifyTwoFactorTokenAsync(user, "Authenticator", model.Token)) + { + await Task.Delay(2000); + throw new BadRequestException("Token", "Invalid token."); + } + + if(user.TwoFactorEnabled) + { + user.TwoFactorRecoveryCode = Guid.NewGuid().ToString("N"); + await _userService.SaveUserAsync(user); + } + + var response = new TwoFactorResponseModel(user); + return response; + } + [HttpPost("delete")] public async Task PostDelete([FromBody]DeleteAccountRequestModel model) { diff --git a/src/Api/Models/Request/Accounts/RecoverTwoFactorRequestModel.cs b/src/Api/Models/Request/Accounts/RecoverTwoFactorRequestModel.cs new file mode 100644 index 0000000000..86faf3db01 --- /dev/null +++ b/src/Api/Models/Request/Accounts/RecoverTwoFactorRequestModel.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.Models +{ + public class RecoverTwoFactorRequestModel + { + [Required] + public string MasterPasswordHash { get; set; } + [Required] + [StringLength(32)] + public string RecoveryCode { get; set; } + } +} diff --git a/src/Api/Models/Request/Accounts/RegenerateTwoFactorRequestModel.cs b/src/Api/Models/Request/Accounts/RegenerateTwoFactorRequestModel.cs new file mode 100644 index 0000000000..d3ab5bee21 --- /dev/null +++ b/src/Api/Models/Request/Accounts/RegenerateTwoFactorRequestModel.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.Models +{ + public class RegenerateTwoFactorRequestModel + { + [Required] + public string MasterPasswordHash { get; set; } + [Required] + [StringLength(50)] + public string Token { get; set; } + } +} diff --git a/src/Api/Models/Request/Accounts/UpdateTwoFactorRequestModel.cs b/src/Api/Models/Request/Accounts/UpdateTwoFactorRequestModel.cs index b18b398146..24bec8dfc8 100644 --- a/src/Api/Models/Request/Accounts/UpdateTwoFactorRequestModel.cs +++ b/src/Api/Models/Request/Accounts/UpdateTwoFactorRequestModel.cs @@ -3,21 +3,14 @@ using System.ComponentModel.DataAnnotations; namespace Bit.Api.Models { - public class UpdateTwoFactorRequestModel : IValidatableObject + public class UpdateTwoFactorRequestModel { [Required] public string MasterPasswordHash { get; set; } [Required] public bool? Enabled { get; set; } + [Required] [StringLength(50)] public string Token { get; set; } - - public IEnumerable Validate(ValidationContext validationContext) - { - if(Enabled.HasValue && Enabled.Value && string.IsNullOrWhiteSpace(Token)) - { - yield return new ValidationResult("Token is required.", new[] { "Token" }); - } - } } } diff --git a/src/Api/Models/Response/TwoFactorResponseModel.cs b/src/Api/Models/Response/TwoFactorResponseModel.cs index 28d346af7e..f2db89c3a1 100644 --- a/src/Api/Models/Response/TwoFactorResponseModel.cs +++ b/src/Api/Models/Response/TwoFactorResponseModel.cs @@ -17,10 +17,12 @@ namespace Bit.Api.Models TwoFactorEnabled = user.TwoFactorEnabled; AuthenticatorKey = user.AuthenticatorKey; TwoFactorProvider = user.TwoFactorProvider; + TwoFactorRecoveryCode = user.TwoFactorRecoveryCode; } public bool TwoFactorEnabled { get; set; } public TwoFactorProvider? TwoFactorProvider { get; set; } public string AuthenticatorKey { get; set; } + public string TwoFactorRecoveryCode { get; set; } } } diff --git a/src/Core/Domains/User.cs b/src/Core/Domains/User.cs index 0a8bda587d..fa627fcfbb 100644 --- a/src/Core/Domains/User.cs +++ b/src/Core/Domains/User.cs @@ -17,6 +17,7 @@ namespace Bit.Core.Domains public bool TwoFactorEnabled { get; set; } public TwoFactorProvider? TwoFactorProvider { get; set; } public string AuthenticatorKey { get; set; } + public string TwoFactorRecoveryCode { get; set; } public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow; diff --git a/src/Sql/dbo/Stored Procedures/User_Update.sql b/src/Sql/dbo/Stored Procedures/User_Update.sql index fa4a52a8e4..ce53d82df8 100644 --- a/src/Sql/dbo/Stored Procedures/User_Update.sql +++ b/src/Sql/dbo/Stored Procedures/User_Update.sql @@ -10,6 +10,7 @@ @TwoFactorEnabled BIT, @TwoFactorProvider TINYINT, @AuthenticatorKey NVARCHAR(50), + @TwoFactorRecoveryCode NVARCHAR(32), @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7) AS @@ -29,6 +30,7 @@ BEGIN [TwoFactorEnabled] = @TwoFactorEnabled, [TwoFactorProvider] = @TwoFactorProvider, [AuthenticatorKey] = @AuthenticatorKey, + [TwoFactorRecoveryCode] = @TwoFactorRecoveryCode, [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate WHERE diff --git a/src/Sql/dbo/Tables/User.sql b/src/Sql/dbo/Tables/User.sql index 016b3ce518..08a8684a57 100644 --- a/src/Sql/dbo/Tables/User.sql +++ b/src/Sql/dbo/Tables/User.sql @@ -1,17 +1,18 @@ CREATE TABLE [dbo].[User] ( - [Id] UNIQUEIDENTIFIER NOT NULL, - [Name] NVARCHAR (50) NULL, - [Email] NVARCHAR (50) NOT NULL, - [EmailVerified] BIT NOT NULL, - [MasterPassword] NVARCHAR (300) NOT NULL, - [MasterPasswordHint] NVARCHAR (50) NULL, - [Culture] NVARCHAR (10) NOT NULL, - [SecurityStamp] NVARCHAR (50) NOT NULL, - [TwoFactorEnabled] BIT NOT NULL, - [TwoFactorProvider] TINYINT NULL, - [AuthenticatorKey] NVARCHAR (50) NULL, - [CreationDate] DATETIME2 (7) NOT NULL, - [RevisionDate] DATETIME2 (7) NOT NULL, + [Id] UNIQUEIDENTIFIER NOT NULL, + [Name] NVARCHAR (50) NULL, + [Email] NVARCHAR (50) NOT NULL, + [EmailVerified] BIT NOT NULL, + [MasterPassword] NVARCHAR (300) NOT NULL, + [MasterPasswordHint] NVARCHAR (50) NULL, + [Culture] NVARCHAR (10) NOT NULL, + [SecurityStamp] NVARCHAR (50) NOT NULL, + [TwoFactorEnabled] BIT NOT NULL, + [TwoFactorProvider] TINYINT NULL, + [AuthenticatorKey] NVARCHAR (50) NULL, + [TwoFactorRecoveryCode] NVARCHAR (32) NULL, + [CreationDate] DATETIME2 (7) NOT NULL, + [RevisionDate] DATETIME2 (7) NOT NULL, CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC) );