mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05:00
[PM-2032] Server endpoints to support authentication with a passkey (#3361)
* [PM-2032] feat: add assertion options tokenable * [PM-2032] feat: add request and response models * [PM-2032] feat: implement `assertion-options` identity endpoint * [PM-2032] feat: implement authentication with passkey * [PM-2032] chore: rename to `WebAuthnGrantValidator` * [PM-2032] fix: add missing subsitute * [PM-2032] feat: start adding builder * [PM-2032] feat: add support for KeyConnector * [PM-2032] feat: add first version of TDE * [PM-2032] chore: refactor WithSso * [PM-2023] feat: add support for TDE feature flag * [PM-2023] feat: add support for approving devices * [PM-2023] feat: add support for hasManageResetPasswordPermission * [PM-2032] feat: add support for hasAdminApproval * [PM-2032] chore: don't supply device if not necessary * [PM-2032] chore: clean up imports * [PM-2023] feat: extract interface * [PM-2023] chore: add clarifying comment * [PM-2023] feat: use new builder in production code * [PM-2032] feat: add support for PRF * [PM-2032] chore: clean-up todos * [PM-2023] chore: remove token which is no longer used * [PM-2032] chore: remove todo * [PM-2032] feat: improve assertion error handling * [PM-2032] fix: linting issues * [PM-2032] fix: revert changes to `launchSettings.json` * [PM-2023] chore: clean up assertion endpoint * [PM-2032] feat: bypass 2FA * [PM-2032] fix: rename prf option to singular * [PM-2032] fix: lint * [PM-2032] fix: typo * [PM-2032] chore: improve builder tests Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> * [PM-2032] chore: clarify why we don't require 2FA * [PM-2023] feat: move `identityProvider` constant to common class * [PM-2032] fix: lint * [PM-2023] fix: move `IdentityProvider` to core.Constants * [PM-2032] fix: missing import * [PM-2032] chore: refactor token timespan to use `TimeSpan` * [PM-2032] chore: make `StartWebAuthnLoginAssertion` sync * [PM-2032] chore: use `FromMinutes` * [PM-2032] fix: change to 17 minutes to cover webauthn assertion * [PM-2032] chore: do not use `async void` * [PM-2032] fix: comment saying wrong amount of minutes * [PM-2032] feat: put validator behind feature flag * [PM-2032] fix: lint --------- Co-authored-by: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com>
This commit is contained in:
@ -1,4 +1,5 @@
|
||||
using System.Text.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using AutoFixture;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Entities;
|
||||
@ -8,26 +9,29 @@ using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Fakes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Fido2NetLib;
|
||||
using Fido2NetLib.Objects;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReceivedExtensions;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
@ -188,7 +192,7 @@ public class UserServiceTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async void CompleteWebAuthLoginRegistrationAsync_ExceedsExistingCredentialsLimit_ReturnsFalse(SutProvider<UserService> sutProvider, User user, CredentialCreateOptions options, AuthenticatorAttestationRawResponse response, Generator<WebAuthnCredential> credentialGenerator)
|
||||
public async Task CompleteWebAuthLoginRegistrationAsync_ExceedsExistingCredentialsLimit_ReturnsFalse(SutProvider<UserService> sutProvider, User user, CredentialCreateOptions options, AuthenticatorAttestationRawResponse response, Generator<WebAuthnCredential> credentialGenerator)
|
||||
{
|
||||
// Arrange
|
||||
var existingCredentials = credentialGenerator.Take(5).ToList();
|
||||
@ -202,6 +206,92 @@ public class UserServiceTests
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().DidNotReceive();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteWebAuthLoginAssertionAsync_InvalidUserHandle_ThrowsBadRequestException(SutProvider<UserService> sutProvider, AssertionOptions options, AuthenticatorAssertionRawResponse response)
|
||||
{
|
||||
// Arrange
|
||||
response.Response.UserHandle = Encoding.UTF8.GetBytes("invalid-user-handle");
|
||||
|
||||
// Act
|
||||
var result = async () => await sutProvider.Sut.CompleteWebAuthLoginAssertionAsync(options, response);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteWebAuthLoginAssertionAsync_UserNotFound_ThrowsBadRequestException(SutProvider<UserService> sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response)
|
||||
{
|
||||
// Arrange
|
||||
response.Response.UserHandle = user.Id.ToByteArray();
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).ReturnsNull();
|
||||
|
||||
// Act
|
||||
var result = async () => await sutProvider.Sut.CompleteWebAuthLoginAssertionAsync(options, response);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteWebAuthLoginAssertionAsync_NoMatchingCredentialExists_ThrowsBadRequestException(SutProvider<UserService> sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response)
|
||||
{
|
||||
// Arrange
|
||||
response.Response.UserHandle = user.Id.ToByteArray();
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { });
|
||||
|
||||
// Act
|
||||
var result = async () => await sutProvider.Sut.CompleteWebAuthLoginAssertionAsync(options, response);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteWebAuthLoginAssertionAsync_AssertionFails_ThrowsBadRequestException(SutProvider<UserService> sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response, WebAuthnCredential credential, AssertionVerificationResult assertionResult)
|
||||
{
|
||||
// Arrange
|
||||
var credentialId = Guid.NewGuid().ToByteArray();
|
||||
credential.CredentialId = CoreHelpers.Base64UrlEncode(credentialId);
|
||||
response.Id = credentialId;
|
||||
response.Response.UserHandle = user.Id.ToByteArray();
|
||||
assertionResult.Status = "Not ok";
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { credential });
|
||||
sutProvider.GetDependency<IFido2>().MakeAssertionAsync(response, options, Arg.Any<byte[]>(), Arg.Any<uint>(), Arg.Any<IsUserHandleOwnerOfCredentialIdAsync>())
|
||||
.Returns(assertionResult);
|
||||
|
||||
// Act
|
||||
var result = async () => await sutProvider.Sut.CompleteWebAuthLoginAssertionAsync(options, response);
|
||||
|
||||
// Assert
|
||||
await Assert.ThrowsAsync<BadRequestException>(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CompleteWebAuthLoginAssertionAsync_AssertionSucceeds_ReturnsUserAndCredential(SutProvider<UserService> sutProvider, User user, AssertionOptions options, AuthenticatorAssertionRawResponse response, WebAuthnCredential credential, AssertionVerificationResult assertionResult)
|
||||
{
|
||||
// Arrange
|
||||
var credentialId = Guid.NewGuid().ToByteArray();
|
||||
credential.CredentialId = CoreHelpers.Base64UrlEncode(credentialId);
|
||||
response.Id = credentialId;
|
||||
response.Response.UserHandle = user.Id.ToByteArray();
|
||||
assertionResult.Status = "ok";
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>().GetManyByUserIdAsync(user.Id).Returns(new WebAuthnCredential[] { credential });
|
||||
sutProvider.GetDependency<IFido2>().MakeAssertionAsync(response, options, Arg.Any<byte[]>(), Arg.Any<uint>(), Arg.Any<IsUserHandleOwnerOfCredentialIdAsync>())
|
||||
.Returns(assertionResult);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.CompleteWebAuthLoginAssertionAsync(options, response);
|
||||
|
||||
// Assert
|
||||
var (userResult, credentialResult) = result;
|
||||
Assert.Equal(user, userResult);
|
||||
Assert.Equal(credential, credentialResult);
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum ShouldCheck
|
||||
{
|
||||
@ -278,8 +368,7 @@ public class UserServiceTests
|
||||
sutProvider.GetDependency<IProviderUserRepository>(),
|
||||
sutProvider.GetDependency<IStripeSyncService>(),
|
||||
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>(),
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnLoginTokenable>>()
|
||||
sutProvider.GetDependency<IWebAuthnCredentialRepository>()
|
||||
);
|
||||
|
||||
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
|
||||
|
Reference in New Issue
Block a user