diff --git a/src/WebAuthn.Net.Storage.MySql/WebAuthn.Net.Storage.MySql.csproj b/src/WebAuthn.Net.Storage.MySql/WebAuthn.Net.Storage.MySql.csproj index bb4ab5a3..fafb23a3 100644 --- a/src/WebAuthn.Net.Storage.MySql/WebAuthn.Net.Storage.MySql.csproj +++ b/src/WebAuthn.Net.Storage.MySql/WebAuthn.Net.Storage.MySql.csproj @@ -32,6 +32,6 @@ - + diff --git a/src/WebAuthn.Net/Services/AuthenticationCeremony/Implementation/DefaultAuthenticationCeremonyService.cs b/src/WebAuthn.Net/Services/AuthenticationCeremony/Implementation/DefaultAuthenticationCeremonyService.cs index dd971f11..8479d6b7 100644 --- a/src/WebAuthn.Net/Services/AuthenticationCeremony/Implementation/DefaultAuthenticationCeremonyService.cs +++ b/src/WebAuthn.Net/Services/AuthenticationCeremony/Implementation/DefaultAuthenticationCeremonyService.cs @@ -234,7 +234,7 @@ public DefaultAuthenticationCeremonyService( protected IAuthenticationCeremonyCounters Counters { get; } /// - public async Task BeginCeremonyAsync( + public virtual async Task BeginCeremonyAsync( HttpContext httpContext, BeginAuthenticationCeremonyRequest request, CancellationToken cancellationToken) @@ -294,7 +294,7 @@ public async Task BeginCeremonyAsync( } /// - public async Task> CompleteCeremonyAsync( + public virtual async Task> CompleteCeremonyAsync( HttpContext httpContext, CompleteAuthenticationCeremonyRequest request, CancellationToken cancellationToken) diff --git a/src/WebAuthn.Net/Services/RegistrationCeremony/Implementation/DefaultRegistrationCeremonyService.cs b/src/WebAuthn.Net/Services/RegistrationCeremony/Implementation/DefaultRegistrationCeremonyService.cs index d94de201..32f164a6 100644 --- a/src/WebAuthn.Net/Services/RegistrationCeremony/Implementation/DefaultRegistrationCeremonyService.cs +++ b/src/WebAuthn.Net/Services/RegistrationCeremony/Implementation/DefaultRegistrationCeremonyService.cs @@ -18,8 +18,11 @@ using WebAuthn.Net.Models.Protocol.RegistrationCeremony.CreateCredential; using WebAuthn.Net.Models.Protocol.RegistrationCeremony.CreateOptions; using WebAuthn.Net.Services.Common.AttestationObjectDecoder; +using WebAuthn.Net.Services.Common.AttestationObjectDecoder.Models; using WebAuthn.Net.Services.Common.AttestationStatementDecoder.Abstractions; +using WebAuthn.Net.Services.Common.AttestationStatementDecoder.Models; using WebAuthn.Net.Services.Common.AttestationStatementVerifier.Abstractions; +using WebAuthn.Net.Services.Common.AttestationStatementVerifier.Models.AttestationStatementVerifier; using WebAuthn.Net.Services.Common.AttestationStatementVerifier.Models.Enums; using WebAuthn.Net.Services.Common.AttestationTrustPathValidator; using WebAuthn.Net.Services.Common.AuthenticatorDataDecoder; @@ -263,12 +266,12 @@ public virtual async Task BeginCeremonyAsync( var expiresAt = GetExpiresAtUtc(createdAt, timeout); var options = CreatePublicKeyCredentialCreationOptions(request, timeout, rpId, challenge, credentialsToExclude); var outputOptions = PublicKeyCredentialCreationOptionsEncoder.Encode(options); - var registrationCeremony = new RegistrationCeremonyParameters( + var registrationCeremonyParameters = new RegistrationCeremonyParameters( options, expectedRpParameters, createdAt, expiresAt); - var ceremonyId = await CeremonyStorage.SaveAsync(context, registrationCeremony, cancellationToken); + var ceremonyId = await CeremonyStorage.SaveAsync(context, registrationCeremonyParameters, cancellationToken); await context.CommitAsync(cancellationToken); var result = new BeginRegistrationCeremonyResult(outputOptions, ceremonyId); Counters.IncrementBeginCeremonyEnd(true); @@ -276,7 +279,10 @@ public virtual async Task BeginCeremonyAsync( } /// - public async Task> CompleteCeremonyAsync(HttpContext httpContext, CompleteRegistrationCeremonyRequest request, CancellationToken cancellationToken) + public virtual async Task> CompleteCeremonyAsync( + HttpContext httpContext, + CompleteRegistrationCeremonyRequest request, + CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(httpContext); ArgumentNullException.ThrowIfNull(request); @@ -285,11 +291,11 @@ public async Task> CompleteCeremonyAs using (Logger.BeginCompleteRegistrationCeremonyScope(request.RegistrationCeremonyId)) await using (var context = await ContextFactory.CreateAsync(httpContext, cancellationToken)) { - var registrationCeremonyOptions = await CeremonyStorage.FindAsync( + var registrationCeremonyParameters = await CeremonyStorage.FindAsync( context, request.RegistrationCeremonyId, cancellationToken); - if (registrationCeremonyOptions is null) + if (registrationCeremonyParameters is null) { Logger.RegistrationCeremonyNotFound(); Counters.IncrementCompleteCeremonyEnd(false); @@ -298,7 +304,7 @@ public async Task> CompleteCeremonyAs // https://www.w3.org/TR/2023/WD-webauthn-3-20230927/#sctn-registering-a-new-credential // 1. Let 'options' be a new 'PublicKeyCredentialCreationOptions' structure configured to the Relying Party's needs for the ceremony. - var options = registrationCeremonyOptions.Options; + var options = registrationCeremonyParameters.Options; // 2. Call navigator.credentials.create() and pass 'options' as the 'publicKey' option. // Let 'credential' be the result of the successfully resolved promise. @@ -360,7 +366,7 @@ public async Task> CompleteCeremonyAs } // 9. Verify that the value of 'C.origin' is an origin expected by the Relying Party. See §13.4.9 Validating the origin of a credential for guidance. - var allowedOrigin = registrationCeremonyOptions.ExpectedRp.Origins.FirstOrDefault(x => string.Equals(x, C.Origin, StringComparison.Ordinal)); + var allowedOrigin = registrationCeremonyParameters.ExpectedRp.Origins.FirstOrDefault(x => string.Equals(x, C.Origin, StringComparison.Ordinal)); if (allowedOrigin is null) { Logger.InvalidOrigin(C.Origin); @@ -373,7 +379,7 @@ public async Task> CompleteCeremonyAs { // 1. Verify that the Relying Party expects that this credential would have been created within an iframe that is not same-origin with its ancestors. // 2. Verify that the value of C.topOrigin matches the origin of a page that the Relying Party expects to be sub-framed within. See §13.4.9 Validating the origin of a credential for guidance. - if (!registrationCeremonyOptions.ExpectedRp.AllowIframe) + if (!registrationCeremonyParameters.ExpectedRp.AllowIframe) { if (!string.Equals(allowedOrigin, C.TopOrigin, StringComparison.Ordinal)) { @@ -384,7 +390,7 @@ public async Task> CompleteCeremonyAs } else { - if (!registrationCeremonyOptions.ExpectedRp.TopOrigins.Any(x => string.Equals(x, C.TopOrigin, StringComparison.Ordinal))) + if (!registrationCeremonyParameters.ExpectedRp.TopOrigins.Any(x => string.Equals(x, C.TopOrigin, StringComparison.Ordinal))) { Logger.InvalidTopOrigin(C.TopOrigin); Counters.IncrementCompleteCeremonyEnd(false); @@ -446,7 +452,7 @@ public async Task> CompleteCeremonyAs // 13. Verify that the 'rpIdHash' in 'authData' is the SHA-256 hash of the 'RP ID' expected by the Relying Party. var authDataRpIdHash = authData.RpIdHash; - var expectedRpIdHash = SHA256.HashData(Encoding.UTF8.GetBytes(registrationCeremonyOptions.ExpectedRp.RpId)); + var expectedRpIdHash = SHA256.HashData(Encoding.UTF8.GetBytes(registrationCeremonyParameters.ExpectedRp.RpId)); if (!authDataRpIdHash.AsSpan().SequenceEqual(expectedRpIdHash.AsSpan())) { Logger.RpIdHashMismatch(); @@ -590,11 +596,16 @@ public async Task> CompleteCeremonyAs currentBe, currentBs, response); - var userCredentialRecord = new UserCredentialRecord( - options.User.Id, - registrationCeremonyOptions.ExpectedRp.RpId, - request.Description, - credentialRecord); + var userCredentialRecord = await CreateUserCredentialRecordAsync( + context, + registrationCeremonyParameters, + request, + credentialRecord, + attestationObjectResult.Ok, + authData, + attStmt, + attStmtVerification, + cancellationToken); var credentialIdNotRegisteredForAnyUser = await CredentialStorage.SaveIfNotRegisteredForOtherUserAsync( context, userCredentialRecord, @@ -749,7 +760,7 @@ protected virtual PublicKeyCredentialCreationOptions CreatePublicKeyCredentialCr } /// - /// Creates a , which is the final artifact of the registration ceremony. + /// Creates a that stores the properties of the registered public key. /// /// PublicKeyCredential. The response received from the authenticator during the registration ceremony. /// Authenticator Data (which has attestedCredentialData). @@ -838,6 +849,55 @@ protected virtual CredentialRecord CreateCredentialRecord( response.ClientDataJson); return credentialRecord; } + + /// + /// Creates a , which is the final artifact of the registration ceremony. + /// + /// The context in which the WebAuthn operation is performed. + /// Registration ceremony parameters. + /// Request containing parameters for completing the registration ceremony. + /// that stores the properties of the registered public key. + /// Decoded attestation object. + /// Decoded value of authenticator data (authData). + /// Decoded value of attestation statement (attStmt). + /// Verified value of the attestation statement (attStmt). + /// Cancellation token for an asynchronous operation. + /// Instance of . + /// Any of the parameters is + /// + /// This method is mostly made to allow the override of any properties of the resulting before it is saved to the database. + /// For example, you can set the description of the registering public key depending on the type of attestation. + /// The asynchronous signature of this method is made for flexibility. + /// The saving of the itself is performed in the next step. + /// Please don't save it to the database in this method. + /// + protected virtual Task CreateUserCredentialRecordAsync( + TContext context, + RegistrationCeremonyParameters registrationCeremonyParameters, + CompleteRegistrationCeremonyRequest request, + CredentialRecord credentialRecord, + AttestationObject attestationObject, + AttestedAuthenticatorData authData, + AbstractAttestationStatement attStmt, + VerifiedAttestationStatement verifiedAttestationStatement, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(registrationCeremonyParameters); + ArgumentNullException.ThrowIfNull(request); + ArgumentNullException.ThrowIfNull(credentialRecord); + ArgumentNullException.ThrowIfNull(attestationObject); + ArgumentNullException.ThrowIfNull(authData); + ArgumentNullException.ThrowIfNull(attStmt); + ArgumentNullException.ThrowIfNull(verifiedAttestationStatement); + var result = new UserCredentialRecord( + registrationCeremonyParameters.Options.User.Id, + registrationCeremonyParameters.ExpectedRp.RpId, + request.Description, + credentialRecord); + return Task.FromResult(result); + } } /// diff --git a/src/WebAuthn.Net/Storage/RegistrationCeremony/IRegistrationCeremonyStorage.cs b/src/WebAuthn.Net/Storage/RegistrationCeremony/IRegistrationCeremonyStorage.cs index 3963cdef..3dadb445 100644 --- a/src/WebAuthn.Net/Storage/RegistrationCeremony/IRegistrationCeremonyStorage.cs +++ b/src/WebAuthn.Net/Storage/RegistrationCeremony/IRegistrationCeremonyStorage.cs @@ -16,12 +16,12 @@ public interface IRegistrationCeremonyStorage /// Saves the parameters of the specified registration ceremony and returns the unique identifier of the saved record. /// /// The context in which the WebAuthn operation is performed. - /// Registration ceremony parameters. + /// Registration ceremony parameters. /// Cancellation token for an asynchronous operation. /// Task SaveAsync( TContext context, - RegistrationCeremonyParameters registrationCeremony, + RegistrationCeremonyParameters registrationCeremonyParameters, CancellationToken cancellationToken); /// diff --git a/src/WebAuthn.Net/Storage/RegistrationCeremony/Implementation/DefaultCookieRegistrationCeremonyStorage.cs b/src/WebAuthn.Net/Storage/RegistrationCeremony/Implementation/DefaultCookieRegistrationCeremonyStorage.cs index 77fdf82b..aad46ded 100644 --- a/src/WebAuthn.Net/Storage/RegistrationCeremony/Implementation/DefaultCookieRegistrationCeremonyStorage.cs +++ b/src/WebAuthn.Net/Storage/RegistrationCeremony/Implementation/DefaultCookieRegistrationCeremonyStorage.cs @@ -69,14 +69,14 @@ public DefaultCookieRegistrationCeremonyStorage( /// public virtual Task SaveAsync( TContext context, - RegistrationCeremonyParameters registrationCeremony, + RegistrationCeremonyParameters registrationCeremonyParameters, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(context); cancellationToken.ThrowIfCancellationRequested(); var options = Options.CurrentValue; var id = Guid.NewGuid().ToString("N").ToLowerInvariant(); - var container = new RegistrationCeremonyParametersCookieContainer(id, registrationCeremony); + var container = new RegistrationCeremonyParametersCookieContainer(id, registrationCeremonyParameters); var jsonBytesResult = SafeJsonSerializer.SerializeToUtf8Bytes(container, options.SerializerOptions); if (jsonBytesResult.HasError) { diff --git a/tests/WebAuthn.Net.Tests.Unit/DSL/Fakes/Storage/FakeRegistrationCeremonyStorage.cs b/tests/WebAuthn.Net.Tests.Unit/DSL/Fakes/Storage/FakeRegistrationCeremonyStorage.cs index 3b3fbf2d..53cd77e4 100644 --- a/tests/WebAuthn.Net.Tests.Unit/DSL/Fakes/Storage/FakeRegistrationCeremonyStorage.cs +++ b/tests/WebAuthn.Net.Tests.Unit/DSL/Fakes/Storage/FakeRegistrationCeremonyStorage.cs @@ -15,14 +15,14 @@ public class FakeRegistrationCeremonyStorage : IRegistrationCeremonyStorage SaveAsync( FakeWebAuthnContext context, - RegistrationCeremonyParameters registrationCeremony, + RegistrationCeremonyParameters registrationCeremonyParameters, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var id = Guid.NewGuid().ToString("N"); lock (_locker) { - _registrationCeremonies[id] = registrationCeremony; + _registrationCeremonies[id] = registrationCeremonyParameters; } return Task.FromResult(id);