1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-04 17:42:49 -05:00

[PM-4168] update keys for WebAuthnLoginCredential (#3506)

* allow update of webauthnlogincredential

* Added Tests

* fixed tests to use commands

* addressing various feedback items
This commit is contained in:
Ike
2023-12-15 13:38:34 -08:00
committed by GitHub
parent d488ebec0f
commit 767c58466c
8 changed files with 286 additions and 22 deletions

View File

@ -5,6 +5,8 @@ using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Api.Response.Accounts;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
@ -23,26 +25,36 @@ namespace Bit.Api.Auth.Controllers;
public class WebAuthnController : Controller
{
private readonly IUserService _userService;
private readonly IPolicyService _policyService;
private readonly IWebAuthnCredentialRepository _credentialRepository;
private readonly IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> _createOptionsDataProtector;
private readonly IPolicyService _policyService;
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
private readonly IGetWebAuthnLoginCredentialCreateOptionsCommand _getWebAuthnLoginCredentialCreateOptionsCommand;
private readonly ICreateWebAuthnLoginCredentialCommand _createWebAuthnLoginCredentialCommand;
private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand;
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
public WebAuthnController(
IUserService userService,
IPolicyService policyService,
IWebAuthnCredentialRepository credentialRepository,
IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> createOptionsDataProtector,
IPolicyService policyService,
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
IGetWebAuthnLoginCredentialCreateOptionsCommand getWebAuthnLoginCredentialCreateOptionsCommand,
ICreateWebAuthnLoginCredentialCommand createWebAuthnLoginCredentialCommand)
ICreateWebAuthnLoginCredentialCommand createWebAuthnLoginCredentialCommand,
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand)
{
_userService = userService;
_policyService = policyService;
_credentialRepository = credentialRepository;
_createOptionsDataProtector = createOptionsDataProtector;
_policyService = policyService;
_assertionOptionsDataProtector = assertionOptionsDataProtector;
_getWebAuthnLoginCredentialCreateOptionsCommand = getWebAuthnLoginCredentialCreateOptionsCommand;
_createWebAuthnLoginCredentialCommand = createWebAuthnLoginCredentialCommand;
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
}
[HttpGet("")]
@ -54,8 +66,8 @@ public class WebAuthnController : Controller
return new ListResponseModel<WebAuthnCredentialResponseModel>(credentials.Select(c => new WebAuthnCredentialResponseModel(c)));
}
[HttpPost("options")]
public async Task<WebAuthnCredentialCreateOptionsResponseModel> PostOptions([FromBody] SecretVerificationRequestModel model)
[HttpPost("attestation-options")]
public async Task<WebAuthnCredentialCreateOptionsResponseModel> AttestationOptions([FromBody] SecretVerificationRequestModel model)
{
var user = await VerifyUserAsync(model);
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
@ -71,8 +83,24 @@ public class WebAuthnController : Controller
};
}
[HttpPost("assertion-options")]
public async Task<WebAuthnLoginAssertionOptionsResponseModel> AssertionOptions([FromBody] SecretVerificationRequestModel model)
{
await VerifyUserAsync(model);
var options = _getWebAuthnLoginCredentialAssertionOptionsCommand.GetWebAuthnLoginCredentialAssertionOptions();
var tokenable = new WebAuthnLoginAssertionOptionsTokenable(WebAuthnLoginAssertionOptionsScope.UpdateKeySet, options);
var token = _assertionOptionsDataProtector.Protect(tokenable);
return new WebAuthnLoginAssertionOptionsResponseModel
{
Options = options,
Token = token
};
}
[HttpPost("")]
public async Task Post([FromBody] WebAuthnCredentialRequestModel model)
public async Task Post([FromBody] WebAuthnLoginCredentialCreateRequestModel model)
{
var user = await GetUserAsync();
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
@ -100,6 +128,29 @@ public class WebAuthnController : Controller
}
}
[HttpPut()]
public async Task UpdateCredential([FromBody] WebAuthnLoginCredentialUpdateRequestModel model)
{
var tokenable = _assertionOptionsDataProtector.Unprotect(model.Token);
if (!tokenable.TokenIsValid(WebAuthnLoginAssertionOptionsScope.UpdateKeySet))
{
throw new BadRequestException("The token associated with your request is invalid or has expired. A valid token is required to continue.");
}
var (_, credential) = await _assertWebAuthnLoginCredentialCommand.AssertWebAuthnLoginCredential(tokenable.Options, model.DeviceResponse);
if (credential == null || credential.SupportsPrf != true)
{
throw new BadRequestException("Unable to update credential.");
}
// assign new keys to credential
credential.EncryptedUserKey = model.EncryptedUserKey;
credential.EncryptedPrivateKey = model.EncryptedPrivateKey;
credential.EncryptedPublicKey = model.EncryptedPublicKey;
await _credentialRepository.UpdateAsync(credential);
}
[HttpPost("{id}/delete")]
public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model)
{

View File

@ -4,7 +4,7 @@ using Fido2NetLib;
namespace Bit.Api.Auth.Models.Request.Webauthn;
public class WebAuthnCredentialRequestModel
public class WebAuthnLoginCredentialCreateRequestModel
{
[Required]
public AuthenticatorAttestationRawResponse DeviceResponse { get; set; }
@ -30,4 +30,3 @@ public class WebAuthnCredentialRequestModel
[EncryptedStringLength(2000)]
public string EncryptedPrivateKey { get; set; }
}

View File

@ -0,0 +1,29 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
using Fido2NetLib;
namespace Bit.Api.Auth.Models.Request.Webauthn;
public class WebAuthnLoginCredentialUpdateRequestModel
{
[Required]
public AuthenticatorAssertionRawResponse DeviceResponse { get; set; }
[Required]
public string Token { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(2000)]
public string EncryptedUserKey { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(2000)]
public string EncryptedPublicKey { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(2000)]
public string EncryptedPrivateKey { get; set; }
}