mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
Auth/PM-3659 - Disable Passkey registration if Require SSO Policy Enabled (#3399)
* PM-3659 - WebAuthnController.cs - Passkey Creation - Add RequireSSO login policy validation to prevent users from creating passkeys if require SSO applies to them. * PM-3659 - per PR feedback, apply new require SSO validation to options call * PM-3659 - Remove unneeded comment * PM-3659 - Per PR feedback, add unit tests for new require SSO scenarios on both Post and Options endpoints on the WebAuthnController * Remove duplicated line * Remove extra whitespace
This commit is contained in:
parent
1fb5e49a05
commit
f5f64059c5
@ -5,6 +5,7 @@ using Bit.Api.Models.Response;
|
|||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
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.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
@ -22,15 +23,18 @@ public class WebAuthnController : Controller
|
|||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IWebAuthnCredentialRepository _credentialRepository;
|
private readonly IWebAuthnCredentialRepository _credentialRepository;
|
||||||
private readonly IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> _createOptionsDataProtector;
|
private readonly IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> _createOptionsDataProtector;
|
||||||
|
private readonly IPolicyService _policyService;
|
||||||
|
|
||||||
public WebAuthnController(
|
public WebAuthnController(
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
IWebAuthnCredentialRepository credentialRepository,
|
IWebAuthnCredentialRepository credentialRepository,
|
||||||
IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> createOptionsDataProtector)
|
IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable> createOptionsDataProtector,
|
||||||
|
IPolicyService policyService)
|
||||||
{
|
{
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_credentialRepository = credentialRepository;
|
_credentialRepository = credentialRepository;
|
||||||
_createOptionsDataProtector = createOptionsDataProtector;
|
_createOptionsDataProtector = createOptionsDataProtector;
|
||||||
|
_policyService = policyService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("")]
|
[HttpGet("")]
|
||||||
@ -46,6 +50,7 @@ public class WebAuthnController : Controller
|
|||||||
public async Task<WebAuthnCredentialCreateOptionsResponseModel> PostOptions([FromBody] SecretVerificationRequestModel model)
|
public async Task<WebAuthnCredentialCreateOptionsResponseModel> PostOptions([FromBody] SecretVerificationRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await VerifyUserAsync(model);
|
var user = await VerifyUserAsync(model);
|
||||||
|
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
||||||
var options = await _userService.StartWebAuthnLoginRegistrationAsync(user);
|
var options = await _userService.StartWebAuthnLoginRegistrationAsync(user);
|
||||||
|
|
||||||
var tokenable = new WebAuthnCredentialCreateOptionsTokenable(user, options);
|
var tokenable = new WebAuthnCredentialCreateOptionsTokenable(user, options);
|
||||||
@ -62,7 +67,9 @@ public class WebAuthnController : Controller
|
|||||||
public async Task Post([FromBody] WebAuthnCredentialRequestModel model)
|
public async Task Post([FromBody] WebAuthnCredentialRequestModel model)
|
||||||
{
|
{
|
||||||
var user = await GetUserAsync();
|
var user = await GetUserAsync();
|
||||||
|
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
||||||
var tokenable = _createOptionsDataProtector.Unprotect(model.Token);
|
var tokenable = _createOptionsDataProtector.Unprotect(model.Token);
|
||||||
|
|
||||||
if (!tokenable.TokenIsValid(user))
|
if (!tokenable.TokenIsValid(user))
|
||||||
{
|
{
|
||||||
throw new BadRequestException("The token associated with your request is expired. A valid token is required to continue.");
|
throw new BadRequestException("The token associated with your request is expired. A valid token is required to continue.");
|
||||||
@ -75,6 +82,16 @@ public class WebAuthnController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ValidateRequireSsoPolicyDisabledOrNotApplicable(Guid userId)
|
||||||
|
{
|
||||||
|
var requireSsoLogin = await _policyService.AnyPoliciesApplicableToUserAsync(userId, PolicyType.RequireSso);
|
||||||
|
|
||||||
|
if (requireSsoLogin)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Passkeys cannot be created for your account. SSO login is required.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/delete")]
|
[HttpPost("{id}/delete")]
|
||||||
public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model)
|
public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model)
|
||||||
{
|
{
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Api.Auth.Models.Request.Accounts;
|
|||||||
using Bit.Api.Auth.Models.Request.Webauthn;
|
using Bit.Api.Auth.Models.Request.Webauthn;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
@ -22,7 +23,7 @@ public class WebAuthnControllerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Get_UserNotFound_ThrowsUnauthorizedAccessException(SutProvider<WebAuthnController> sutProvider)
|
public async Task Get_UserNotFound_ThrowsUnauthorizedAccessException(SutProvider<WebAuthnController> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@ -35,7 +36,7 @@ public class WebAuthnControllerTests
|
|||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task PostOptions_UserNotFound_ThrowsUnauthorizedAccessException(SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
public async Task PostOptions_UserNotFound_ThrowsUnauthorizedAccessException(SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@ -59,10 +60,25 @@ public class WebAuthnControllerTests
|
|||||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task PostOptions_RequireSsoPolicyApplicable_ThrowsBadRequestException(
|
||||||
|
SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||||
|
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).ReturnsForAnyArgs(true);
|
||||||
|
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(true);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.PostOptions(requestModel));
|
||||||
|
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Post_UserNotFound_ThrowsUnauthorizedAccessException(WebAuthnCredentialRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
public async Task Post_UserNotFound_ThrowsUnauthorizedAccessException(WebAuthnCredentialRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@ -113,10 +129,36 @@ public class WebAuthnControllerTests
|
|||||||
// Nothing to assert since return is void
|
// Nothing to assert since return is void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task Post_RequireSsoPolicyApplicable_ThrowsBadRequestException(
|
||||||
|
WebAuthnCredentialRequestModel requestModel,
|
||||||
|
CredentialCreateOptions createOptions,
|
||||||
|
User user,
|
||||||
|
SutProvider<WebAuthnController> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.GetUserByPrincipalAsync(default)
|
||||||
|
.ReturnsForAnyArgs(user);
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.CompleteWebAuthLoginRegistrationAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>())
|
||||||
|
.Returns(true);
|
||||||
|
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||||
|
.Unprotect(requestModel.Token)
|
||||||
|
.Returns(token);
|
||||||
|
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(true);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.Post(requestModel));
|
||||||
|
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task Delete_UserNotFound_ThrowsUnauthorizedAccessException(Guid credentialId, SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
public async Task Delete_UserNotFound_ThrowsUnauthorizedAccessException(Guid credentialId, SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
Loading…
Reference in New Issue
Block a user