mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-21 11:25:56 +01:00
[PM-5731] feat: implement fido2 client createCredential
This commit is contained in:
parent
2075f8168c
commit
b8c5ef501a
@ -15,7 +15,7 @@ namespace Bit.Core.Services
|
||||
|
||||
public async Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams)
|
||||
{
|
||||
if (makeCredentialParams.CredTypesAndPubKeyAlgs.All((p) => p.Algorithm != (int) Fido2AlgorithmIdentifier.ES256))
|
||||
if (makeCredentialParams.CredTypesAndPubKeyAlgs.All((p) => p.Alg != (int) Fido2AlgorithmIdentifier.ES256))
|
||||
{
|
||||
// var requestedAlgorithms = string.Join(", ", makeCredentialParams.CredTypesAndPubKeyAlgs.Select((p) => p.Algorithm).ToArray());
|
||||
// _logService.Warning(
|
||||
|
@ -1,17 +1,62 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Services
|
||||
{
|
||||
public class Fido2ClientService : IFido2ClientService
|
||||
{
|
||||
public Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams)
|
||||
private readonly IStateService _stateService;
|
||||
private readonly IEnvironmentService _environmentService;
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
private readonly IFido2AuthenticatorService _fido2AuthenticatorService;
|
||||
|
||||
public Fido2ClientService(
|
||||
IStateService stateService,
|
||||
IEnvironmentService environmentService,
|
||||
ICryptoFunctionService cryptoFunctionService,
|
||||
IFido2AuthenticatorService fido2AuthenticatorService
|
||||
)
|
||||
{
|
||||
_stateService = stateService;
|
||||
_environmentService = environmentService;
|
||||
_cryptoFunctionService = cryptoFunctionService;
|
||||
_fido2AuthenticatorService = fido2AuthenticatorService;
|
||||
}
|
||||
|
||||
public async Task<Fido2ClientCreateCredentialResult> CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams)
|
||||
{
|
||||
var blockedUris = await _stateService.GetAutofillBlacklistedUrisAsync();
|
||||
var domain = CoreHelpers.GetHostname(createCredentialParams.Origin);
|
||||
if (blockedUris.Contains(domain))
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.UriBlockedError,
|
||||
"Origin is blocked by the user");
|
||||
}
|
||||
|
||||
if (!await _stateService.IsAuthenticatedAsync())
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.InvalidStateError,
|
||||
"No user is logged in");
|
||||
}
|
||||
|
||||
if (createCredentialParams.Origin == _environmentService.GetWebVaultUrl())
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.NotAllowedError,
|
||||
"Saving Bitwarden credentials in a Bitwarden vault is not allowed");
|
||||
}
|
||||
|
||||
if (!createCredentialParams.SameOriginWithAncestors)
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.NotAllowedError,
|
||||
"Credential creation is now allowed from embedded contexts with different origins.");
|
||||
"Credential creation is now allowed from embedded contexts with different origins");
|
||||
}
|
||||
|
||||
if (createCredentialParams.User.Id.Length < 1 || createCredentialParams.User.Id.Length > 64)
|
||||
@ -19,12 +64,101 @@ namespace Bit.Core.Services
|
||||
// TODO: Should we use ArgumentException here instead?
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.TypeError,
|
||||
"The length of user.id is not between 1 and 64 bytes (inclusive).");
|
||||
"The length of user.id is not between 1 and 64 bytes (inclusive)");
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
if (!createCredentialParams.Origin.StartsWith("https://"))
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.SecurityError,
|
||||
"Origin is not a valid https origin");
|
||||
}
|
||||
|
||||
if (!Fido2DomainUtils.IsValidRpId(createCredentialParams.Rp.Id, createCredentialParams.Origin))
|
||||
{
|
||||
throw new Fido2ClientException(
|
||||
Fido2ClientException.ErrorCode.SecurityError,
|
||||
"RP ID cannot be used with this origin");
|
||||
}
|
||||
|
||||
PublicKeyCredentialParameters[] credTypesAndPubKeyAlgs;
|
||||
if (createCredentialParams.PubKeyCredParams?.Length > 0)
|
||||
{
|
||||
// Filter out all unsupported algorithms
|
||||
credTypesAndPubKeyAlgs = createCredentialParams.PubKeyCredParams
|
||||
.Where(kp => kp.Alg == -7 && kp.Type == "public-key")
|
||||
.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assign default algorithms
|
||||
credTypesAndPubKeyAlgs = [
|
||||
new PublicKeyCredentialParameters { Alg = -7, Type = "public-key" },
|
||||
new PublicKeyCredentialParameters { Alg = -257, Type = "public-key" }
|
||||
];
|
||||
}
|
||||
|
||||
if (credTypesAndPubKeyAlgs.Length == 0)
|
||||
{
|
||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.NotSupportedError, "No supported algorithms found");
|
||||
}
|
||||
|
||||
var clientDataJSON = JsonSerializer.Serialize(new {
|
||||
type = "webauthn.create",
|
||||
challenge = CoreHelpers.Base64UrlEncode(createCredentialParams.Challenge),
|
||||
origin = createCredentialParams.Origin,
|
||||
crossOrigin = !createCredentialParams.SameOriginWithAncestors,
|
||||
// tokenBinding: {} // Not supported
|
||||
});
|
||||
var clientDataJSONBytes = Encoding.UTF8.GetBytes(clientDataJSON);
|
||||
var clientDataHash = await _cryptoFunctionService.HashAsync(clientDataJSONBytes, CryptoHashAlgorithm.Sha256);
|
||||
var makeCredentialParams = MapToMakeCredentialParams(createCredentialParams, credTypesAndPubKeyAlgs, clientDataHash);
|
||||
|
||||
try {
|
||||
var makeCredentialResult = await _fido2AuthenticatorService.MakeCredentialAsync(makeCredentialParams);
|
||||
|
||||
return new Fido2ClientCreateCredentialResult {
|
||||
CredentialId = makeCredentialResult.CredentialId,
|
||||
AttestationObject = makeCredentialResult.AttestationObject,
|
||||
AuthData = makeCredentialResult.AuthData,
|
||||
ClientDataJSON = clientDataJSONBytes,
|
||||
PublicKey = makeCredentialResult.PublicKey,
|
||||
PublicKeyAlgorithm = makeCredentialResult.PublicKeyAlgorithm,
|
||||
Transports = createCredentialParams.Rp.Id == "google.com" ? ["internal", "usb"] : ["internal"] // workaround for a bug on Google's side
|
||||
};
|
||||
} catch (InvalidStateError) {
|
||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.InvalidStateError, "Unknown invalid state encountered");
|
||||
} catch (Exception) {
|
||||
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
|
||||
}
|
||||
}
|
||||
|
||||
public Task<Fido2ClientAssertCredentialResult> AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams) => throw new NotImplementedException();
|
||||
|
||||
private Fido2AuthenticatorMakeCredentialParams MapToMakeCredentialParams(
|
||||
Fido2ClientCreateCredentialParams createCredentialParams,
|
||||
PublicKeyCredentialParameters[] credTypesAndPubKeyAlgs,
|
||||
byte[] clientDataHash)
|
||||
{
|
||||
var requireResidentKey = createCredentialParams.AuthenticatorSelection?.ResidentKey == "required" ||
|
||||
createCredentialParams.AuthenticatorSelection?.ResidentKey == "preferred" ||
|
||||
(createCredentialParams.AuthenticatorSelection?.ResidentKey == null &&
|
||||
createCredentialParams.AuthenticatorSelection?.RequireResidentKey == true);
|
||||
|
||||
var requireUserVerification = createCredentialParams.AuthenticatorSelection?.UserVerification == "required" ||
|
||||
createCredentialParams.AuthenticatorSelection?.UserVerification == "preferred" ||
|
||||
createCredentialParams.AuthenticatorSelection?.UserVerification == null;
|
||||
|
||||
return new Fido2AuthenticatorMakeCredentialParams {
|
||||
RequireResidentKey = requireResidentKey,
|
||||
RequireUserVerification = requireUserVerification,
|
||||
ExcludeCredentialDescriptorList = createCredentialParams.ExcludeCredentials,
|
||||
CredTypesAndPubKeyAlgs = credTypesAndPubKeyAlgs,
|
||||
Hash = clientDataHash,
|
||||
RpEntity = createCredentialParams.Rp,
|
||||
UserEntity = createCredentialParams.User,
|
||||
Extensions = createCredentialParams.Extensions
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ namespace Bit.Core.Utilities
|
||||
|
||||
/// <summary>
|
||||
/// Returns the host (and not port) of the given uri.
|
||||
/// Does not support plain hostnames/only top level domain.
|
||||
/// Does not support plain hostnames without a protocol.
|
||||
///
|
||||
/// Input => Output examples:
|
||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||
@ -59,7 +59,7 @@ namespace Bit.Core.Utilities
|
||||
|
||||
/// <summary>
|
||||
/// Returns the host and port of the given uri.
|
||||
/// Does not support plain hostnames/only top level domain.
|
||||
/// Does not support plain hostnames without
|
||||
///
|
||||
/// Input => Output examples:
|
||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||
@ -89,7 +89,7 @@ namespace Bit.Core.Utilities
|
||||
|
||||
/// <summary>
|
||||
/// Returns the second and top level domain of the given uri.
|
||||
/// Does not support plain hostnames/only top level domain.
|
||||
/// Does not support plain hostnames without
|
||||
///
|
||||
/// Input => Output examples:
|
||||
/// <para>https://bitwarden.com => bitwarden.com</para>
|
||||
|
@ -21,6 +21,13 @@ namespace Bit.Core.Utilities.Fido2
|
||||
}
|
||||
}
|
||||
|
||||
public class InvalidStateError : Fido2AuthenticatorException
|
||||
{
|
||||
public InvalidStateError() : base("InvalidStateError")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class UnknownError : Fido2AuthenticatorException
|
||||
{
|
||||
public UnknownError() : base("UnknownError")
|
||||
|
@ -20,7 +20,7 @@ namespace Bit.Core.Utilities.Fido2
|
||||
/// <summary>
|
||||
/// A sequence of pairs of PublicKeyCredentialType and public key algorithms (COSEAlgorithmIdentifier) requested by the Relying Party. This sequence is ordered from most preferred to least preferred. The authenticator makes a best-effort to create the most preferred credential that it can.
|
||||
/// </summary>
|
||||
public PublicKeyCredentialAlgorithmDescriptor[] CredTypesAndPubKeyAlgs { get; set; }
|
||||
public PublicKeyCredentialParameters[] CredTypesAndPubKeyAlgs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
///An OPTIONAL list of PublicKeyCredentialDescriptor objects provided by the Relying Party with the intention that, if any of these are known to the authenticator, it SHOULD NOT create a new credential. excludeCredentialDescriptorList contains a list of known credentials.
|
||||
|
@ -39,7 +39,7 @@ namespace Bit.Core.Utilities.Fido2
|
||||
/// the same account on a single authenticator. The client is requested to return an error if the new credential would
|
||||
/// be created on an authenticator that also contains one of the credentials enumerated in this parameter.
|
||||
/// </summary>
|
||||
public List<PublicKeyCredentialDescriptor>? ExcludeCredentials { get; set; }
|
||||
public PublicKeyCredentialDescriptor[]? ExcludeCredentials { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This member contains additional parameters requesting additional processing by the client and authenticator.
|
||||
@ -52,7 +52,7 @@ namespace Bit.Core.Utilities.Fido2
|
||||
/// The sequence is ordered from most preferred to least preferred.
|
||||
/// The client makes a best-effort to create the most preferred credential that it can.
|
||||
/// </summary>
|
||||
public required List<PublicKeyCredentialParameters> PubKeyCredParams { get; set; }
|
||||
public required PublicKeyCredentialParameters[] PubKeyCredParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Data about the Relying Party responsible for the request.
|
||||
|
@ -7,6 +7,9 @@ namespace Bit.Core.Utilities.Fido2
|
||||
NotAllowedError,
|
||||
TypeError,
|
||||
SecurityError,
|
||||
UriBlockedError,
|
||||
NotSupportedError,
|
||||
InvalidStateError,
|
||||
UnknownError
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ using Bit.Core.Models.View;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
@ -49,9 +48,9 @@ namespace Bit.Core.Test.Services
|
||||
Name = "Bitwarden"
|
||||
},
|
||||
CredTypesAndPubKeyAlgs = [
|
||||
new PublicKeyCredentialAlgorithmDescriptor {
|
||||
new PublicKeyCredentialParameters {
|
||||
Type = "public-key",
|
||||
Algorithm = -7 // ES256
|
||||
Alg = -7 // ES256
|
||||
}
|
||||
],
|
||||
RequireResidentKey = false,
|
||||
@ -84,9 +83,9 @@ namespace Bit.Core.Test.Services
|
||||
{
|
||||
// Arrange
|
||||
_params.CredTypesAndPubKeyAlgs = [
|
||||
new PublicKeyCredentialAlgorithmDescriptor {
|
||||
new PublicKeyCredentialParameters {
|
||||
Type = "public-key",
|
||||
Algorithm = -257 // RS256 which we do not support
|
||||
Alg = -257 // RS256 which we do not support
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -1,8 +1,15 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services
|
||||
@ -36,6 +43,9 @@ namespace Bit.Core.Test.Services
|
||||
DisplayName = "User"
|
||||
}
|
||||
};
|
||||
|
||||
_sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns([]);
|
||||
_sutProvider.GetDependency<IStateService>().IsAuthenticatedAsync().Returns(true);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@ -46,10 +56,13 @@ namespace Bit.Core.Test.Services
|
||||
// Spec: If sameOriginWithAncestors is false, return a "NotAllowedError" DOMException.
|
||||
public async Task CreateCredentialAsync_ThrowsNotAllowedError_SameOriginWithAncestorsIsFalse()
|
||||
{
|
||||
// Arrange
|
||||
_params.SameOriginWithAncestors = false;
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code);
|
||||
}
|
||||
|
||||
@ -57,10 +70,13 @@ namespace Bit.Core.Test.Services
|
||||
// Spec: If the length of options.user.id is not between 1 and 64 bytes (inclusive) then return a TypeError.
|
||||
public async Task CreateCredentialAsync_ThrowsTypeError_UserIdIsTooSmall()
|
||||
{
|
||||
// Arrange
|
||||
_params.User.Id = RandomBytes(0);
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Fido2ClientException.ErrorCode.TypeError, exception.Code);
|
||||
}
|
||||
|
||||
@ -68,13 +84,216 @@ namespace Bit.Core.Test.Services
|
||||
// Spec: If the length of options.user.id is not between 1 and 64 bytes (inclusive) then return a TypeError.
|
||||
public async Task CreateCredentialAsync_ThrowsTypeError_UserIdIsTooLarge()
|
||||
{
|
||||
// Arrange
|
||||
_params.User.Id = RandomBytes(65);
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Fido2ClientException.ErrorCode.TypeError, exception.Code);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Not sure how to check this, or if it matters.")]
|
||||
// Spec: If callerOrigin is an opaque origin, return a DOMException whose name is "NotAllowedError", and terminate this algorithm.
|
||||
public Task CreateCredentialAsync_ThrowsNotAllowedError_UserIdIsTooLarge() => throw new NotImplementedException();
|
||||
|
||||
[Fact]
|
||||
// Spec: Let effectiveDomain be the callerOrigin’s effective domain. If effective domain is not a valid domain,
|
||||
// then return a DOMException whose name is "SecurityError" and terminate this algorithm.
|
||||
public async Task CreateCredentialAsync_ThrowsSecurityError_OriginIsNotValidDomain()
|
||||
{
|
||||
// Arrange
|
||||
_params.Origin = "invalid-domain-name";
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Fido2ClientException.ErrorCode.SecurityError, exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
// Spec: If options.rp.id is not a registrable domain suffix of and is not equal to effectiveDomain,
|
||||
// return a DOMException whose name is "SecurityError", and terminate this algorithm.
|
||||
public async Task CreateCredentialAsync_ThrowsSecurityError_RpIdIsNotValidForOrigin()
|
||||
{
|
||||
// Arrange
|
||||
_params.Origin = "https://passwordless.dev";
|
||||
_params.Rp.Id = "bitwarden.com";
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Fido2ClientException.ErrorCode.SecurityError, exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
// Spec: The origin's scheme must be https.
|
||||
public async Task CreateCredentialAsync_ThrowsSecurityError_OriginIsNotHttps()
|
||||
{
|
||||
// Arrange
|
||||
_params.Origin = "http://bitwarden.com";
|
||||
_params.Rp.Id = "bitwarden.com";
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Fido2ClientException.ErrorCode.SecurityError, exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
// Spec: If the origin's hostname is a blocked uri, then return UriBlockedError.
|
||||
public async Task CreateCredentialAsync_ThrowsUriBlockedError_OriginIsBlocked()
|
||||
{
|
||||
// Arrange
|
||||
_params.Origin = "https://sub.bitwarden.com";
|
||||
_sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns([
|
||||
"sub.bitwarden.com"
|
||||
]);
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Fido2ClientException.ErrorCode.UriBlockedError, exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
// Spec: If credTypesAndPubKeyAlgs is empty, return a DOMException whose name is "NotSupportedError", and terminate this algorithm.
|
||||
public async Task CreateCredentialAsync_ThrowsNotSupportedError_CredTypesAndPubKeyAlgsIsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
_params.PubKeyCredParams = [
|
||||
new PublicKeyCredentialParameters {
|
||||
Type = "not-supported",
|
||||
Alg = -7
|
||||
},
|
||||
new PublicKeyCredentialParameters {
|
||||
Type = "public-key",
|
||||
Alg = -9001
|
||||
}
|
||||
];
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Fido2ClientException.ErrorCode.NotSupportedError, exception.Code);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Not implemented")]
|
||||
// Spec: If the options.signal is present and its aborted flag is set to true, return a DOMException whose name is "AbortError" and terminate this algorithm.
|
||||
public Task CreateCredentialAsync_ThrowsAbortError_AbortedByCaller() => throw new NotImplementedException();
|
||||
|
||||
[Fact]
|
||||
public async Task CreateCredentialAsync_ReturnsNewCredential()
|
||||
{
|
||||
// Arrange
|
||||
_params.AuthenticatorSelection = new AuthenticatorSelectionCriteria {
|
||||
ResidentKey = "required",
|
||||
UserVerification = "required"
|
||||
};
|
||||
var authenticatorResult = new Fido2AuthenticatorMakeCredentialResult {
|
||||
CredentialId = RandomBytes(32),
|
||||
AttestationObject = RandomBytes(32),
|
||||
AuthData = RandomBytes(32),
|
||||
PublicKey = RandomBytes(32),
|
||||
PublicKeyAlgorithm = -7,
|
||||
};
|
||||
_sutProvider.GetDependency<IFido2AuthenticatorService>()
|
||||
.MakeCredentialAsync(Arg.Any<Fido2AuthenticatorMakeCredentialParams>())
|
||||
.Returns(authenticatorResult);
|
||||
|
||||
// Act
|
||||
var result = await _sutProvider.Sut.CreateCredentialAsync(_params);
|
||||
|
||||
// Assert
|
||||
await _sutProvider.GetDependency<IFido2AuthenticatorService>()
|
||||
.Received()
|
||||
.MakeCredentialAsync(Arg.Is<Fido2AuthenticatorMakeCredentialParams>(x =>
|
||||
x.RequireResidentKey == true &&
|
||||
x.RequireUserVerification == true &&
|
||||
x.RpEntity.Id == _params.Rp.Id &&
|
||||
x.UserEntity.DisplayName == _params.User.DisplayName
|
||||
));
|
||||
Assert.Equal(authenticatorResult.CredentialId, result.CredentialId);
|
||||
Assert.Equal(authenticatorResult.AttestationObject, result.AttestationObject);
|
||||
Assert.Equal(authenticatorResult.AuthData, result.AuthData);
|
||||
Assert.Equal(authenticatorResult.PublicKey, result.PublicKey);
|
||||
Assert.Equal(authenticatorResult.PublicKeyAlgorithm, result.PublicKeyAlgorithm);
|
||||
Assert.Equal(["internal"], result.Transports);
|
||||
|
||||
var clientDataJSON = JsonSerializer.Deserialize<JsonObject>(Encoding.UTF8.GetString(result.ClientDataJSON));
|
||||
Assert.Equal("webauthn.create", clientDataJSON["type"].GetValue<string>());
|
||||
Assert.Equal(CoreHelpers.Base64UrlEncode(_params.Challenge), clientDataJSON["challenge"].GetValue<string>());
|
||||
Assert.Equal(_params.Origin, clientDataJSON["origin"].GetValue<string>());
|
||||
Assert.Equal(!_params.SameOriginWithAncestors, clientDataJSON["crossOrigin"].GetValue<bool>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateCredentialAsync_ThrowsInvalidStateError_AuthenticatorThrowsInvalidStateError()
|
||||
{
|
||||
// Arrange
|
||||
_params.AuthenticatorSelection = new AuthenticatorSelectionCriteria {
|
||||
ResidentKey = "required",
|
||||
UserVerification = "required"
|
||||
};
|
||||
_sutProvider.GetDependency<IFido2AuthenticatorService>()
|
||||
.MakeCredentialAsync(Arg.Any<Fido2AuthenticatorMakeCredentialParams>())
|
||||
.Throws(new InvalidStateError());
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Fido2ClientException.ErrorCode.InvalidStateError, exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
// This keeps sensetive information form leaking
|
||||
public async Task CreateCredentialAsync_ThrowsUnknownError_AuthenticatorThrowsUnknownError()
|
||||
{
|
||||
// Arrange
|
||||
_sutProvider.GetDependency<IFido2AuthenticatorService>()
|
||||
.MakeCredentialAsync(Arg.Any<Fido2AuthenticatorMakeCredentialParams>())
|
||||
.Throws(new Exception("unknown error"));
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Fido2ClientException.ErrorCode.UnknownError, exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateCredentialAsync_ThrowsInvalidStateError_UserIsLoggedOut()
|
||||
{
|
||||
// Arrange
|
||||
_sutProvider.GetDependency<IStateService>().IsAuthenticatedAsync().Returns(false);
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Fido2ClientException.ErrorCode.InvalidStateError, exception.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateCredentialAsync_ThrowsNotAllowedError_OriginIsBitwardenVault()
|
||||
{
|
||||
// Arrange
|
||||
_params.Origin = "https://vault.bitwarden.com";
|
||||
_sutProvider.GetDependency<IEnvironmentService>().GetWebVaultUrl().Returns("https://vault.bitwarden.com");
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
|
||||
// Assert
|
||||
Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code);
|
||||
}
|
||||
|
||||
private byte[] RandomBytes(int length)
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user