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:
@ -194,7 +194,7 @@ namespace Bit.Api.Controllers
|
|||||||
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
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);
|
await Task.Delay(2000);
|
||||||
throw new BadRequestException("Token", "Invalid token.");
|
throw new BadRequestException("Token", "Invalid token.");
|
||||||
@ -202,12 +202,66 @@ namespace Bit.Api.Controllers
|
|||||||
|
|
||||||
user.TwoFactorProvider = TwoFactorProvider.Authenticator;
|
user.TwoFactorProvider = TwoFactorProvider.Authenticator;
|
||||||
user.TwoFactorEnabled = model.Enabled.Value;
|
user.TwoFactorEnabled = model.Enabled.Value;
|
||||||
|
user.TwoFactorRecoveryCode = user.TwoFactorEnabled ? Guid.NewGuid().ToString("N") : null;
|
||||||
await _userService.SaveUserAsync(user);
|
await _userService.SaveUserAsync(user);
|
||||||
|
|
||||||
var response = new TwoFactorResponseModel(user);
|
var response = new TwoFactorResponseModel(user);
|
||||||
return response;
|
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")]
|
[HttpPost("delete")]
|
||||||
public async Task PostDelete([FromBody]DeleteAccountRequestModel model)
|
public async Task PostDelete([FromBody]DeleteAccountRequestModel model)
|
||||||
{
|
{
|
||||||
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
@ -3,21 +3,14 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
|
|
||||||
namespace Bit.Api.Models
|
namespace Bit.Api.Models
|
||||||
{
|
{
|
||||||
public class UpdateTwoFactorRequestModel : IValidatableObject
|
public class UpdateTwoFactorRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
public string MasterPasswordHash { get; set; }
|
public string MasterPasswordHash { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
public bool? Enabled { get; set; }
|
public bool? Enabled { get; set; }
|
||||||
|
[Required]
|
||||||
[StringLength(50)]
|
[StringLength(50)]
|
||||||
public string Token { get; set; }
|
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" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,10 +17,12 @@ namespace Bit.Api.Models
|
|||||||
TwoFactorEnabled = user.TwoFactorEnabled;
|
TwoFactorEnabled = user.TwoFactorEnabled;
|
||||||
AuthenticatorKey = user.AuthenticatorKey;
|
AuthenticatorKey = user.AuthenticatorKey;
|
||||||
TwoFactorProvider = user.TwoFactorProvider;
|
TwoFactorProvider = user.TwoFactorProvider;
|
||||||
|
TwoFactorRecoveryCode = user.TwoFactorRecoveryCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TwoFactorEnabled { get; set; }
|
public bool TwoFactorEnabled { get; set; }
|
||||||
public TwoFactorProvider? TwoFactorProvider { get; set; }
|
public TwoFactorProvider? TwoFactorProvider { get; set; }
|
||||||
public string AuthenticatorKey { get; set; }
|
public string AuthenticatorKey { get; set; }
|
||||||
|
public string TwoFactorRecoveryCode { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ namespace Bit.Core.Domains
|
|||||||
public bool TwoFactorEnabled { get; set; }
|
public bool TwoFactorEnabled { get; set; }
|
||||||
public TwoFactorProvider? TwoFactorProvider { get; set; }
|
public TwoFactorProvider? TwoFactorProvider { get; set; }
|
||||||
public string AuthenticatorKey { get; set; }
|
public string AuthenticatorKey { get; set; }
|
||||||
|
public string TwoFactorRecoveryCode { get; set; }
|
||||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
@TwoFactorEnabled BIT,
|
@TwoFactorEnabled BIT,
|
||||||
@TwoFactorProvider TINYINT,
|
@TwoFactorProvider TINYINT,
|
||||||
@AuthenticatorKey NVARCHAR(50),
|
@AuthenticatorKey NVARCHAR(50),
|
||||||
|
@TwoFactorRecoveryCode NVARCHAR(32),
|
||||||
@CreationDate DATETIME2(7),
|
@CreationDate DATETIME2(7),
|
||||||
@RevisionDate DATETIME2(7)
|
@RevisionDate DATETIME2(7)
|
||||||
AS
|
AS
|
||||||
@ -29,6 +30,7 @@ BEGIN
|
|||||||
[TwoFactorEnabled] = @TwoFactorEnabled,
|
[TwoFactorEnabled] = @TwoFactorEnabled,
|
||||||
[TwoFactorProvider] = @TwoFactorProvider,
|
[TwoFactorProvider] = @TwoFactorProvider,
|
||||||
[AuthenticatorKey] = @AuthenticatorKey,
|
[AuthenticatorKey] = @AuthenticatorKey,
|
||||||
|
[TwoFactorRecoveryCode] = @TwoFactorRecoveryCode,
|
||||||
[CreationDate] = @CreationDate,
|
[CreationDate] = @CreationDate,
|
||||||
[RevisionDate] = @RevisionDate
|
[RevisionDate] = @RevisionDate
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
CREATE TABLE [dbo].[User] (
|
CREATE TABLE [dbo].[User] (
|
||||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||||
[Name] NVARCHAR (50) NULL,
|
[Name] NVARCHAR (50) NULL,
|
||||||
[Email] NVARCHAR (50) NOT NULL,
|
[Email] NVARCHAR (50) NOT NULL,
|
||||||
[EmailVerified] BIT NOT NULL,
|
[EmailVerified] BIT NOT NULL,
|
||||||
[MasterPassword] NVARCHAR (300) NOT NULL,
|
[MasterPassword] NVARCHAR (300) NOT NULL,
|
||||||
[MasterPasswordHint] NVARCHAR (50) NULL,
|
[MasterPasswordHint] NVARCHAR (50) NULL,
|
||||||
[Culture] NVARCHAR (10) NOT NULL,
|
[Culture] NVARCHAR (10) NOT NULL,
|
||||||
[SecurityStamp] NVARCHAR (50) NOT NULL,
|
[SecurityStamp] NVARCHAR (50) NOT NULL,
|
||||||
[TwoFactorEnabled] BIT NOT NULL,
|
[TwoFactorEnabled] BIT NOT NULL,
|
||||||
[TwoFactorProvider] TINYINT NULL,
|
[TwoFactorProvider] TINYINT NULL,
|
||||||
[AuthenticatorKey] NVARCHAR (50) NULL,
|
[AuthenticatorKey] NVARCHAR (50) NULL,
|
||||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
[TwoFactorRecoveryCode] NVARCHAR (32) NULL,
|
||||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||||
|
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||||
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
|
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user