From f5f64059c5d5ef19e38f27cba7d0dad0fa36a706 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 1 Nov 2023 13:39:00 -0400 Subject: [PATCH] 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 --- .../Auth/Controllers/WebAuthnController.cs | 19 ++++++- .../Controllers/WebAuthnControllerTests.cs | 50 +++++++++++++++++-- 2 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/Api/Auth/Controllers/WebAuthnController.cs b/src/Api/Auth/Controllers/WebAuthnController.cs index b7e9c5bb8..908915662 100644 --- a/src/Api/Auth/Controllers/WebAuthnController.cs +++ b/src/Api/Auth/Controllers/WebAuthnController.cs @@ -5,6 +5,7 @@ using Bit.Api.Models.Response; using Bit.Core; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Tokens; @@ -22,15 +23,18 @@ public class WebAuthnController : Controller private readonly IUserService _userService; private readonly IWebAuthnCredentialRepository _credentialRepository; private readonly IDataProtectorTokenFactory _createOptionsDataProtector; + private readonly IPolicyService _policyService; public WebAuthnController( IUserService userService, IWebAuthnCredentialRepository credentialRepository, - IDataProtectorTokenFactory createOptionsDataProtector) + IDataProtectorTokenFactory createOptionsDataProtector, + IPolicyService policyService) { _userService = userService; _credentialRepository = credentialRepository; _createOptionsDataProtector = createOptionsDataProtector; + _policyService = policyService; } [HttpGet("")] @@ -46,6 +50,7 @@ public class WebAuthnController : Controller public async Task PostOptions([FromBody] SecretVerificationRequestModel model) { var user = await VerifyUserAsync(model); + await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id); var options = await _userService.StartWebAuthnLoginRegistrationAsync(user); var tokenable = new WebAuthnCredentialCreateOptionsTokenable(user, options); @@ -62,7 +67,9 @@ public class WebAuthnController : Controller public async Task Post([FromBody] WebAuthnCredentialRequestModel model) { var user = await GetUserAsync(); + await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id); var tokenable = _createOptionsDataProtector.Unprotect(model.Token); + if (!tokenable.TokenIsValid(user)) { 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")] public async Task Delete(Guid id, [FromBody] SecretVerificationRequestModel model) { diff --git a/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs b/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs index 32f2d5d49..dd5ffb15f 100644 --- a/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs @@ -3,6 +3,7 @@ using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Request.Webauthn; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Services; using Bit.Core.Tokens; @@ -22,7 +23,7 @@ public class WebAuthnControllerTests [Theory, BitAutoData] public async Task Get_UserNotFound_ThrowsUnauthorizedAccessException(SutProvider sutProvider) { - // Arrange + // Arrange sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs(); // Act @@ -35,7 +36,7 @@ public class WebAuthnControllerTests [Theory, BitAutoData] public async Task PostOptions_UserNotFound_ThrowsUnauthorizedAccessException(SecretVerificationRequestModel requestModel, SutProvider sutProvider) { - // Arrange + // Arrange sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs(); // Act @@ -59,10 +60,25 @@ public class WebAuthnControllerTests await Assert.ThrowsAsync(result); } + [Theory, BitAutoData] + public async Task PostOptions_RequireSsoPolicyApplicable_ThrowsBadRequestException( + SecretVerificationRequestModel requestModel, User user, SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user); + sutProvider.GetDependency().VerifySecretAsync(user, default).ReturnsForAnyArgs(true); + sutProvider.GetDependency().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PostOptions(requestModel)); + Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message); + } + [Theory, BitAutoData] public async Task Post_UserNotFound_ThrowsUnauthorizedAccessException(WebAuthnCredentialRequestModel requestModel, SutProvider sutProvider) { - // Arrange + // Arrange sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs(); // Act @@ -113,10 +129,36 @@ public class WebAuthnControllerTests // Nothing to assert since return is void } + [Theory, BitAutoData] + public async Task Post_RequireSsoPolicyApplicable_ThrowsBadRequestException( + WebAuthnCredentialRequestModel requestModel, + CredentialCreateOptions createOptions, + User user, + SutProvider sutProvider) + { + // Arrange + var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions); + sutProvider.GetDependency() + .GetUserByPrincipalAsync(default) + .ReturnsForAnyArgs(user); + sutProvider.GetDependency() + .CompleteWebAuthLoginRegistrationAsync(user, requestModel.Name, createOptions, Arg.Any()) + .Returns(true); + sutProvider.GetDependency>() + .Unprotect(requestModel.Token) + .Returns(token); + sutProvider.GetDependency().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(true); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.Post(requestModel)); + Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message); + } + [Theory, BitAutoData] public async Task Delete_UserNotFound_ThrowsUnauthorizedAccessException(Guid credentialId, SecretVerificationRequestModel requestModel, SutProvider sutProvider) { - // Arrange + // Arrange sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs(); // Act