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

[PM-9925] Tokenable for User Verification on Two Factor Authenticator settings (#4558)

* initial changes

* Fixing some bits

* fixing issue when feature flag is `false`; also names;

* consume OTP on read if FF true

* comment typo

* fix formatting

* check access code first to not consume token

* add docs

* revert checking access code first

* update error messages

* remove line number from comment

---------

Co-authored-by: Jake Fink <jfink@bitwarden.com>
This commit is contained in:
Ike
2024-07-25 07:51:23 -07:00
committed by GitHub
parent f211e969c7
commit aba2f023cd
6 changed files with 130 additions and 14 deletions

View File

@ -3,6 +3,7 @@ using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Response.TwoFactor;
using Bit.Api.Models.Request;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
using Bit.Core.Auth.Models.Business.Tokenables;
@ -33,7 +34,10 @@ public class TwoFactorController : Controller
private readonly UserManager<User> _userManager;
private readonly ICurrentContext _currentContext;
private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
private readonly IFeatureService _featureService;
private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;
private readonly bool _TwoFactorAuthenticatorTokenFeatureFlagEnabled;
public TwoFactorController(
IUserService userService,
@ -43,7 +47,9 @@ public class TwoFactorController : Controller
UserManager<User> userManager,
ICurrentContext currentContext,
IVerifyAuthRequestCommand verifyAuthRequestCommand,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory)
IFeatureService featureService,
IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> twoFactorAuthenticatorDataProtector,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector)
{
_userService = userService;
_organizationRepository = organizationRepository;
@ -52,7 +58,10 @@ public class TwoFactorController : Controller
_userManager = userManager;
_currentContext = currentContext;
_verifyAuthRequestCommand = verifyAuthRequestCommand;
_tokenDataFactory = tokenDataFactory;
_featureService = featureService;
_twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
_ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
_TwoFactorAuthenticatorTokenFeatureFlagEnabled = _featureService.IsEnabled(FeatureFlagKeys.AuthenticatorTwoFactorToken);
}
[HttpGet("")]
@ -93,8 +102,13 @@ public class TwoFactorController : Controller
public async Task<TwoFactorAuthenticatorResponseModel> GetAuthenticator(
[FromBody] SecretVerificationRequestModel model)
{
var user = await CheckAsync(model, false, true);
var user = _TwoFactorAuthenticatorTokenFeatureFlagEnabled ? await CheckAsync(model, false) : await CheckAsync(model, false, true);
var response = new TwoFactorAuthenticatorResponseModel(user);
if (_TwoFactorAuthenticatorTokenFeatureFlagEnabled)
{
var tokenable = new TwoFactorAuthenticatorUserVerificationTokenable(user, response.Key);
response.UserVerificationToken = _twoFactorAuthenticatorDataProtector.Protect(tokenable);
}
return response;
}
@ -103,8 +117,21 @@ public class TwoFactorController : Controller
public async Task<TwoFactorAuthenticatorResponseModel> PutAuthenticator(
[FromBody] UpdateTwoFactorAuthenticatorRequestModel model)
{
var user = await CheckAsync(model, false);
model.ToUser(user);
User user;
if (_TwoFactorAuthenticatorTokenFeatureFlagEnabled)
{
user = model.ToUser(await _userService.GetUserByPrincipalAsync(User));
_twoFactorAuthenticatorDataProtector.TryUnprotect(model.UserVerificationToken, out var decryptedToken);
if (!decryptedToken.TokenIsValid(user, model.Key))
{
throw new BadRequestException("UserVerificationToken", "User verification failed.");
}
}
else
{
user = await CheckAsync(model, false);
model.ToUser(user); // populates user obj with proper metadata for VerifyTwoFactorTokenAsync
}
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Authenticator), model.Token))
@ -118,6 +145,22 @@ public class TwoFactorController : Controller
return response;
}
[RequireFeature(FeatureFlagKeys.AuthenticatorTwoFactorToken)]
[HttpDelete("authenticator")]
public async Task<TwoFactorProviderResponseModel> DisableAuthenticator(
[FromBody] TwoFactorAuthenticatorDisableRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
_twoFactorAuthenticatorDataProtector.TryUnprotect(model.UserVerificationToken, out var decryptedToken);
if (!decryptedToken.TokenIsValid(user, model.Key))
{
throw new BadRequestException("UserVerificationToken", "User verification failed.");
}
await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value, _organizationService);
return new TwoFactorProviderResponseModel(model.Type.Value, user);
}
[HttpPost("get-yubikey")]
public async Task<TwoFactorYubiKeyResponseModel> GetYubiKey([FromBody] SecretVerificationRequestModel model)
{
@ -477,7 +520,7 @@ public class TwoFactorController : Controller
private bool ValidateSsoEmail2FaToken(string ssoEmail2FaSessionToken, User user)
{
return _tokenDataFactory.TryUnprotect(ssoEmail2FaSessionToken, out var decryptedToken) &&
return _ssoEmailTwoFactorSessionDataProtector.TryUnprotect(ssoEmail2FaSessionToken, out var decryptedToken) &&
decryptedToken.Valid && decryptedToken.TokenIsValid(user);
}

View File

@ -17,7 +17,7 @@ public class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationReques
[Required]
[StringLength(50)]
public string Key { get; set; }
public string UserVerificationToken { get; set; }
public User ToUser(User existingUser)
{
var providers = existingUser.GetTwoFactorProviders();
@ -323,3 +323,11 @@ public class TwoFactorRecoveryRequestModel : TwoFactorEmailRequestModel
[StringLength(32)]
public string RecoveryCode { get; set; }
}
public class TwoFactorAuthenticatorDisableRequestModel : TwoFactorProviderRequestModel
{
[Required]
public string UserVerificationToken { get; set; }
[Required]
public string Key { get; set; }
}

View File

@ -10,10 +10,7 @@ public class TwoFactorAuthenticatorResponseModel : ResponseModel
public TwoFactorAuthenticatorResponseModel(User user)
: base("twoFactorAuthenticator")
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
ArgumentNullException.ThrowIfNull(user);
var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator);
if (provider?.MetaData?.ContainsKey("Key") ?? false)
@ -31,4 +28,5 @@ public class TwoFactorAuthenticatorResponseModel : ResponseModel
public bool Enabled { get; set; }
public string Key { get; set; }
public string UserVerificationToken { get; set; }
}