1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

Require 2FA token in order to disiable 2FA. Added 2FA recovery code to data/domain model and exposed recover and regenerate 2FA APIs

This commit is contained in:
Kyle Spearrin
2016-11-14 21:13:53 -05:00
parent 17f8d0f677
commit e68ed04f77
8 changed files with 104 additions and 23 deletions

View File

@ -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<TwoFactorResponseModel> 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<TwoFactorResponseModel> 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)
{

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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<ValidationResult> Validate(ValidationContext validationContext)
{
if(Enabled.HasValue && Enabled.Value && string.IsNullOrWhiteSpace(Token))
{
yield return new ValidationResult("Token is required.", new[] { "Token" });
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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;

View File

@ -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

View File

@ -10,6 +10,7 @@
[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)