mirror of
https://github.com/bitwarden/server.git
synced 2025-05-20 11:04:31 -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:
parent
d488ebec0f
commit
767c58466c
@ -5,6 +5,8 @@ using Bit.Api.Models.Response;
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Services;
|
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.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||||
@ -23,26 +25,36 @@ namespace Bit.Api.Auth.Controllers;
|
|||||||
public class WebAuthnController : Controller
|
public class WebAuthnController : Controller
|
||||||
{
|
{
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
|
private readonly IPolicyService _policyService;
|
||||||
private readonly IWebAuthnCredentialRepository _credentialRepository;
|
private readonly IWebAuthnCredentialRepository _credentialRepository;
|
||||||
private readonly IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> _createOptionsDataProtector;
|
private readonly IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> _createOptionsDataProtector;
|
||||||
private readonly IPolicyService _policyService;
|
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||||
private readonly IGetWebAuthnLoginCredentialCreateOptionsCommand _getWebAuthnLoginCredentialCreateOptionsCommand;
|
private readonly IGetWebAuthnLoginCredentialCreateOptionsCommand _getWebAuthnLoginCredentialCreateOptionsCommand;
|
||||||
private readonly ICreateWebAuthnLoginCredentialCommand _createWebAuthnLoginCredentialCommand;
|
private readonly ICreateWebAuthnLoginCredentialCommand _createWebAuthnLoginCredentialCommand;
|
||||||
|
private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand;
|
||||||
|
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||||
|
|
||||||
public WebAuthnController(
|
public WebAuthnController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
|
IPolicyService policyService,
|
||||||
IWebAuthnCredentialRepository credentialRepository,
|
IWebAuthnCredentialRepository credentialRepository,
|
||||||
IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> createOptionsDataProtector,
|
IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> createOptionsDataProtector,
|
||||||
IPolicyService policyService,
|
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
||||||
IGetWebAuthnLoginCredentialCreateOptionsCommand getWebAuthnLoginCredentialCreateOptionsCommand,
|
IGetWebAuthnLoginCredentialCreateOptionsCommand getWebAuthnLoginCredentialCreateOptionsCommand,
|
||||||
ICreateWebAuthnLoginCredentialCommand createWebAuthnLoginCredentialCommand)
|
ICreateWebAuthnLoginCredentialCommand createWebAuthnLoginCredentialCommand,
|
||||||
|
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
|
||||||
|
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
|
_policyService = policyService;
|
||||||
_credentialRepository = credentialRepository;
|
_credentialRepository = credentialRepository;
|
||||||
_createOptionsDataProtector = createOptionsDataProtector;
|
_createOptionsDataProtector = createOptionsDataProtector;
|
||||||
_policyService = policyService;
|
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||||
_getWebAuthnLoginCredentialCreateOptionsCommand = getWebAuthnLoginCredentialCreateOptionsCommand;
|
_getWebAuthnLoginCredentialCreateOptionsCommand = getWebAuthnLoginCredentialCreateOptionsCommand;
|
||||||
_createWebAuthnLoginCredentialCommand = createWebAuthnLoginCredentialCommand;
|
_createWebAuthnLoginCredentialCommand = createWebAuthnLoginCredentialCommand;
|
||||||
|
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||||
|
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
@ -54,8 +66,8 @@ public class WebAuthnController : Controller
|
|||||||
return new ListResponseModel<WebAuthnCredentialResponseModel>(credentials.Select(c => new WebAuthnCredentialResponseModel(c)));
|
return new ListResponseModel<WebAuthnCredentialResponseModel>(credentials.Select(c => new WebAuthnCredentialResponseModel(c)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("options")]
|
[HttpPost("attestation-options")]
|
||||||
public async Task<WebAuthnCredentialCreateOptionsResponseModel> PostOptions([FromBody] SecretVerificationRequestModel model)
|
public async Task<WebAuthnCredentialCreateOptionsResponseModel> AttestationOptions([FromBody] SecretVerificationRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await VerifyUserAsync(model);
|
var user = await VerifyUserAsync(model);
|
||||||
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
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("")]
|
[HttpPost("")]
|
||||||
public async Task Post([FromBody] WebAuthnCredentialRequestModel model)
|
public async Task Post([FromBody] WebAuthnLoginCredentialCreateRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await GetUserAsync();
|
var user = await GetUserAsync();
|
||||||
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
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")]
|
[HttpPost("{id}/delete")]
|
||||||
public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model)
|
public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model)
|
||||||
{
|
{
|
||||||
|
@ -4,7 +4,7 @@ using Fido2NetLib;
|
|||||||
|
|
||||||
namespace Bit.Api.Auth.Models.Request.Webauthn;
|
namespace Bit.Api.Auth.Models.Request.Webauthn;
|
||||||
|
|
||||||
public class WebAuthnCredentialRequestModel
|
public class WebAuthnLoginCredentialCreateRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
public AuthenticatorAttestationRawResponse DeviceResponse { get; set; }
|
public AuthenticatorAttestationRawResponse DeviceResponse { get; set; }
|
||||||
@ -30,4 +30,3 @@ public class WebAuthnCredentialRequestModel
|
|||||||
[EncryptedStringLength(2000)]
|
[EncryptedStringLength(2000)]
|
||||||
public string EncryptedPrivateKey { get; set; }
|
public string EncryptedPrivateKey { get; set; }
|
||||||
}
|
}
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
public enum WebAuthnLoginAssertionOptionsScope
|
public enum WebAuthnLoginAssertionOptionsScope
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
|
Authentication is used when a user is trying to login in with a credential.
|
||||||
|
*/
|
||||||
Authentication = 0,
|
Authentication = 0,
|
||||||
PrfRegistration = 1
|
/*
|
||||||
|
PrfRegistration is used when a user is trying to register a new credential.
|
||||||
|
*/
|
||||||
|
PrfRegistration = 1,
|
||||||
|
/*
|
||||||
|
UpdateKeySet is used when a user is enabling a credential for passwordless login
|
||||||
|
This is done by adding rotatable keys to the credential.
|
||||||
|
*/
|
||||||
|
UpdateKeySet = 2
|
||||||
}
|
}
|
||||||
|
@ -7,4 +7,5 @@ public interface IWebAuthnCredentialRepository : IRepository<WebAuthnCredential,
|
|||||||
{
|
{
|
||||||
Task<WebAuthnCredential> GetByIdAsync(Guid id, Guid userId);
|
Task<WebAuthnCredential> GetByIdAsync(Guid id, Guid userId);
|
||||||
Task<ICollection<WebAuthnCredential>> GetManyByUserIdAsync(Guid userId);
|
Task<ICollection<WebAuthnCredential>> GetManyByUserIdAsync(Guid userId);
|
||||||
|
Task<bool> UpdateAsync(WebAuthnCredential credential);
|
||||||
}
|
}
|
||||||
|
@ -44,4 +44,15 @@ public class WebAuthnCredentialRepository : Repository<WebAuthnCredential, Guid>
|
|||||||
return results.ToList();
|
return results.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateAsync(WebAuthnCredential credential)
|
||||||
|
{
|
||||||
|
using var connection = new SqlConnection(ConnectionString);
|
||||||
|
var affectedRows = await connection.ExecuteAsync(
|
||||||
|
$"[{Schema}].[{Table}_Update]",
|
||||||
|
credential,
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return affectedRows > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,4 +34,26 @@ public class WebAuthnCredentialRepository : Repository<Core.Auth.Entities.WebAut
|
|||||||
return Mapper.Map<List<Core.Auth.Entities.WebAuthnCredential>>(creds);
|
return Mapper.Map<List<Core.Auth.Entities.WebAuthnCredential>>(creds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateAsync(Core.Auth.Entities.WebAuthnCredential credential)
|
||||||
|
{
|
||||||
|
using (var scope = ServiceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var cred = await dbContext.WebAuthnCredentials
|
||||||
|
.FirstOrDefaultAsync(d => d.Id == credential.Id &&
|
||||||
|
d.UserId == credential.UserId);
|
||||||
|
if (cred == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cred.EncryptedPrivateKey = credential.EncryptedPrivateKey;
|
||||||
|
cred.EncryptedPublicKey = credential.EncryptedPublicKey;
|
||||||
|
cred.EncryptedUserKey = credential.EncryptedUserKey;
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,10 @@ using Bit.Api.Auth.Models.Request.Accounts;
|
|||||||
using Bit.Api.Auth.Models.Request.Webauthn;
|
using Bit.Api.Auth.Models.Request.Webauthn;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Auth.Entities;
|
||||||
|
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -36,34 +39,34 @@ public class WebAuthnControllerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task PostOptions_UserNotFound_ThrowsUnauthorizedAccessException(SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
public async Task AttestationOptions_UserNotFound_ThrowsUnauthorizedAccessException(SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = () => sutProvider.Sut.PostOptions(requestModel);
|
var result = () => sutProvider.Sut.AttestationOptions(requestModel);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(result);
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task PostOptions_UserVerificationFailed_ThrowsBadRequestException(SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
public async Task AttestationOptions_UserVerificationFailed_ThrowsBadRequestException(SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).Returns(false);
|
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).Returns(false);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var result = () => sutProvider.Sut.PostOptions(requestModel);
|
var result = () => sutProvider.Sut.AttestationOptions(requestModel);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task PostOptions_RequireSsoPolicyApplicable_ThrowsBadRequestException(
|
public async Task AttestationOptions_RequireSsoPolicyApplicable_ThrowsBadRequestException(
|
||||||
SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@ -73,12 +76,58 @@ public class WebAuthnControllerTests
|
|||||||
|
|
||||||
// Act & Assert
|
// Act & Assert
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
() => sutProvider.Sut.PostOptions(requestModel));
|
() => sutProvider.Sut.AttestationOptions(requestModel));
|
||||||
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Assertion Options
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Post_UserNotFound_ThrowsUnauthorizedAccessException(WebAuthnCredentialRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
public async Task AssertionOptions_UserNotFound_ThrowsUnauthorizedAccessException(SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = () => sutProvider.Sut.AssertionOptions(requestModel);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task AssertionOptions_UserVerificationFailed_ThrowsBadRequestException(SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||||
|
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).Returns(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = () => sutProvider.Sut.AssertionOptions(requestModel);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task AssertionOptions_UserVerificationSuccess_ReturnsAssertionOptions(SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||||
|
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, requestModel.Secret).Returns(true);
|
||||||
|
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>()
|
||||||
|
.Protect(Arg.Any<WebAuthnLoginAssertionOptionsTokenable>()).Returns("token");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.AssertionOptions(requestModel);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.IsType<WebAuthnLoginAssertionOptionsResponseModel>(result);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Post_UserNotFound_ThrowsUnauthorizedAccessException(WebAuthnLoginCredentialCreateRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
||||||
@ -91,7 +140,7 @@ public class WebAuthnControllerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Post_ExpiredToken_ThrowsBadRequestException(WebAuthnCredentialRequestModel requestModel, CredentialCreateOptions createOptions, User user, SutProvider<WebAuthnController> sutProvider)
|
public async Task Post_ExpiredToken_ThrowsBadRequestException(WebAuthnLoginCredentialCreateRequestModel requestModel, CredentialCreateOptions createOptions, User user, SutProvider<WebAuthnController> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);
|
var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);
|
||||||
@ -110,7 +159,7 @@ public class WebAuthnControllerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Post_ValidInput_Returns(WebAuthnCredentialRequestModel requestModel, CredentialCreateOptions createOptions, User user, SutProvider<WebAuthnController> sutProvider)
|
public async Task Post_ValidInput_Returns(WebAuthnLoginCredentialCreateRequestModel requestModel, CredentialCreateOptions createOptions, User user, SutProvider<WebAuthnController> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);
|
var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);
|
||||||
@ -128,12 +177,17 @@ public class WebAuthnControllerTests
|
|||||||
await sutProvider.Sut.Post(requestModel);
|
await sutProvider.Sut.Post(requestModel);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
// Nothing to assert since return is void
|
await sutProvider.GetDependency<IUserService>()
|
||||||
|
.Received(1)
|
||||||
|
.GetUserByPrincipalAsync(default);
|
||||||
|
await sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()
|
||||||
|
.Received(1)
|
||||||
|
.CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Post_RequireSsoPolicyApplicable_ThrowsBadRequestException(
|
public async Task Post_RequireSsoPolicyApplicable_ThrowsBadRequestException(
|
||||||
WebAuthnCredentialRequestModel requestModel,
|
WebAuthnLoginCredentialCreateRequestModel requestModel,
|
||||||
CredentialCreateOptions createOptions,
|
CredentialCreateOptions createOptions,
|
||||||
User user,
|
User user,
|
||||||
SutProvider<WebAuthnController> sutProvider)
|
SutProvider<WebAuthnController> sutProvider)
|
||||||
@ -183,5 +237,91 @@ public class WebAuthnControllerTests
|
|||||||
// Assert
|
// Assert
|
||||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
#region Update Credential
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Put_TokenVerificationFailed_ThrowsBadRequestException(AssertionOptions assertionOptions, WebAuthnLoginCredentialUpdateRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedMessage = "The token associated with your request is invalid or has expired. A valid token is required to continue.";
|
||||||
|
var token = new WebAuthnLoginAssertionOptionsTokenable(
|
||||||
|
Core.Auth.Enums.WebAuthnLoginAssertionOptionsScope.PrfRegistration, assertionOptions);
|
||||||
|
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>()
|
||||||
|
.Unprotect(requestModel.Token)
|
||||||
|
.Returns(token);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateCredential(requestModel));
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedMessage, exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Put_CredentialNotFound_ThrowsBadRequestException(AssertionOptions assertionOptions, WebAuthnLoginCredentialUpdateRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedMessage = "Unable to update credential.";
|
||||||
|
var token = new WebAuthnLoginAssertionOptionsTokenable(
|
||||||
|
Core.Auth.Enums.WebAuthnLoginAssertionOptionsScope.UpdateKeySet, assertionOptions);
|
||||||
|
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>()
|
||||||
|
.Unprotect(requestModel.Token)
|
||||||
|
.Returns(token);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateCredential(requestModel));
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedMessage, exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Put_PrfNotSupported_ThrowsBadRequestException(User user, WebAuthnCredential credential, AssertionOptions assertionOptions, WebAuthnLoginCredentialUpdateRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedMessage = "Unable to update credential.";
|
||||||
|
credential.SupportsPrf = false;
|
||||||
|
var token = new WebAuthnLoginAssertionOptionsTokenable(
|
||||||
|
Core.Auth.Enums.WebAuthnLoginAssertionOptionsScope.UpdateKeySet, assertionOptions);
|
||||||
|
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>()
|
||||||
|
.Unprotect(requestModel.Token)
|
||||||
|
.Returns(token);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAssertWebAuthnLoginCredentialCommand>()
|
||||||
|
.AssertWebAuthnLoginCredential(assertionOptions, requestModel.DeviceResponse)
|
||||||
|
.Returns((user, credential));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateCredential(requestModel));
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(expectedMessage, exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Put_UpdateCredential_Success(User user, WebAuthnCredential credential, AssertionOptions assertionOptions, WebAuthnLoginCredentialUpdateRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var token = new WebAuthnLoginAssertionOptionsTokenable(
|
||||||
|
Core.Auth.Enums.WebAuthnLoginAssertionOptionsScope.UpdateKeySet, assertionOptions);
|
||||||
|
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>()
|
||||||
|
.Unprotect(requestModel.Token)
|
||||||
|
.Returns(token);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAssertWebAuthnLoginCredentialCommand>()
|
||||||
|
.AssertWebAuthnLoginCredential(assertionOptions, requestModel.DeviceResponse)
|
||||||
|
.Returns((user, credential));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.UpdateCredential(requestModel);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable>>()
|
||||||
|
.Received(1)
|
||||||
|
.Unprotect(requestModel.Token);
|
||||||
|
await sutProvider.GetDependency<IAssertWebAuthnLoginCredentialCommand>()
|
||||||
|
.Received(1)
|
||||||
|
.AssertWebAuthnLoginCredential(assertionOptions, requestModel.DeviceResponse);
|
||||||
|
await sutProvider.GetDependency<IWebAuthnCredentialRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.UpdateAsync(credential);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user