diff --git a/src/Identity/Core/src/DefaultPasskeyHandler.cs b/src/Identity/Core/src/DefaultPasskeyHandler.cs
index 3843ea3463ec..609bd59afd8b 100644
--- a/src/Identity/Core/src/DefaultPasskeyHandler.cs
+++ b/src/Identity/Core/src/DefaultPasskeyHandler.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
@@ -13,23 +14,154 @@ namespace Microsoft.AspNetCore.Identity;
///
/// The default passkey handler.
///
-public partial class DefaultPasskeyHandler : IPasskeyHandler
+public sealed class DefaultPasskeyHandler : IPasskeyHandler
where TUser : class
{
- private readonly PasskeyOptions _passkeyOptions;
+ private readonly UserManager _userManager;
+ private readonly PasskeyOptions _options;
///
/// Constructs a new instance.
///
+ /// The .
/// The .
- public DefaultPasskeyHandler(IOptions options)
+ public DefaultPasskeyHandler(UserManager userManager, IOptions options)
{
- _passkeyOptions = options.Value.Passkey;
+ ArgumentNullException.ThrowIfNull(userManager);
+ ArgumentNullException.ThrowIfNull(options);
+
+ _userManager = userManager;
+ _options = options.Value;
+ }
+
+ ///
+ public async Task MakeCreationOptionsAsync(PasskeyUserEntity userEntity, HttpContext httpContext)
+ {
+ ArgumentNullException.ThrowIfNull(userEntity);
+ ArgumentNullException.ThrowIfNull(httpContext);
+
+ var excludeCredentials = await GetExcludeCredentialsAsync().ConfigureAwait(false);
+ var serverDomain = GetServerDomain(httpContext);
+ var challenge = RandomNumberGenerator.GetBytes(_options.ChallengeSize);
+ var pubKeyCredParams = _options.IsAllowedAlgorithm is { } isAllowedAlgorithm
+ ? [.. CredentialPublicKey.AllSupportedParameters.Where(p => isAllowedAlgorithm((int)p.Alg))]
+ : CredentialPublicKey.AllSupportedParameters;
+ var options = new PublicKeyCredentialCreationOptions
+ {
+ Rp = new()
+ {
+ Name = serverDomain,
+ Id = serverDomain,
+ },
+ User = new()
+ {
+ Id = BufferSource.FromString(userEntity.Id),
+ Name = userEntity.Name,
+ DisplayName = userEntity.DisplayName,
+ },
+ Challenge = BufferSource.FromBytes(challenge),
+ Timeout = (uint)_options.Timeout.TotalMilliseconds,
+ ExcludeCredentials = excludeCredentials,
+ PubKeyCredParams = pubKeyCredParams,
+ AuthenticatorSelection = new()
+ {
+ AuthenticatorAttachment = _options.AuthenticatorAttachment,
+ ResidentKey = _options.ResidentKeyRequirement,
+ UserVerification = _options.UserVerificationRequirement,
+ },
+ Attestation = _options.AttestationConveyancePreference,
+ };
+ var attestationState = new PasskeyAttestationState
+ {
+ Challenge = challenge,
+ UserEntity = userEntity,
+ };
+ var optionsJson = JsonSerializer.Serialize(options, IdentityJsonSerializerContext.Default.PublicKeyCredentialCreationOptions);
+ var attestationStateJson = JsonSerializer.Serialize(attestationState, IdentityJsonSerializerContext.Default.PasskeyAttestationState);
+ var creationOptions = new PasskeyCreationOptionsResult
+ {
+ CreationOptionsJson = optionsJson,
+ AttestationState = attestationStateJson,
+ };
+
+ return creationOptions;
+
+ async Task GetExcludeCredentialsAsync()
+ {
+ var existingUser = await _userManager.FindByIdAsync(userEntity.Id).ConfigureAwait(false);
+ if (existingUser is null)
+ {
+ return [];
+ }
+
+ var passkeys = await _userManager.GetPasskeysAsync(existingUser).ConfigureAwait(false);
+ var excludeCredentials = passkeys
+ .Select(p => new PublicKeyCredentialDescriptor
+ {
+ Type = "public-key",
+ Id = BufferSource.FromBytes(p.CredentialId),
+ Transports = p.Transports ?? [],
+ });
+ return [.. excludeCredentials];
+ }
+ }
+
+ ///
+ public async Task MakeRequestOptionsAsync(TUser? user, HttpContext httpContext)
+ {
+ ArgumentNullException.ThrowIfNull(httpContext);
+
+ var allowCredentials = await GetAllowCredentialsAsync().ConfigureAwait(false);
+ var serverDomain = _options.ServerDomain ?? httpContext.Request.Host.Host;
+ var challenge = RandomNumberGenerator.GetBytes(_options.ChallengeSize);
+ var options = new PublicKeyCredentialRequestOptions
+ {
+ Challenge = BufferSource.FromBytes(challenge),
+ RpId = serverDomain,
+ Timeout = (uint)_options.Timeout.TotalMilliseconds,
+ AllowCredentials = allowCredentials,
+ UserVerification = _options.UserVerificationRequirement,
+ };
+ var userId = user is not null ? await _userManager.GetUserIdAsync(user).ConfigureAwait(false) : null;
+ var assertionState = new PasskeyAssertionState
+ {
+ Challenge = challenge,
+ UserId = userId,
+ };
+ var optionsJson = JsonSerializer.Serialize(options, IdentityJsonSerializerContext.Default.PublicKeyCredentialRequestOptions);
+ var assertionStateJson = JsonSerializer.Serialize(assertionState, IdentityJsonSerializerContext.Default.PasskeyAssertionState);
+ var requestOptions = new PasskeyRequestOptionsResult
+ {
+ RequestOptionsJson = optionsJson,
+ AssertionState = assertionStateJson,
+ };
+
+ return requestOptions;
+
+ async Task GetAllowCredentialsAsync()
+ {
+ if (user is null)
+ {
+ return [];
+ }
+
+ var passkeys = await _userManager.GetPasskeysAsync(user).ConfigureAwait(false);
+ var allowCredentials = passkeys
+ .Select(p => new PublicKeyCredentialDescriptor
+ {
+ Type = "public-key",
+ Id = BufferSource.FromBytes(p.CredentialId),
+ Transports = p.Transports ?? [],
+ });
+ return [.. allowCredentials];
+ }
}
///
- public async Task PerformAttestationAsync(PasskeyAttestationContext context)
+ public async Task PerformAttestationAsync(PasskeyAttestationContext context)
{
+ ArgumentNullException.ThrowIfNull(context);
+
try
{
return await PerformAttestationCoreAsync(context).ConfigureAwait(false);
@@ -50,8 +182,10 @@ public async Task PerformAttestationAsync(PasskeyAttes
}
///
- public async Task> PerformAssertionAsync(PasskeyAssertionContext context)
+ public async Task> PerformAssertionAsync(PasskeyAssertionContext context)
{
+ ArgumentNullException.ThrowIfNull(context);
+
try
{
return await PerformAssertionCoreAsync(context).ConfigureAwait(false);
@@ -71,85 +205,19 @@ public async Task> PerformAssertionAsync(PasskeyAs
}
}
- ///
- /// Determines whether the specified origin is valid for passkey operations.
- ///
- /// Information about the passkey's origin.
- /// The HTTP context for the request.
- /// true if the origin is valid; otherwise, false.
- protected virtual Task IsValidOriginAsync(PasskeyOriginInfo originInfo, HttpContext httpContext)
- {
- var result = IsValidOrigin();
- return Task.FromResult(result);
-
- bool IsValidOrigin()
- {
- if (string.IsNullOrEmpty(originInfo.Origin))
- {
- return false;
- }
-
- if (originInfo.CrossOrigin && !_passkeyOptions.AllowCrossOriginIframes)
- {
- return false;
- }
-
- if (!Uri.TryCreate(originInfo.Origin, UriKind.Absolute, out var originUri))
- {
- return false;
- }
-
- if (_passkeyOptions.AllowedOrigins.Count > 0)
- {
- foreach (var allowedOrigin in _passkeyOptions.AllowedOrigins)
- {
- // Uri.Equals correctly handles string comparands.
- if (originUri.Equals(allowedOrigin))
- {
- return true;
- }
- }
- }
-
- if (_passkeyOptions.AllowCurrentOrigin && httpContext.Request.Headers.Origin is [var origin])
- {
- // Uri.Equals correctly handles string comparands.
- if (originUri.Equals(origin))
- {
- return true;
- }
- }
-
- return false;
- }
- }
-
- ///
- /// Verifies the attestation statement of a passkey.
- ///
- ///
- /// See .
- ///
- /// The attestation object to verify. See .
- /// The hash of the client data used during registration.
- /// The HTTP context for the request.
- /// A task that represents the asynchronous operation. The task result contains true if the verification is successful; otherwise, false.
- protected virtual Task VerifyAttestationStatementAsync(ReadOnlyMemory attestationObject, ReadOnlyMemory clientDataHash, HttpContext httpContext)
- => Task.FromResult(true);
-
///
/// Performs passkey attestation using the provided credential JSON and original options JSON.
///
/// The context containing necessary information for passkey attestation.
/// A task object representing the asynchronous operation containing the .
- protected virtual async Task PerformAttestationCoreAsync(PasskeyAttestationContext context)
+ private async Task PerformAttestationCoreAsync(PasskeyAttestationContext context)
{
// See: https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential
// NOTE: Quotes from the spec may have been modified.
// NOTE: Steps 1-3 are expected to have been performed prior to the execution of this method.
PublicKeyCredential credential;
- PublicKeyCredentialCreationOptions originalOptions;
+ PasskeyAttestationState attestationState;
try
{
@@ -163,12 +231,17 @@ protected virtual async Task PerformAttestationCoreAsy
try
{
- originalOptions = JsonSerializer.Deserialize(context.OriginalOptionsJson, IdentityJsonSerializerContext.Default.PublicKeyCredentialCreationOptions)
- ?? throw PasskeyException.NullOriginalCreationOptionsJson();
+ if (context.AttestationState is not { } attestationStateJson)
+ {
+ throw PasskeyException.NullAttestationStateJson();
+ }
+
+ attestationState = JsonSerializer.Deserialize(attestationStateJson, IdentityJsonSerializerContext.Default.PasskeyAttestationState)
+ ?? throw PasskeyException.NullAttestationStateJson();
}
catch (JsonException ex)
{
- throw PasskeyException.InvalidOriginalCreationOptionsJsonFormat(ex);
+ throw PasskeyException.InvalidAttestationStateJsonFormat(ex);
}
VerifyCredentialType(credential);
@@ -186,7 +259,7 @@ protected virtual async Task PerformAttestationCoreAsy
// 9-11. Verify that the value of C.origin matches the Relying Party's origin.
await VerifyClientDataAsync(
utf8Json: response.ClientDataJSON.AsMemory(),
- originalChallenge: originalOptions.Challenge.AsMemory(),
+ originalChallenge: attestationState.Challenge,
expectedType: "webauthn.create",
context.HttpContext)
.ConfigureAwait(false);
@@ -204,14 +277,13 @@ await VerifyClientDataAsync(
// 15. If options.mediation is not set to conditional, verify that the UP bit of the flags in authData is set.
// 16. If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.
// 17. If the BE bit of the flags in authData is not set, verify that the BS bit is not set.
- // 18. If the Relying Party uses the credential’s backup eligibility to inform its user experience flows and/or policies,
+ // 18. If the Relying Party uses the credential's backup eligibility to inform its user experience flows and/or policies,
// evaluate the BE bit of the flags in authData.
- // 19. If the Relying Party uses the credential’s backup state to inform its user experience flows and/or policies, evaluate the BS
+ // 19. If the Relying Party uses the credential's backup state to inform its user experience flows and/or policies, evaluate the BS
// bit of the flags in authData.
- VerifyAuthenticatorData(
- authenticatorData,
- originalRpId: originalOptions.Rp.Id,
- originalUserVerificationRequirement: originalOptions.AuthenticatorSelection?.UserVerification);
+ // NOTE: It's up to application code to evaluate BE and BS flags on the returned passkey and determine
+ // whether any action should be taken based on them.
+ VerifyAuthenticatorData(authenticatorData, context.HttpContext);
if (!authenticatorData.HasAttestedCredentialData)
{
@@ -220,15 +292,27 @@ await VerifyClientDataAsync(
// 20. Verify that the "alg" parameter in the credential public key in authData matches the alg attribute of one of the items in pkOptions.pubKeyCredParams.
var attestedCredentialData = authenticatorData.AttestedCredentialData;
- if (!originalOptions.PubKeyCredParams.Any(a => attestedCredentialData.CredentialPublicKey.Alg == a.Alg))
+ var algorithm = attestedCredentialData.CredentialPublicKey.Alg;
+ if (!CredentialPublicKey.IsSupportedAlgorithm(algorithm))
{
+ // The algorithm is not implemented.
+ throw PasskeyException.UnsupportedCredentialPublicKeyAlgorithm();
+ }
+ if (_options.IsAllowedAlgorithm is { } isAllowedAlgorithm && !isAllowedAlgorithm((int)algorithm))
+ {
+ // The algorithm is disallowed by the application.
throw PasskeyException.UnsupportedCredentialPublicKeyAlgorithm();
}
// 21-24. Determine the attestation statement format by performing a USASCII case-sensitive match on fmt against the set of supported WebAuthn
// Attestation Statement Format Identifier values...
// Handles all validation related to the attestation statement (21-24).
- var isAttestationStatementValid = await VerifyAttestationStatementAsync(attestationObjectMemory, clientDataHash, context.HttpContext).ConfigureAwait(false);
+ var isAttestationStatementValid = await _options.VerifyAttestationStatement(new()
+ {
+ HttpContext = context.HttpContext,
+ AttestationObject = attestationObjectMemory,
+ ClientDataHash = clientDataHash,
+ }).ConfigureAwait(false);
if (!isAttestationStatementValid)
{
throw PasskeyException.InvalidAttestationStatement();
@@ -244,7 +328,7 @@ await VerifyClientDataAsync(
var credentialId = attestedCredentialData.CredentialId.ToArray();
// 26. Verify that the credentialId is not yet registered for any user.
- var existingUser = await context.UserManager.FindByPasskeyIdAsync(credentialId).ConfigureAwait(false);
+ var existingUser = await _userManager.FindByPasskeyIdAsync(credentialId).ConfigureAwait(false);
if (existingUser is not null)
{
throw PasskeyException.CredentialAlreadyRegistered();
@@ -270,7 +354,7 @@ await VerifyClientDataAsync(
// 29. If all the above steps are successful, store credentialRecord in the user account that was denoted
// and continue the registration ceremony as appropriate.
- return PasskeyAttestationResult.Success(credentialRecord);
+ return PasskeyAttestationResult.Success(credentialRecord, attestationState.UserEntity);
}
///
@@ -278,14 +362,14 @@ await VerifyClientDataAsync(
///
/// The context containing necessary information for passkey assertion.
/// A task object representing the asynchronous operation containing the .
- protected virtual async Task> PerformAssertionCoreAsync(PasskeyAssertionContext context)
+ private async Task> PerformAssertionCoreAsync(PasskeyAssertionContext context)
{
// See https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion
// NOTE: Quotes from the spec may have been modified.
// NOTE: Steps 1-3 are expected to have been performed prior to the execution of this method.
PublicKeyCredential credential;
- PublicKeyCredentialRequestOptions originalOptions;
+ PasskeyAssertionState assertionState;
try
{
@@ -299,12 +383,25 @@ protected virtual async Task> PerformAssertionCore
try
{
- originalOptions = JsonSerializer.Deserialize(context.OriginalOptionsJson, IdentityJsonSerializerContext.Default.PublicKeyCredentialRequestOptions)
- ?? throw PasskeyException.NullOriginalRequestOptionsJson();
+ if (context.AssertionState is not { } assertionStateJson)
+ {
+ throw PasskeyException.NullAssertionStateJson();
+ }
+
+ assertionState = JsonSerializer.Deserialize(assertionStateJson, IdentityJsonSerializerContext.Default.PasskeyAssertionState)
+ ?? throw PasskeyException.NullAssertionStateJson();
}
catch (JsonException ex)
{
- throw PasskeyException.InvalidOriginalRequestOptionsJsonFormat(ex);
+ throw PasskeyException.InvalidAssertionStateJsonFormat(ex);
+ }
+
+ TUser? user = null;
+ var originalUserId = assertionState.UserId;
+ if (originalUserId is not null)
+ {
+ user = await _userManager.FindByIdAsync(originalUserId).ConfigureAwait(false)
+ ?? throw PasskeyException.CredentialDoesNotBelongToUser();
}
VerifyCredentialType(credential);
@@ -317,35 +414,32 @@ protected virtual async Task> PerformAssertionCore
// 5. If originalOptions.allowCredentials is not empty, verify that credential.id identifies one of the public key
// credentials listed in pkOptions.allowCredentials.
- if (originalOptions.AllowCredentials is { Count: > 0 } allowCredentials &&
- !originalOptions.AllowCredentials.Any(c => c.Id.Equals(credential.Id)))
- {
- throw PasskeyException.CredentialNotAllowed();
- }
+ // NOTE: Since we always include the user's full list of credentials in the options,
+ // we can simply check that the credential ID is present on the user.
+ // If we change this behavior, we may need to explicitly handle this step.
var credentialId = credential.Id.ToArray();
var userHandle = response.UserHandle?.ToString();
UserPasskeyInfo? storedPasskey;
// 6. Identify the user being authenticated and let credentialRecord be the credential record for the credential:
- if (context.User is { } user)
+ if (user is not null)
{
+ // The user should only be non-null if the user ID was provided in the properties.
+ Debug.Assert(originalUserId is not null);
+
// * If the user was identified before the authentication ceremony was initiated, e.g., via a username or cookie,
// verify that the identified user account contains a credential record whose id equals
// credential.rawId. Let credentialRecord be that credential record. If response.userHandle is
// present, verify that it equals the user handle of the user account.
- storedPasskey = await context.UserManager.GetPasskeyAsync(user, credentialId).ConfigureAwait(false);
+ storedPasskey = await _userManager.GetPasskeyAsync(user, credentialId).ConfigureAwait(false);
if (storedPasskey is null)
{
throw PasskeyException.CredentialDoesNotBelongToUser();
}
- if (userHandle is not null)
+ if (userHandle is not null && !string.Equals(originalUserId, userHandle, StringComparison.Ordinal))
{
- var userId = await context.UserManager.GetUserIdAsync(user).ConfigureAwait(false);
- if (!string.Equals(userHandle, userId, StringComparison.Ordinal))
- {
- throw PasskeyException.UserHandleMismatch(userId, userHandle);
- }
+ throw PasskeyException.UserHandleMismatch(originalUserId, userHandle);
}
}
else
@@ -359,19 +453,19 @@ protected virtual async Task> PerformAssertionCore
throw PasskeyException.MissingUserHandle();
}
- user = await context.UserManager.FindByIdAsync(userHandle).ConfigureAwait(false);
+ user = await _userManager.FindByIdAsync(userHandle).ConfigureAwait(false);
if (user is null)
{
throw PasskeyException.CredentialDoesNotBelongToUser();
}
- storedPasskey = await context.UserManager.GetPasskeyAsync(user, credentialId).ConfigureAwait(false);
+ storedPasskey = await _userManager.GetPasskeyAsync(user, credentialId).ConfigureAwait(false);
if (storedPasskey is null)
{
throw PasskeyException.CredentialDoesNotBelongToUser();
}
}
- // 7. Let cData, authData and sig denote the value of response’s clientDataJSON, authenticatorData, and signature respectively.
+ // 7. Let cData, authData and sig denote the value of response's clientDataJSON, authenticatorData, and signature respectively.
var authenticatorData = AuthenticatorData.Parse(response.AuthenticatorData.AsMemory());
// 8. Let JSONtext be the result of running UTF-8 decode on the value of cData.
@@ -381,7 +475,7 @@ protected virtual async Task> PerformAssertionCore
// 12-14. Verify that the value of C.origin is an origin expected by the Relying Party.
await VerifyClientDataAsync(
utf8Json: response.ClientDataJSON.AsMemory(),
- originalChallenge: originalOptions.Challenge.AsMemory(),
+ originalChallenge: assertionState.Challenge,
expectedType: "webauthn.get",
context.HttpContext)
.ConfigureAwait(false);
@@ -391,10 +485,7 @@ await VerifyClientDataAsync(
// 17. If user verification was determined to be required, verify that the UV bit of the flags in authData is set.
// Otherwise, ignore the value of the UV flag.
// 18. If the BE bit of the flags in authData is not set, verify that the BS bit is not set.
- VerifyAuthenticatorData(
- authenticatorData,
- originalRpId: originalOptions.RpId,
- originalUserVerificationRequirement: originalOptions.UserVerification);
+ VerifyAuthenticatorData(authenticatorData, context.HttpContext);
// 19. If the credential backup state is used as part of Relying Party business logic or policy, let currentBe and currentBs
// be the values of the BE and BS bits, respectively, of the flags in authData. Compare currentBe and currentBs with
@@ -402,7 +493,7 @@ await VerifyClientDataAsync(
// 1. If credentialRecord.backupEligible is set, verify that currentBe is set.
// 2. If credentialRecord.backupEligible is not set, verify that currentBe is not set.
// 3. Apply Relying Party policy, if any.
- // NOTE: RP policy applied in VerifyAuthenticatorData() above.
+ // NOTE: Additional RP policies should be handled by application code.
if (storedPasskey.IsBackupEligible && !authenticatorData.IsBackupEligible)
{
throw PasskeyException.ExpectedBackupEligibleCredential();
@@ -500,11 +591,14 @@ private async Task VerifyClientDataAsync(
}
// Verify that the value of C.origin is an origin expected by the Relying Party.
- // NOTE: The level 3 draft permits having multiple origins and validating the "top origin" when a cross-origin request is made.
- // For future-proofing, we pass a PasskeyOriginInfo to the origin validator so that we're able to add more properties to
- // it later.
- var originInfo = new PasskeyOriginInfo(clientData.Origin, clientData.CrossOrigin == true);
- var isOriginValid = await IsValidOriginAsync(originInfo, httpContext).ConfigureAwait(false);
+ var originInfo = new PasskeyOriginValidationContext
+ {
+ HttpContext = httpContext,
+ Origin = clientData.Origin,
+ CrossOrigin = clientData.CrossOrigin == true,
+ TopOrigin = clientData.TopOrigin,
+ };
+ var isOriginValid = await _options.ValidateOrigin(originInfo).ConfigureAwait(false);
if (!isOriginValid)
{
throw PasskeyException.InvalidOrigin(clientData.Origin);
@@ -526,10 +620,10 @@ private async Task VerifyClientDataAsync(
private void VerifyAuthenticatorData(
AuthenticatorData authenticatorData,
- string? originalRpId,
- string? originalUserVerificationRequirement)
+ HttpContext httpContext)
{
// Verify that the rpIdHash in authenticatorData is the SHA-256 hash of the RP ID expected by the Relying Party.
+ var originalRpId = GetServerDomain(httpContext);
var originalRpIdHash = SHA256.HashData(Encoding.UTF8.GetBytes(originalRpId ?? string.Empty));
if (!CryptographicOperations.FixedTimeEquals(authenticatorData.RpIdHash.Span, originalRpIdHash.AsSpan()))
{
@@ -545,6 +639,7 @@ private void VerifyAuthenticatorData(
}
// If user verification is required for this registration, verify that the User Verified bit of the flags in authData is set.
+ var originalUserVerificationRequirement = _options.UserVerificationRequirement;
if (string.Equals("required", originalUserVerificationRequirement, StringComparison.Ordinal) && !authenticatorData.IsUserVerified)
{
throw PasskeyException.UserNotVerified();
@@ -555,27 +650,8 @@ private void VerifyAuthenticatorData(
{
throw PasskeyException.NotBackupEligibleYetBackedUp();
}
-
- // If the Relying Party uses the credential’s backup eligibility to inform its user experience flows and/or policies,
- // evaluate the BE bit of the flags in authData.
- if (authenticatorData.IsBackupEligible && _passkeyOptions.BackupEligibleCredentialPolicy is PasskeyOptions.CredentialBackupPolicy.Disallowed)
- {
- throw PasskeyException.BackupEligibilityDisallowedYetBackupEligible();
- }
- if (!authenticatorData.IsBackupEligible && _passkeyOptions.BackupEligibleCredentialPolicy is PasskeyOptions.CredentialBackupPolicy.Required)
- {
- throw PasskeyException.BackupEligibilityRequiredYetNotBackupEligible();
- }
-
- // If the Relying Party uses the credential’s backup state to inform its user experience flows and/or policies, evaluate the BS
- // bit of the flags in authData.
- if (authenticatorData.IsBackedUp && _passkeyOptions.BackedUpCredentialPolicy is PasskeyOptions.CredentialBackupPolicy.Disallowed)
- {
- throw PasskeyException.BackupDisallowedYetBackedUp();
- }
- if (!authenticatorData.IsBackedUp && _passkeyOptions.BackedUpCredentialPolicy is PasskeyOptions.CredentialBackupPolicy.Required)
- {
- throw PasskeyException.BackupRequiredYetNotBackedUp();
- }
}
+
+ private string GetServerDomain(HttpContext httpContext)
+ => _options.ServerDomain ?? httpContext.Request.Host.Host;
}
diff --git a/src/Identity/Core/src/IPasskeyHandler.cs b/src/Identity/Core/src/IPasskeyHandler.cs
index be2a68a48d68..50898659edf0 100644
--- a/src/Identity/Core/src/IPasskeyHandler.cs
+++ b/src/Identity/Core/src/IPasskeyHandler.cs
@@ -1,25 +1,44 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.AspNetCore.Http;
+
namespace Microsoft.AspNetCore.Identity;
///
-/// Represents a handler for passkey assertion and attestation.
+/// Represents a handler for generating passkey creation and request options and performing
+/// passkey assertion and attestation.
///
public interface IPasskeyHandler
where TUser : class
{
///
- /// Performs passkey attestation using the provided credential JSON and original options JSON.
+ /// Generates passkey creation options for the specified user entity and HTTP context.
+ ///
+ /// The passkey user entity for which to generate creation options.
+ /// The HTTP context associated with the request.
+ /// A representing the result.
+ Task MakeCreationOptionsAsync(PasskeyUserEntity userEntity, HttpContext httpContext);
+
+ ///
+ /// Generates passkey request options for the specified user and HTTP context.
+ ///
+ /// The user for whom to generate request options.
+ /// The HTTP context associated with the request.
+ /// A representing the result.
+ Task MakeRequestOptionsAsync(TUser? user, HttpContext httpContext);
+
+ ///
+ /// Performs passkey attestation using the provided .
///
/// The context containing necessary information for passkey attestation.
- /// A task object representing the asynchronous operation containing the .
- Task PerformAttestationAsync(PasskeyAttestationContext context);
+ /// A representing the result.
+ Task PerformAttestationAsync(PasskeyAttestationContext context);
///
- /// Performs passkey assertion using the provided credential JSON, original options JSON, and optional user.
+ /// Performs passkey assertion using the provided .
///
/// The context containing necessary information for passkey assertion.
- /// A task object representing the asynchronous operation containing the .
- Task> PerformAssertionAsync(PasskeyAssertionContext context);
+ /// A representing the result.
+ Task> PerformAssertionAsync(PasskeyAssertionContext context);
}
diff --git a/src/Identity/Core/src/IdentityJsonSerializerContext.cs b/src/Identity/Core/src/IdentityJsonSerializerContext.cs
index 81ddf44b6acc..34b5843b8f89 100644
--- a/src/Identity/Core/src/IdentityJsonSerializerContext.cs
+++ b/src/Identity/Core/src/IdentityJsonSerializerContext.cs
@@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.Identity;
[JsonSerializable(typeof(PublicKeyCredentialRequestOptions))]
[JsonSerializable(typeof(PublicKeyCredential))]
[JsonSerializable(typeof(PublicKeyCredential))]
+[JsonSerializable(typeof(PasskeyAttestationState))]
+[JsonSerializable(typeof(PasskeyAssertionState))]
[JsonSourceGenerationOptions(
JsonSerializerDefaults.Web,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
diff --git a/src/Identity/Core/src/PasskeyAssertionContext.cs b/src/Identity/Core/src/PasskeyAssertionContext.cs
index 0c748f5e907c..c80a5c5394d6 100644
--- a/src/Identity/Core/src/PasskeyAssertionContext.cs
+++ b/src/Identity/Core/src/PasskeyAssertionContext.cs
@@ -8,14 +8,12 @@ namespace Microsoft.AspNetCore.Identity;
///
/// Represents the context for passkey assertion.
///
-/// The type of user associated with the passkey.
-public sealed class PasskeyAssertionContext
- where TUser : class
+public sealed class PasskeyAssertionContext
{
///
- /// Gets or sets the user associated with the passkey, if known.
+ /// Gets or sets the for the current request.
///
- public TUser? User { get; init; }
+ public required HttpContext HttpContext { get; init; }
///
/// Gets or sets the credentials obtained by JSON-serializing the result of the
@@ -24,17 +22,11 @@ public sealed class PasskeyAssertionContext
public required string CredentialJson { get; init; }
///
- /// Gets or sets the JSON representation of the original passkey creation options provided to the browser.
- ///
- public required string OriginalOptionsJson { get; init; }
-
- ///
- /// Gets or sets the to retrieve user information from.
+ /// Gets or sets the state to be used in the assertion procedure.
///
- public required UserManager UserManager { get; init; }
-
- ///
- /// Gets or sets the for the current request.
- ///
- public required HttpContext HttpContext { get; init; }
+ ///
+ /// This is expected to match the
+ /// previously returned from .
+ ///
+ public required string? AssertionState { get; init; }
}
diff --git a/src/Identity/Core/src/PasskeyAttestationContext.cs b/src/Identity/Core/src/PasskeyAttestationContext.cs
index 8ee14b31fa64..90e5687d2232 100644
--- a/src/Identity/Core/src/PasskeyAttestationContext.cs
+++ b/src/Identity/Core/src/PasskeyAttestationContext.cs
@@ -8,28 +8,25 @@ namespace Microsoft.AspNetCore.Identity;
///
/// Represents the context for passkey attestation.
///
-/// The type of user associated with the passkey.
-public sealed class PasskeyAttestationContext
- where TUser : class
+public sealed class PasskeyAttestationContext
{
///
- /// Gets or sets the credentials obtained by JSON-serializing the result of the
- /// navigator.credentials.create() JavaScript function.
- ///
- public required string CredentialJson { get; init; }
-
- ///
- /// Gets or sets the JSON representation of the original passkey creation options provided to the browser.
+ /// Gets or sets the for the current request.
///
- public required string OriginalOptionsJson { get; init; }
+ public required HttpContext HttpContext { get; init; }
///
- /// Gets or sets the to retrieve user information from.
+ /// Gets or sets the credentials obtained by JSON-serializing the result of the
+ /// navigator.credentials.create() JavaScript function.
///
- public required UserManager UserManager { get; init; }
+ public required string CredentialJson { get; init; }
///
- /// Gets or sets the for the current request.
+ /// Gets or sets the state to be used in the attestation procedure.
///
- public required HttpContext HttpContext { get; init; }
+ ///
+ /// This is expected to match the
+ /// previously returned from .
+ ///
+ public required string? AttestationState { get; init; }
}
diff --git a/src/Identity/Core/src/PasskeyAttestationResult.cs b/src/Identity/Core/src/PasskeyAttestationResult.cs
index 3034cb3d5c45..7d0966087512 100644
--- a/src/Identity/Core/src/PasskeyAttestationResult.cs
+++ b/src/Identity/Core/src/PasskeyAttestationResult.cs
@@ -14,6 +14,7 @@ public sealed class PasskeyAttestationResult
/// Gets whether the attestation was successful.
///
[MemberNotNullWhen(true, nameof(Passkey))]
+ [MemberNotNullWhen(true, nameof(UserEntity))]
[MemberNotNullWhen(false, nameof(Failure))]
public bool Succeeded { get; }
@@ -22,15 +23,21 @@ public sealed class PasskeyAttestationResult
///
public UserPasskeyInfo? Passkey { get; }
+ ///
+ /// Gets the user entity associated with the passkey when successful.
+ ///
+ public PasskeyUserEntity? UserEntity { get; }
+
///
/// Gets the error that occurred during attestation.
///
public PasskeyException? Failure { get; }
- private PasskeyAttestationResult(UserPasskeyInfo passkey)
+ private PasskeyAttestationResult(UserPasskeyInfo passkey, PasskeyUserEntity userEntity)
{
Succeeded = true;
Passkey = passkey;
+ UserEntity = userEntity;
}
private PasskeyAttestationResult(PasskeyException failure)
@@ -43,11 +50,12 @@ private PasskeyAttestationResult(PasskeyException failure)
/// Creates a successful result for a passkey attestation operation.
///
/// The passkey information associated with the attestation.
+ /// The user entity associated with the attestation.
/// A instance representing a successful attestation.
- public static PasskeyAttestationResult Success(UserPasskeyInfo passkey)
+ public static PasskeyAttestationResult Success(UserPasskeyInfo passkey, PasskeyUserEntity userEntity)
{
ArgumentNullException.ThrowIfNull(passkey);
- return new PasskeyAttestationResult(passkey);
+ return new PasskeyAttestationResult(passkey, userEntity);
}
///
diff --git a/src/Identity/Core/src/PasskeyAttestationStatementVerificationContext.cs b/src/Identity/Core/src/PasskeyAttestationStatementVerificationContext.cs
new file mode 100644
index 000000000000..b3035dccb6ad
--- /dev/null
+++ b/src/Identity/Core/src/PasskeyAttestationStatementVerificationContext.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Identity;
+
+///
+/// Contains the context for passkey attestation statement verification.
+///
+///
+/// See .
+///
+public readonly struct PasskeyAttestationStatementVerificationContext
+{
+ ///
+ /// Gets or sets the for the current request.
+ ///
+ public required HttpContext HttpContext { get; init; }
+
+ ///
+ /// Gets or sets the attestation object as a byte array.
+ ///
+ ///
+ /// See .
+ ///
+ public required ReadOnlyMemory AttestationObject { get; init; }
+
+ ///
+ /// Gets or sets the hash of the client data as a byte array.
+ ///
+ public required ReadOnlyMemory ClientDataHash { get; init; }
+}
diff --git a/src/Identity/Core/src/PasskeyCreationArgs.cs b/src/Identity/Core/src/PasskeyCreationArgs.cs
deleted file mode 100644
index 9db4f97ac269..000000000000
--- a/src/Identity/Core/src/PasskeyCreationArgs.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Text.Json;
-
-namespace Microsoft.AspNetCore.Identity;
-
-///
-/// Represents arguments for generating .
-///
-/// The passkey user entity.
-public sealed class PasskeyCreationArgs(PasskeyUserEntity userEntity)
-{
- ///
- /// Gets the passkey user entity.
- ///
- ///
- /// See .
- ///
- public PasskeyUserEntity UserEntity { get; } = userEntity;
-
- ///
- /// Gets or sets the authenticator selection criteria.
- ///
- ///
- /// See .
- ///
- public AuthenticatorSelectionCriteria? AuthenticatorSelection { get; set; }
-
- ///
- /// Gets or sets the attestation conveyance preference.
- ///
- ///
- /// See .
- /// The default value is "none".
- ///
- public string Attestation { get; set; } = "none";
-
- ///
- /// Gets or sets the client extension inputs.
- ///
- ///
- /// See .
- ///
- public JsonElement? Extensions { get; set; }
-}
diff --git a/src/Identity/Core/src/PasskeyCreationOptions.cs b/src/Identity/Core/src/PasskeyCreationOptions.cs
deleted file mode 100644
index f784b0afe461..000000000000
--- a/src/Identity/Core/src/PasskeyCreationOptions.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.AspNetCore.Identity;
-
-///
-/// Represents options for creating a passkey.
-///
-/// The user entity associated with the passkey.
-/// The JSON representation of the options.
-///
-/// See .
-///
-public sealed class PasskeyCreationOptions(PasskeyUserEntity userEntity, string optionsJson)
-{
- private readonly string _optionsJson = optionsJson;
-
- ///
- /// Gets the user entity associated with the passkey.
- ///
- ///
- /// See .
- /// >
- public PasskeyUserEntity UserEntity { get; } = userEntity;
-
- ///
- /// Gets the JSON representation of the options.
- ///
- ///
- /// The structure of the JSON string matches the description in the WebAuthn specification.
- /// See .
- ///
- public string AsJson()
- => _optionsJson;
-
- ///
- /// Gets the JSON representation of the options.
- ///
- ///
- /// The structure of the JSON string matches the description in the WebAuthn specification.
- /// See .
- ///
- public override string ToString()
- => _optionsJson;
-}
diff --git a/src/Identity/Core/src/PasskeyCreationOptionsResult.cs b/src/Identity/Core/src/PasskeyCreationOptionsResult.cs
new file mode 100644
index 000000000000..2c1565815744
--- /dev/null
+++ b/src/Identity/Core/src/PasskeyCreationOptionsResult.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Identity;
+
+///
+/// Represents the result of a passkey creation options generation.
+///
+public sealed class PasskeyCreationOptionsResult
+{
+ ///
+ /// Gets or sets the JSON representation of the creation options.
+ ///
+ ///
+ /// The structure of this JSON is compatible with
+ ///
+ /// and should be used with the navigator.credentials.create() JavaScript API.
+ ///
+ public required string CreationOptionsJson { get; init; }
+
+ ///
+ /// Gets or sets the state to be used in the attestation procedure.
+ ///
+ ///
+ /// This can be later retrieved during assertion with .
+ ///
+ public string? AttestationState { get; init; }
+}
diff --git a/src/Identity/Core/src/PasskeyExceptionExtensions.cs b/src/Identity/Core/src/PasskeyExceptionExtensions.cs
index 82fc60c92464..3caac0ba81cd 100644
--- a/src/Identity/Core/src/PasskeyExceptionExtensions.cs
+++ b/src/Identity/Core/src/PasskeyExceptionExtensions.cs
@@ -33,18 +33,6 @@ public static PasskeyException UserNotVerified()
public static PasskeyException NotBackupEligibleYetBackedUp()
=> new("The credential is backed up, but the authenticator data flags did not have the 'BackupEligible' flag.");
- public static PasskeyException BackupEligibilityDisallowedYetBackupEligible()
- => new("Credential backup eligibility is disallowed, but the credential was eligible for backup.");
-
- public static PasskeyException BackupEligibilityRequiredYetNotBackupEligible()
- => new("Credential backup eligibility is required, but the credential was not eligible for backup.");
-
- public static PasskeyException BackupDisallowedYetBackedUp()
- => new("Credential backup is disallowed, but the credential was backed up.");
-
- public static PasskeyException BackupRequiredYetNotBackedUp()
- => new("Credential backup is required, but the credential was not backed up.");
-
public static PasskeyException MissingAttestedCredentialData()
=> new("No attested credential data was provided by the authenticator.");
@@ -63,9 +51,6 @@ public static PasskeyException CredentialIdMismatch()
public static PasskeyException CredentialAlreadyRegistered()
=> new("The credential is already registered for a user.");
- public static PasskeyException CredentialNotAllowed()
- => new("The provided credential ID was not in the list of allowed credentials.");
-
public static PasskeyException CredentialDoesNotBelongToUser()
=> new("The provided credential does not belong to the specified user.");
@@ -123,24 +108,12 @@ public static PasskeyException NullAttestationCredentialJson()
public static PasskeyException InvalidAttestationCredentialJsonFormat(JsonException ex)
=> new($"The attestation credential JSON had an invalid format: {ex.Message}", ex);
- public static PasskeyException NullOriginalCreationOptionsJson()
- => new("The original passkey creation options were unexpectedly null.");
-
- public static PasskeyException InvalidOriginalCreationOptionsJsonFormat(JsonException ex)
- => new($"The original passkey creation options had an invalid format: {ex.Message}", ex);
-
public static PasskeyException NullAssertionCredentialJson()
=> new("The assertion credential JSON was unexpectedly null.");
public static PasskeyException InvalidAssertionCredentialJsonFormat(JsonException ex)
=> new($"The assertion credential JSON had an invalid format: {ex.Message}", ex);
- public static PasskeyException NullOriginalRequestOptionsJson()
- => new("The original passkey request options were unexpectedly null.");
-
- public static PasskeyException InvalidOriginalRequestOptionsJsonFormat(JsonException ex)
- => new($"The original passkey request options had an invalid format: {ex.Message}", ex);
-
public static PasskeyException NullClientDataJson()
=> new("The client data JSON was unexpectedly null.");
@@ -149,5 +122,17 @@ public static PasskeyException InvalidClientDataJsonFormat(JsonException ex)
public static PasskeyException InvalidCredentialPublicKey(Exception ex)
=> new($"The credential public key was invalid.", ex);
+
+ public static PasskeyException NullAttestationStateJson()
+ => new("the assertion state json was unexpectedly null.");
+
+ public static PasskeyException NullAssertionStateJson()
+ => new("the assertion state json was unexpectedly null.");
+
+ public static PasskeyException InvalidAttestationStateJsonFormat(JsonException ex)
+ => new($"The attestation state JSON had an invalid format: {ex.Message}", ex);
+
+ public static PasskeyException InvalidAssertionStateJsonFormat(JsonException ex)
+ => new($"The assertion state JSON had an invalid format: {ex.Message}", ex);
}
}
diff --git a/src/Identity/Core/src/PasskeyOptions.cs b/src/Identity/Core/src/PasskeyOptions.cs
new file mode 100644
index 000000000000..6e123ec3c5fd
--- /dev/null
+++ b/src/Identity/Core/src/PasskeyOptions.cs
@@ -0,0 +1,124 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Identity;
+
+///
+/// Specifies options for passkey requirements.
+///
+public class PasskeyOptions
+{
+ ///
+ /// Gets or sets the time that the server is willing to wait for a passkey operation to complete.
+ ///
+ ///
+ /// The default value is 5 minutes.
+ /// See
+ /// and .
+ ///
+ public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(5);
+
+ ///
+ /// The size of the challenge in bytes sent to the client during WebAuthn attestation and assertion.
+ ///
+ ///
+ /// The default value is 32 bytes.
+ /// See
+ /// and .
+ ///
+ public int ChallengeSize { get; set; } = 32;
+
+ ///
+ /// The effective domain of the server. Should be unique and will be used as the identity for the server.
+ ///
+ ///
+ /// If left , the server's origin may be used instead.
+ /// See .
+ ///
+ public string? ServerDomain { get; set; }
+
+ ///
+ /// Gets or sets the user verification requirement.
+ ///
+ ///
+ /// See .
+ /// Possible values are "required", "preferred", and "discouraged".
+ /// The default value is "preferred".
+ ///
+ public string? UserVerificationRequirement { get; set; }
+
+ ///
+ /// Gets or sets the extent to which the server desires to create a client-side discoverable credential.
+ /// Supported values are "discouraged", "preferred", or "required".
+ ///
+ ///
+ /// See .
+ ///
+ public string? ResidentKeyRequirement { get; set; }
+
+ ///
+ /// Gets or sets the attestation conveyance preference.
+ ///
+ ///
+ /// See .
+ /// The default value is "none".
+ ///
+ public string? AttestationConveyancePreference { get; set; }
+
+ ///
+ /// Gets or sets the authenticator attachment.
+ ///
+ ///
+ /// See .
+ ///
+ public string? AuthenticatorAttachment { get; set; }
+
+ ///
+ /// Gets or sets a function that determines whether the given COSE algorithm identifier
+ /// is allowed for passkey operations.
+ ///
+ ///
+ /// If all supported algorithms are allowed.
+ /// See .
+ ///
+ public Func? IsAllowedAlgorithm { get; set; }
+
+ ///
+ /// Gets or sets a function that validates the origin of the request.
+ ///
+ ///
+ /// By default, this function disallows cross-origin requests and checks
+ /// that the request's origin header matches the credential's origin.
+ ///
+ public Func> ValidateOrigin { get; set; } = DefaultValidateOrigin;
+
+ ///
+ /// Gets or sets a function that verifies the attestation statement of a passkey.
+ ///
+ ///
+ /// By default, this function does not perform any verification and always returns .
+ ///
+ public Func> VerifyAttestationStatement { get; set; } = DefaultVerifyAttestationStatement;
+
+ private static Task DefaultValidateOrigin(PasskeyOriginValidationContext context)
+ {
+ var result = IsValidOrigin();
+ return Task.FromResult(result);
+
+ bool IsValidOrigin()
+ {
+ if (string.IsNullOrEmpty(context.Origin) ||
+ context.CrossOrigin ||
+ !Uri.TryCreate(context.Origin, UriKind.Absolute, out var originUri))
+ {
+ return false;
+ }
+
+ // Uri.Equals correctly handles string comparands.
+ return context.HttpContext.Request.Headers.Origin is [var origin] && originUri.Equals(origin);
+ }
+ }
+
+ private static Task DefaultVerifyAttestationStatement(PasskeyAttestationStatementVerificationContext context)
+ => Task.FromResult(true);
+}
diff --git a/src/Identity/Core/src/PasskeyOriginInfo.cs b/src/Identity/Core/src/PasskeyOriginInfo.cs
deleted file mode 100644
index 30576f1609fc..000000000000
--- a/src/Identity/Core/src/PasskeyOriginInfo.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.AspNetCore.Identity;
-
-///
-/// Contains information used for determining whether a passkey's origin is valid.
-///
-/// The fully-qualified origin of the requester.
-/// Whether the request came from a cross-origin <iframe>
-public readonly struct PasskeyOriginInfo(string origin, bool crossOrigin)
-{
- ///
- /// Gets the fully-qualified origin of the requester.
- ///
- public string Origin { get; } = origin;
-
- ///
- /// Gets whether the request came from a cross-origin <iframe>.
- ///
- public bool CrossOrigin { get; } = crossOrigin;
-}
diff --git a/src/Identity/Core/src/PasskeyOriginValidationContext.cs b/src/Identity/Core/src/PasskeyOriginValidationContext.cs
new file mode 100644
index 000000000000..14bc735282e8
--- /dev/null
+++ b/src/Identity/Core/src/PasskeyOriginValidationContext.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Identity;
+
+///
+/// Contains information used for determining whether a passkey's origin is valid.
+///
+public readonly struct PasskeyOriginValidationContext
+{
+ ///
+ /// Gets or sets the HTTP context associated with the request.
+ ///
+ public required HttpContext HttpContext { get; init; }
+
+ ///
+ /// Gets or sets the fully-qualified origin of the requester.
+ ///
+ ///
+ /// See .
+ ///
+ public required string Origin { get; init; }
+
+ ///
+ /// Gets or sets whether the request came from a cross-origin <iframe>.
+ ///
+ ///
+ /// See .
+ ///
+ public required bool CrossOrigin { get; init; }
+
+ ///
+ /// Gets or sets the fully-qualified top-level origin of the requester.
+ ///
+ ///
+ /// See .
+ ///
+ public string? TopOrigin { get; init; }
+}
diff --git a/src/Identity/Core/src/PasskeyRequestArgs.cs b/src/Identity/Core/src/PasskeyRequestArgs.cs
deleted file mode 100644
index 25df25909e49..000000000000
--- a/src/Identity/Core/src/PasskeyRequestArgs.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Text.Json;
-
-namespace Microsoft.AspNetCore.Identity;
-
-///
-/// Represents arguments for generating .
-///
-public sealed class PasskeyRequestArgs
- where TUser : class
-{
- ///
- /// Gets or sets the user verification requirement.
- ///
- ///
- /// See .
- /// Possible values are "required", "preferred", and "discouraged".
- /// The default value is "preferred".
- ///
- public string UserVerification { get; set; } = "preferred";
-
- ///
- /// Gets or sets the user to be authenticated.
- ///
- ///
- /// While this value is optional, it should be specified if the authenticating
- /// user can be identified. This can happen if, for example, the user provides
- /// a username before signing in with a passkey.
- ///
- public TUser? User { get; set; }
-
- ///
- /// Gets or sets the client extension inputs.
- ///
- ///
- /// See .
- ///
- public JsonElement? Extensions { get; set; }
-}
diff --git a/src/Identity/Core/src/PasskeyRequestOptions.cs b/src/Identity/Core/src/PasskeyRequestOptions.cs
deleted file mode 100644
index ac034c8711e7..000000000000
--- a/src/Identity/Core/src/PasskeyRequestOptions.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.AspNetCore.Identity;
-
-///
-/// Represents options for a passkey request.
-///
-/// The ID of the user for whom this request is made.
-/// The JSON representation of the options.
-///
-/// See .
-///
-public sealed class PasskeyRequestOptions(string? userId, string optionsJson)
-{
- private readonly string _optionsJson = optionsJson;
-
- ///
- /// Gets the ID of the user for whom this request is made.
- ///
- public string? UserId { get; } = userId;
-
- ///
- /// Gets the JSON representation of the options.
- ///
- ///
- /// The structure of the JSON string matches the description in the WebAuthn specification.
- /// See .
- ///
- public string AsJson()
- => _optionsJson;
-
- ///
- /// Gets the JSON representation of the options.
- ///
- ///
- /// The structure of the JSON string matches the description in the WebAuthn specification.
- /// See .
- ///
- public override string ToString()
- => _optionsJson;
-}
diff --git a/src/Identity/Core/src/PasskeyRequestOptionsResult.cs b/src/Identity/Core/src/PasskeyRequestOptionsResult.cs
new file mode 100644
index 000000000000..f5e78a2bcb3b
--- /dev/null
+++ b/src/Identity/Core/src/PasskeyRequestOptionsResult.cs
@@ -0,0 +1,28 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Identity;
+
+///
+/// Represents the result of a passkey request options generation.
+///
+public sealed class PasskeyRequestOptionsResult
+{
+ ///
+ /// Gets or sets the JSON representation of the request options.
+ ///
+ ///
+ /// The structure of this JSON is compatible with
+ ///
+ /// and should be used with the navigator.credentials.get() JavaScript API.
+ ///
+ public required string RequestOptionsJson { get; init; }
+
+ ///
+ /// Gets or sets the state to be used in the assertion procedure.
+ ///
+ ///
+ /// This can be later retrieved during assertion with .
+ ///
+ public string? AssertionState { get; init; }
+}
diff --git a/src/Identity/Core/src/PasskeyUserEntity.cs b/src/Identity/Core/src/PasskeyUserEntity.cs
index 91e8de5ea09c..8b19c0d38783 100644
--- a/src/Identity/Core/src/PasskeyUserEntity.cs
+++ b/src/Identity/Core/src/PasskeyUserEntity.cs
@@ -6,23 +6,20 @@ namespace Microsoft.AspNetCore.Identity;
///
/// Represents information about the user associated with a passkey.
///
-/// The user ID.
-/// The name of the user.
-/// The display name of the user. When omitted, defaults to the user's name.
-public sealed class PasskeyUserEntity(string id, string name, string? displayName)
+public sealed class PasskeyUserEntity
{
///
/// Gets the user ID associated with a passkey.
///
- public string Id { get; } = id;
+ public required string Id { get; init; }
///
/// Gets the name of the user associated with a passkey.
///
- public string Name { get; } = name;
+ public required string Name { get; init; }
///
/// Gets the display name of the user associated with a passkey.
///
- public string DisplayName { get; } = displayName ?? name;
+ public required string DisplayName { get; init; }
}
diff --git a/src/Identity/Core/src/AuthenticatorSelectionCriteria.cs b/src/Identity/Core/src/Passkeys/AuthenticatorSelectionCriteria.cs
similarity index 93%
rename from src/Identity/Core/src/AuthenticatorSelectionCriteria.cs
rename to src/Identity/Core/src/Passkeys/AuthenticatorSelectionCriteria.cs
index fd834ad3e516..0247bab57954 100644
--- a/src/Identity/Core/src/AuthenticatorSelectionCriteria.cs
+++ b/src/Identity/Core/src/Passkeys/AuthenticatorSelectionCriteria.cs
@@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Identity;
///
/// See .
///
-public sealed class AuthenticatorSelectionCriteria
+internal sealed class AuthenticatorSelectionCriteria
{
///
/// Gets or sets the authenticator attachment.
@@ -42,5 +42,5 @@ public sealed class AuthenticatorSelectionCriteria
///
/// See .
///
- public string UserVerification { get; set; } = "preferred";
+ public string? UserVerification { get; set; }
}
diff --git a/src/Identity/Core/src/Passkeys/CollectedClientData.cs b/src/Identity/Core/src/Passkeys/CollectedClientData.cs
index 8e2e747283da..0d94864fcd6a 100644
--- a/src/Identity/Core/src/Passkeys/CollectedClientData.cs
+++ b/src/Identity/Core/src/Passkeys/CollectedClientData.cs
@@ -35,6 +35,11 @@ internal sealed class CollectedClientData
///
public bool? CrossOrigin { get; init; }
+ ///
+ /// Gets or sets the fully qualified top-level origin of the requester.
+ ///
+ public string? TopOrigin { get; init; }
+
///
/// Gets or sets information about the state of the token binding protocol.
///
diff --git a/src/Identity/Core/src/Passkeys/CredentialPublicKey.cs b/src/Identity/Core/src/Passkeys/CredentialPublicKey.cs
index 27a322cf6741..8774524c2edb 100644
--- a/src/Identity/Core/src/Passkeys/CredentialPublicKey.cs
+++ b/src/Identity/Core/src/Passkeys/CredentialPublicKey.cs
@@ -16,6 +16,46 @@ internal sealed class CredentialPublicKey
public COSEAlgorithmIdentifier Alg => _alg;
+ ///
+ /// Contains all supported public key credential parameters.
+ ///
+ ///
+ /// This list is sorted in the order of preference, with the most preferred algorithm first.
+ ///
+ internal static IReadOnlyList AllSupportedParameters { get; } =
+ // Keep this list in sync with IsSupportedAlgorithm.
+ [
+ new() { Type = "public-key", Alg = COSEAlgorithmIdentifier.ES256 },
+ new() { Type = "public-key", Alg = COSEAlgorithmIdentifier.PS256 },
+ new() { Type = "public-key", Alg = COSEAlgorithmIdentifier.ES384 },
+ new() { Type = "public-key", Alg = COSEAlgorithmIdentifier.PS384 },
+ new() { Type = "public-key", Alg = COSEAlgorithmIdentifier.PS512 },
+ new() { Type = "public-key", Alg = COSEAlgorithmIdentifier.RS256 },
+ new() { Type = "public-key", Alg = COSEAlgorithmIdentifier.ES512 },
+ new() { Type = "public-key", Alg = COSEAlgorithmIdentifier.RS384 },
+ new() { Type = "public-key", Alg = COSEAlgorithmIdentifier.RS512 },
+ ];
+
+ ///
+ /// Gets whether the specified COSE algorithm identifier is supported.
+ ///
+ /// The algorithm identifier.
+ internal static bool IsSupportedAlgorithm(COSEAlgorithmIdentifier alg)
+ // Keep this in sync with AllSupportedParameters.
+ => alg switch
+ {
+ COSEAlgorithmIdentifier.ES256 or
+ COSEAlgorithmIdentifier.PS256 or
+ COSEAlgorithmIdentifier.ES384 or
+ COSEAlgorithmIdentifier.PS384 or
+ COSEAlgorithmIdentifier.PS512 or
+ COSEAlgorithmIdentifier.RS256 or
+ COSEAlgorithmIdentifier.ES512 or
+ COSEAlgorithmIdentifier.RS384 or
+ COSEAlgorithmIdentifier.RS512 => true,
+ _ => false,
+ };
+
private CredentialPublicKey(ReadOnlyMemory bytes)
{
var reader = Ctap2CborReader.Create(bytes);
diff --git a/src/Identity/Core/src/Passkeys/PasskeyAssertionState.cs b/src/Identity/Core/src/Passkeys/PasskeyAssertionState.cs
new file mode 100644
index 000000000000..fd5f2ca01095
--- /dev/null
+++ b/src/Identity/Core/src/Passkeys/PasskeyAssertionState.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Identity;
+
+// Represents the state to persist between creating the passkey request options
+// and performing passkey assertion.
+internal sealed class PasskeyAssertionState
+{
+ public required ReadOnlyMemory Challenge { get; init; }
+
+ public string? UserId { get; init; }
+}
diff --git a/src/Identity/Core/src/Passkeys/PasskeyAttestationState.cs b/src/Identity/Core/src/Passkeys/PasskeyAttestationState.cs
new file mode 100644
index 000000000000..95c91639c71e
--- /dev/null
+++ b/src/Identity/Core/src/Passkeys/PasskeyAttestationState.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Identity;
+
+// Represents the state to persist between creating the passkey creation options
+// and performing passkey attestation.
+internal sealed class PasskeyAttestationState
+{
+ public required ReadOnlyMemory Challenge { get; init; }
+
+ public required PasskeyUserEntity UserEntity { get; init; }
+}
diff --git a/src/Identity/Core/src/Passkeys/PublicKeyCredentialCreationOptions.cs b/src/Identity/Core/src/Passkeys/PublicKeyCredentialCreationOptions.cs
index 2f07198a61db..5d92df9a796f 100644
--- a/src/Identity/Core/src/Passkeys/PublicKeyCredentialCreationOptions.cs
+++ b/src/Identity/Core/src/Passkeys/PublicKeyCredentialCreationOptions.cs
@@ -56,7 +56,7 @@ internal sealed class PublicKeyCredentialCreationOptions
///
/// Gets or sets the attestation conveyance preference for the relying party.
///
- public string Attestation { get; init; } = "none";
+ public string? Attestation { get; init; }
///
/// Gets or sets the attestation statement format preferences of the relying party, ordered from most preferred to least preferred.
diff --git a/src/Identity/Core/src/Passkeys/PublicKeyCredentialParameters.cs b/src/Identity/Core/src/Passkeys/PublicKeyCredentialParameters.cs
index d6abed1c1d6a..399e81221e03 100644
--- a/src/Identity/Core/src/Passkeys/PublicKeyCredentialParameters.cs
+++ b/src/Identity/Core/src/Passkeys/PublicKeyCredentialParameters.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Text.Json.Serialization;
-
namespace Microsoft.AspNetCore.Identity;
///
@@ -11,33 +9,8 @@ namespace Microsoft.AspNetCore.Identity;
///
/// See
///
-[method: JsonConstructor]
-internal readonly struct PublicKeyCredentialParameters(string type, COSEAlgorithmIdentifier alg)
+internal readonly struct PublicKeyCredentialParameters()
{
- ///
- /// Contains all supported public key credential parameters.
- ///
- ///
- /// Keep this list in sync with the supported algorithms in .
- /// This list is sorted in the order of preference, with the most preferred algorithm first.
- ///
- internal static IReadOnlyList AllSupportedParameters { get; } =
- [
- new(COSEAlgorithmIdentifier.ES256),
- new(COSEAlgorithmIdentifier.PS256),
- new(COSEAlgorithmIdentifier.ES384),
- new(COSEAlgorithmIdentifier.PS384),
- new(COSEAlgorithmIdentifier.PS512),
- new(COSEAlgorithmIdentifier.RS256),
- new(COSEAlgorithmIdentifier.ES512),
- new(COSEAlgorithmIdentifier.RS384),
- new(COSEAlgorithmIdentifier.RS512),
- ];
-
- public PublicKeyCredentialParameters(COSEAlgorithmIdentifier alg)
- : this(type: "public-key", alg)
- {
- }
///
/// Gets the type of the credential.
@@ -45,7 +18,7 @@ public PublicKeyCredentialParameters(COSEAlgorithmIdentifier alg)
///
/// See .
///
- public string Type { get; } = type;
+ public required string Type { get; init; }
///
/// Gets or sets the cryptographic signature algorithm identifier.
@@ -53,5 +26,5 @@ public PublicKeyCredentialParameters(COSEAlgorithmIdentifier alg)
///
/// See .
///
- public COSEAlgorithmIdentifier Alg { get; } = alg;
+ public required COSEAlgorithmIdentifier Alg { get; init; }
}
diff --git a/src/Identity/Core/src/Passkeys/PublicKeyCredentialRequestOptions.cs b/src/Identity/Core/src/Passkeys/PublicKeyCredentialRequestOptions.cs
index 3978cd795693..cd4a7fdc1b9e 100644
--- a/src/Identity/Core/src/Passkeys/PublicKeyCredentialRequestOptions.cs
+++ b/src/Identity/Core/src/Passkeys/PublicKeyCredentialRequestOptions.cs
@@ -36,7 +36,7 @@ internal sealed class PublicKeyCredentialRequestOptions
///
/// Gets or sets the user verification requirement for the request.
///
- public string UserVerification { get; init; } = "preferred";
+ public string? UserVerification { get; init; }
///
/// Gets or sets hints that guide the user agent in interacting with the user.
diff --git a/src/Identity/Core/src/PublicAPI.Unshipped.txt b/src/Identity/Core/src/PublicAPI.Unshipped.txt
index 9594235ec62f..603bd5e20e68 100644
--- a/src/Identity/Core/src/PublicAPI.Unshipped.txt
+++ b/src/Identity/Core/src/PublicAPI.Unshipped.txt
@@ -1,107 +1,111 @@
#nullable enable
-Microsoft.AspNetCore.Identity.AuthenticatorSelectionCriteria
-Microsoft.AspNetCore.Identity.AuthenticatorSelectionCriteria.AuthenticatorAttachment.get -> string?
-Microsoft.AspNetCore.Identity.AuthenticatorSelectionCriteria.AuthenticatorAttachment.set -> void
-Microsoft.AspNetCore.Identity.AuthenticatorSelectionCriteria.AuthenticatorSelectionCriteria() -> void
-Microsoft.AspNetCore.Identity.AuthenticatorSelectionCriteria.RequireResidentKey.get -> bool
-Microsoft.AspNetCore.Identity.AuthenticatorSelectionCriteria.ResidentKey.get -> string?
-Microsoft.AspNetCore.Identity.AuthenticatorSelectionCriteria.ResidentKey.set -> void
-Microsoft.AspNetCore.Identity.AuthenticatorSelectionCriteria.UserVerification.get -> string!
-Microsoft.AspNetCore.Identity.AuthenticatorSelectionCriteria.UserVerification.set -> void
Microsoft.AspNetCore.Identity.DefaultPasskeyHandler
-Microsoft.AspNetCore.Identity.DefaultPasskeyHandler.DefaultPasskeyHandler(Microsoft.Extensions.Options.IOptions! options) -> void
-Microsoft.AspNetCore.Identity.DefaultPasskeyHandler.PerformAssertionAsync(Microsoft.AspNetCore.Identity.PasskeyAssertionContext! context) -> System.Threading.Tasks.Task!>!
-Microsoft.AspNetCore.Identity.DefaultPasskeyHandler.PerformAttestationAsync(Microsoft.AspNetCore.Identity.PasskeyAttestationContext! context) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Identity.DefaultPasskeyHandler.DefaultPasskeyHandler(Microsoft.AspNetCore.Identity.UserManager! userManager, Microsoft.Extensions.Options.IOptions! options) -> void
+Microsoft.AspNetCore.Identity.DefaultPasskeyHandler.MakeCreationOptionsAsync(Microsoft.AspNetCore.Identity.PasskeyUserEntity! userEntity, Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Identity.DefaultPasskeyHandler.MakeRequestOptionsAsync(TUser? user, Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Identity.DefaultPasskeyHandler.PerformAssertionAsync(Microsoft.AspNetCore.Identity.PasskeyAssertionContext! context) -> System.Threading.Tasks.Task!>!
+Microsoft.AspNetCore.Identity.DefaultPasskeyHandler.PerformAttestationAsync(Microsoft.AspNetCore.Identity.PasskeyAttestationContext! context) -> System.Threading.Tasks.Task!
Microsoft.AspNetCore.Identity.IPasskeyHandler
-Microsoft.AspNetCore.Identity.IPasskeyHandler.PerformAssertionAsync(Microsoft.AspNetCore.Identity.PasskeyAssertionContext! context) -> System.Threading.Tasks.Task!>!
-Microsoft.AspNetCore.Identity.IPasskeyHandler.PerformAttestationAsync(Microsoft.AspNetCore.Identity.PasskeyAttestationContext! context) -> System.Threading.Tasks.Task!
-Microsoft.AspNetCore.Identity.PasskeyAssertionContext
-Microsoft.AspNetCore.Identity.PasskeyAssertionContext.CredentialJson.get -> string!
-Microsoft.AspNetCore.Identity.PasskeyAssertionContext.CredentialJson.init -> void
-Microsoft.AspNetCore.Identity.PasskeyAssertionContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
-Microsoft.AspNetCore.Identity.PasskeyAssertionContext.HttpContext.init -> void
-Microsoft.AspNetCore.Identity.PasskeyAssertionContext.OriginalOptionsJson.get -> string!
-Microsoft.AspNetCore.Identity.PasskeyAssertionContext.OriginalOptionsJson.init -> void
-Microsoft.AspNetCore.Identity.PasskeyAssertionContext.PasskeyAssertionContext() -> void
-Microsoft.AspNetCore.Identity.PasskeyAssertionContext.User.get -> TUser?
-Microsoft.AspNetCore.Identity.PasskeyAssertionContext.User.init -> void
-Microsoft.AspNetCore.Identity.PasskeyAssertionContext.UserManager.get -> Microsoft.AspNetCore.Identity.UserManager!
-Microsoft.AspNetCore.Identity.PasskeyAssertionContext.UserManager.init -> void
+Microsoft.AspNetCore.Identity.IPasskeyHandler.MakeCreationOptionsAsync(Microsoft.AspNetCore.Identity.PasskeyUserEntity! userEntity, Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Identity.IPasskeyHandler.MakeRequestOptionsAsync(TUser? user, Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Identity.IPasskeyHandler.PerformAssertionAsync(Microsoft.AspNetCore.Identity.PasskeyAssertionContext! context) -> System.Threading.Tasks.Task!>!
+Microsoft.AspNetCore.Identity.IPasskeyHandler.PerformAttestationAsync(Microsoft.AspNetCore.Identity.PasskeyAttestationContext! context) -> System.Threading.Tasks.Task!
+Microsoft.AspNetCore.Identity.PasskeyAssertionContext
+Microsoft.AspNetCore.Identity.PasskeyAssertionContext.AssertionState.get -> string?
+Microsoft.AspNetCore.Identity.PasskeyAssertionContext.AssertionState.init -> void
+Microsoft.AspNetCore.Identity.PasskeyAssertionContext.CredentialJson.get -> string!
+Microsoft.AspNetCore.Identity.PasskeyAssertionContext.CredentialJson.init -> void
+Microsoft.AspNetCore.Identity.PasskeyAssertionContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
+Microsoft.AspNetCore.Identity.PasskeyAssertionContext.HttpContext.init -> void
+Microsoft.AspNetCore.Identity.PasskeyAssertionContext.PasskeyAssertionContext() -> void
Microsoft.AspNetCore.Identity.PasskeyAssertionResult
Microsoft.AspNetCore.Identity.PasskeyAssertionResult
Microsoft.AspNetCore.Identity.PasskeyAssertionResult.Failure.get -> Microsoft.AspNetCore.Identity.PasskeyException?
Microsoft.AspNetCore.Identity.PasskeyAssertionResult.Passkey.get -> Microsoft.AspNetCore.Identity.UserPasskeyInfo?
Microsoft.AspNetCore.Identity.PasskeyAssertionResult.Succeeded.get -> bool
Microsoft.AspNetCore.Identity.PasskeyAssertionResult.User.get -> TUser?
-Microsoft.AspNetCore.Identity.PasskeyAttestationContext
-Microsoft.AspNetCore.Identity.PasskeyAttestationContext.CredentialJson.get -> string!
-Microsoft.AspNetCore.Identity.PasskeyAttestationContext.CredentialJson.init -> void
-Microsoft.AspNetCore.Identity.PasskeyAttestationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
-Microsoft.AspNetCore.Identity.PasskeyAttestationContext.HttpContext.init -> void
-Microsoft.AspNetCore.Identity.PasskeyAttestationContext.OriginalOptionsJson.get -> string!
-Microsoft.AspNetCore.Identity.PasskeyAttestationContext.OriginalOptionsJson.init -> void
-Microsoft.AspNetCore.Identity.PasskeyAttestationContext.PasskeyAttestationContext() -> void
-Microsoft.AspNetCore.Identity.PasskeyAttestationContext.UserManager.get -> Microsoft.AspNetCore.Identity.UserManager!
-Microsoft.AspNetCore.Identity.PasskeyAttestationContext.UserManager.init -> void
+Microsoft.AspNetCore.Identity.PasskeyAttestationContext
+Microsoft.AspNetCore.Identity.PasskeyAttestationContext.AttestationState.get -> string?
+Microsoft.AspNetCore.Identity.PasskeyAttestationContext.AttestationState.init -> void
+Microsoft.AspNetCore.Identity.PasskeyAttestationContext.CredentialJson.get -> string!
+Microsoft.AspNetCore.Identity.PasskeyAttestationContext.CredentialJson.init -> void
+Microsoft.AspNetCore.Identity.PasskeyAttestationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
+Microsoft.AspNetCore.Identity.PasskeyAttestationContext.HttpContext.init -> void
+Microsoft.AspNetCore.Identity.PasskeyAttestationContext.PasskeyAttestationContext() -> void
Microsoft.AspNetCore.Identity.PasskeyAttestationResult
Microsoft.AspNetCore.Identity.PasskeyAttestationResult.Failure.get -> Microsoft.AspNetCore.Identity.PasskeyException?
Microsoft.AspNetCore.Identity.PasskeyAttestationResult.Passkey.get -> Microsoft.AspNetCore.Identity.UserPasskeyInfo?
Microsoft.AspNetCore.Identity.PasskeyAttestationResult.Succeeded.get -> bool
-Microsoft.AspNetCore.Identity.PasskeyCreationArgs
-Microsoft.AspNetCore.Identity.PasskeyCreationArgs.Attestation.get -> string!
-Microsoft.AspNetCore.Identity.PasskeyCreationArgs.Attestation.set -> void
-Microsoft.AspNetCore.Identity.PasskeyCreationArgs.AuthenticatorSelection.get -> Microsoft.AspNetCore.Identity.AuthenticatorSelectionCriteria?
-Microsoft.AspNetCore.Identity.PasskeyCreationArgs.AuthenticatorSelection.set -> void
-Microsoft.AspNetCore.Identity.PasskeyCreationArgs.Extensions.get -> System.Text.Json.JsonElement?
-Microsoft.AspNetCore.Identity.PasskeyCreationArgs.Extensions.set -> void
-Microsoft.AspNetCore.Identity.PasskeyCreationArgs.PasskeyCreationArgs(Microsoft.AspNetCore.Identity.PasskeyUserEntity! userEntity) -> void
-Microsoft.AspNetCore.Identity.PasskeyCreationArgs.UserEntity.get -> Microsoft.AspNetCore.Identity.PasskeyUserEntity!
-Microsoft.AspNetCore.Identity.PasskeyCreationOptions
-Microsoft.AspNetCore.Identity.PasskeyCreationOptions.AsJson() -> string!
-Microsoft.AspNetCore.Identity.PasskeyCreationOptions.PasskeyCreationOptions(Microsoft.AspNetCore.Identity.PasskeyUserEntity! userEntity, string! optionsJson) -> void
-Microsoft.AspNetCore.Identity.PasskeyCreationOptions.UserEntity.get -> Microsoft.AspNetCore.Identity.PasskeyUserEntity!
+Microsoft.AspNetCore.Identity.PasskeyAttestationResult.UserEntity.get -> Microsoft.AspNetCore.Identity.PasskeyUserEntity?
+Microsoft.AspNetCore.Identity.PasskeyAttestationStatementVerificationContext
+Microsoft.AspNetCore.Identity.PasskeyAttestationStatementVerificationContext.AttestationObject.get -> System.ReadOnlyMemory
+Microsoft.AspNetCore.Identity.PasskeyAttestationStatementVerificationContext.AttestationObject.init -> void
+Microsoft.AspNetCore.Identity.PasskeyAttestationStatementVerificationContext.ClientDataHash.get -> System.ReadOnlyMemory
+Microsoft.AspNetCore.Identity.PasskeyAttestationStatementVerificationContext.ClientDataHash.init -> void
+Microsoft.AspNetCore.Identity.PasskeyAttestationStatementVerificationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
+Microsoft.AspNetCore.Identity.PasskeyAttestationStatementVerificationContext.HttpContext.init -> void
+Microsoft.AspNetCore.Identity.PasskeyAttestationStatementVerificationContext.PasskeyAttestationStatementVerificationContext() -> void
+Microsoft.AspNetCore.Identity.PasskeyCreationOptionsResult
+Microsoft.AspNetCore.Identity.PasskeyCreationOptionsResult.AttestationState.get -> string?
+Microsoft.AspNetCore.Identity.PasskeyCreationOptionsResult.AttestationState.init -> void
+Microsoft.AspNetCore.Identity.PasskeyCreationOptionsResult.CreationOptionsJson.get -> string!
+Microsoft.AspNetCore.Identity.PasskeyCreationOptionsResult.CreationOptionsJson.init -> void
+Microsoft.AspNetCore.Identity.PasskeyCreationOptionsResult.PasskeyCreationOptionsResult() -> void
Microsoft.AspNetCore.Identity.PasskeyException
Microsoft.AspNetCore.Identity.PasskeyException.PasskeyException(string! message) -> void
Microsoft.AspNetCore.Identity.PasskeyException.PasskeyException(string! message, System.Exception? innerException) -> void
-Microsoft.AspNetCore.Identity.PasskeyOriginInfo
-Microsoft.AspNetCore.Identity.PasskeyOriginInfo.CrossOrigin.get -> bool
-Microsoft.AspNetCore.Identity.PasskeyOriginInfo.Origin.get -> string!
-Microsoft.AspNetCore.Identity.PasskeyOriginInfo.PasskeyOriginInfo() -> void
-Microsoft.AspNetCore.Identity.PasskeyOriginInfo.PasskeyOriginInfo(string! origin, bool crossOrigin) -> void
-Microsoft.AspNetCore.Identity.PasskeyRequestArgs
-Microsoft.AspNetCore.Identity.PasskeyRequestArgs.Extensions.get -> System.Text.Json.JsonElement?
-Microsoft.AspNetCore.Identity.PasskeyRequestArgs.Extensions.set -> void
-Microsoft.AspNetCore.Identity.PasskeyRequestArgs.PasskeyRequestArgs() -> void
-Microsoft.AspNetCore.Identity.PasskeyRequestArgs.User.get -> TUser?
-Microsoft.AspNetCore.Identity.PasskeyRequestArgs.User.set -> void
-Microsoft.AspNetCore.Identity.PasskeyRequestArgs.UserVerification.get -> string!
-Microsoft.AspNetCore.Identity.PasskeyRequestArgs.UserVerification.set -> void
-Microsoft.AspNetCore.Identity.PasskeyRequestOptions
-Microsoft.AspNetCore.Identity.PasskeyRequestOptions.AsJson() -> string!
-Microsoft.AspNetCore.Identity.PasskeyRequestOptions.PasskeyRequestOptions(string? userId, string! optionsJson) -> void
-Microsoft.AspNetCore.Identity.PasskeyRequestOptions.UserId.get -> string?
+Microsoft.AspNetCore.Identity.PasskeyOptions
+Microsoft.AspNetCore.Identity.PasskeyOptions.AttestationConveyancePreference.get -> string?
+Microsoft.AspNetCore.Identity.PasskeyOptions.AttestationConveyancePreference.set -> void
+Microsoft.AspNetCore.Identity.PasskeyOptions.AuthenticatorAttachment.get -> string?
+Microsoft.AspNetCore.Identity.PasskeyOptions.AuthenticatorAttachment.set -> void
+Microsoft.AspNetCore.Identity.PasskeyOptions.ChallengeSize.get -> int
+Microsoft.AspNetCore.Identity.PasskeyOptions.ChallengeSize.set -> void
+Microsoft.AspNetCore.Identity.PasskeyOptions.IsAllowedAlgorithm.get -> System.Func?
+Microsoft.AspNetCore.Identity.PasskeyOptions.IsAllowedAlgorithm.set -> void
+Microsoft.AspNetCore.Identity.PasskeyOptions.PasskeyOptions() -> void
+Microsoft.AspNetCore.Identity.PasskeyOptions.ResidentKeyRequirement.get -> string?
+Microsoft.AspNetCore.Identity.PasskeyOptions.ResidentKeyRequirement.set -> void
+Microsoft.AspNetCore.Identity.PasskeyOptions.ServerDomain.get -> string?
+Microsoft.AspNetCore.Identity.PasskeyOptions.ServerDomain.set -> void
+Microsoft.AspNetCore.Identity.PasskeyOptions.Timeout.get -> System.TimeSpan
+Microsoft.AspNetCore.Identity.PasskeyOptions.Timeout.set -> void
+Microsoft.AspNetCore.Identity.PasskeyOptions.UserVerificationRequirement.get -> string?
+Microsoft.AspNetCore.Identity.PasskeyOptions.UserVerificationRequirement.set -> void
+Microsoft.AspNetCore.Identity.PasskeyOptions.ValidateOrigin.get -> System.Func!>!
+Microsoft.AspNetCore.Identity.PasskeyOptions.ValidateOrigin.set -> void
+Microsoft.AspNetCore.Identity.PasskeyOptions.VerifyAttestationStatement.get -> System.Func!>!
+Microsoft.AspNetCore.Identity.PasskeyOptions.VerifyAttestationStatement.set -> void
+Microsoft.AspNetCore.Identity.PasskeyOriginValidationContext
+Microsoft.AspNetCore.Identity.PasskeyOriginValidationContext.CrossOrigin.get -> bool
+Microsoft.AspNetCore.Identity.PasskeyOriginValidationContext.CrossOrigin.init -> void
+Microsoft.AspNetCore.Identity.PasskeyOriginValidationContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext!
+Microsoft.AspNetCore.Identity.PasskeyOriginValidationContext.HttpContext.init -> void
+Microsoft.AspNetCore.Identity.PasskeyOriginValidationContext.Origin.get -> string!
+Microsoft.AspNetCore.Identity.PasskeyOriginValidationContext.Origin.init -> void
+Microsoft.AspNetCore.Identity.PasskeyOriginValidationContext.PasskeyOriginValidationContext() -> void
+Microsoft.AspNetCore.Identity.PasskeyOriginValidationContext.TopOrigin.get -> string?
+Microsoft.AspNetCore.Identity.PasskeyOriginValidationContext.TopOrigin.init -> void
+Microsoft.AspNetCore.Identity.PasskeyRequestOptionsResult
+Microsoft.AspNetCore.Identity.PasskeyRequestOptionsResult.AssertionState.get -> string?
+Microsoft.AspNetCore.Identity.PasskeyRequestOptionsResult.AssertionState.init -> void
+Microsoft.AspNetCore.Identity.PasskeyRequestOptionsResult.PasskeyRequestOptionsResult() -> void
+Microsoft.AspNetCore.Identity.PasskeyRequestOptionsResult.RequestOptionsJson.get -> string!
+Microsoft.AspNetCore.Identity.PasskeyRequestOptionsResult.RequestOptionsJson.init -> void
Microsoft.AspNetCore.Identity.PasskeyUserEntity
Microsoft.AspNetCore.Identity.PasskeyUserEntity.DisplayName.get -> string!
+Microsoft.AspNetCore.Identity.PasskeyUserEntity.DisplayName.init -> void
Microsoft.AspNetCore.Identity.PasskeyUserEntity.Id.get -> string!
+Microsoft.AspNetCore.Identity.PasskeyUserEntity.Id.init -> void
Microsoft.AspNetCore.Identity.PasskeyUserEntity.Name.get -> string!
-Microsoft.AspNetCore.Identity.PasskeyUserEntity.PasskeyUserEntity(string! id, string! name, string? displayName) -> void
-Microsoft.AspNetCore.Identity.SignInManager.SignInManager(Microsoft.AspNetCore.Identity.UserManager! userManager, Microsoft.AspNetCore.Http.IHttpContextAccessor! contextAccessor, Microsoft.AspNetCore.Identity.IUserClaimsPrincipalFactory! claimsFactory, Microsoft.Extensions.Options.IOptions! optionsAccessor, Microsoft.Extensions.Logging.ILogger!>! logger, Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider! schemes, Microsoft.AspNetCore.Identity.IUserConfirmation! confirmation, Microsoft.AspNetCore.Identity.IPasskeyHandler! passkeyHandler) -> void
-override Microsoft.AspNetCore.Identity.PasskeyCreationOptions.ToString() -> string!
-override Microsoft.AspNetCore.Identity.PasskeyRequestOptions.ToString() -> string!
+Microsoft.AspNetCore.Identity.PasskeyUserEntity.Name.init -> void
+Microsoft.AspNetCore.Identity.PasskeyUserEntity.PasskeyUserEntity() -> void
static Microsoft.AspNetCore.Identity.PasskeyAssertionResult.Fail(Microsoft.AspNetCore.Identity.PasskeyException! failure) -> Microsoft.AspNetCore.Identity.PasskeyAssertionResult!
static Microsoft.AspNetCore.Identity.PasskeyAssertionResult.Success(Microsoft.AspNetCore.Identity.UserPasskeyInfo! passkey, TUser! user) -> Microsoft.AspNetCore.Identity.PasskeyAssertionResult!
static Microsoft.AspNetCore.Identity.PasskeyAttestationResult.Fail(Microsoft.AspNetCore.Identity.PasskeyException! failure) -> Microsoft.AspNetCore.Identity.PasskeyAttestationResult!
-static Microsoft.AspNetCore.Identity.PasskeyAttestationResult.Success(Microsoft.AspNetCore.Identity.UserPasskeyInfo! passkey) -> Microsoft.AspNetCore.Identity.PasskeyAttestationResult!
-virtual Microsoft.AspNetCore.Identity.DefaultPasskeyHandler.IsValidOriginAsync(Microsoft.AspNetCore.Identity.PasskeyOriginInfo originInfo, Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
-virtual Microsoft.AspNetCore.Identity.DefaultPasskeyHandler.PerformAssertionCoreAsync(Microsoft.AspNetCore.Identity.PasskeyAssertionContext! context) -> System.Threading.Tasks.Task!>!
-virtual Microsoft.AspNetCore.Identity.DefaultPasskeyHandler.PerformAttestationCoreAsync(Microsoft.AspNetCore.Identity.PasskeyAttestationContext! context) -> System.Threading.Tasks.Task!
-virtual Microsoft.AspNetCore.Identity.DefaultPasskeyHandler.VerifyAttestationStatementAsync(System.ReadOnlyMemory attestationObject, System.ReadOnlyMemory clientDataHash, Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task!
-virtual Microsoft.AspNetCore.Identity.SignInManager.ConfigurePasskeyCreationOptionsAsync(Microsoft.AspNetCore.Identity.PasskeyCreationArgs! creationArgs) -> System.Threading.Tasks.Task!
-virtual Microsoft.AspNetCore.Identity.SignInManager.ConfigurePasskeyRequestOptionsAsync(Microsoft.AspNetCore.Identity.PasskeyRequestArgs! requestArgs) -> System.Threading.Tasks.Task!
-virtual Microsoft.AspNetCore.Identity.SignInManager.GeneratePasskeyCreationOptionsAsync(Microsoft.AspNetCore.Identity.PasskeyCreationArgs! creationArgs) -> System.Threading.Tasks.Task!
-virtual Microsoft.AspNetCore.Identity.SignInManager.GeneratePasskeyRequestOptionsAsync(Microsoft.AspNetCore.Identity.PasskeyRequestArgs! requestArgs) -> System.Threading.Tasks.Task!
-virtual Microsoft.AspNetCore.Identity.SignInManager.PasskeySignInAsync(string! credentialJson, Microsoft.AspNetCore.Identity.PasskeyRequestOptions! options) -> System.Threading.Tasks.Task!
-virtual Microsoft.AspNetCore.Identity.SignInManager.PerformPasskeyAssertionAsync(string! credentialJson, Microsoft.AspNetCore.Identity.PasskeyRequestOptions! options) -> System.Threading.Tasks.Task!>!
-virtual Microsoft.AspNetCore.Identity.SignInManager.PerformPasskeyAttestationAsync(string! credentialJson, Microsoft.AspNetCore.Identity.PasskeyCreationOptions! options) -> System.Threading.Tasks.Task!
-virtual Microsoft.AspNetCore.Identity.SignInManager.RetrievePasskeyCreationOptionsAsync() -> System.Threading.Tasks.Task!
-virtual Microsoft.AspNetCore.Identity.SignInManager.RetrievePasskeyRequestOptionsAsync() -> System.Threading.Tasks.Task!
+static Microsoft.AspNetCore.Identity.PasskeyAttestationResult.Success(Microsoft.AspNetCore.Identity.UserPasskeyInfo! passkey, Microsoft.AspNetCore.Identity.PasskeyUserEntity! userEntity) -> Microsoft.AspNetCore.Identity.PasskeyAttestationResult!
+virtual Microsoft.AspNetCore.Identity.SignInManager.MakePasskeyCreationOptionsAsync(Microsoft.AspNetCore.Identity.PasskeyUserEntity! userEntity) -> System.Threading.Tasks.Task!
+virtual Microsoft.AspNetCore.Identity.SignInManager.MakePasskeyRequestOptionsAsync(TUser? user) -> System.Threading.Tasks.Task!
+virtual Microsoft.AspNetCore.Identity.SignInManager.PasskeySignInAsync(string! credentialJson) -> System.Threading.Tasks.Task!
+virtual Microsoft.AspNetCore.Identity.SignInManager.PerformPasskeyAssertionAsync(string! credentialJson) -> System.Threading.Tasks.Task!>!
+virtual Microsoft.AspNetCore.Identity.SignInManager.PerformPasskeyAttestationAsync(string! credentialJson) -> System.Threading.Tasks.Task!
diff --git a/src/Identity/Core/src/SignInManager.cs b/src/Identity/Core/src/SignInManager.cs
index a41cc20d01f8..5d9e7659b4b5 100644
--- a/src/Identity/Core/src/SignInManager.cs
+++ b/src/Identity/Core/src/SignInManager.cs
@@ -4,11 +4,10 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Security.Claims;
-using System.Security.Cryptography;
using System.Text;
-using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -21,9 +20,9 @@ namespace Microsoft.AspNetCore.Identity;
public class SignInManager where TUser : class
{
private const string LoginProviderKey = "LoginProvider";
- private const string PasskeyCreationOptionsKey = "PasskeyCreationOptions";
- private const string PasskeyRequestOptionsKey = "PasskeyRequestOptions";
private const string XsrfKey = "XsrfId";
+ private const string PasskeyOperationKey = "PasskeyOperation";
+ private const string PasskeyStateKey = "PasskeyState";
private readonly IHttpContextAccessor _contextAccessor;
private readonly IAuthenticationSchemeProvider _schemes;
@@ -31,8 +30,7 @@ public class SignInManager where TUser : class
private readonly IPasskeyHandler? _passkeyHandler;
private HttpContext? _context;
private TwoFactorAuthenticationInfo? _twoFactorInfo;
- private PasskeyCreationOptions? _passkeyCreationOptions;
- private PasskeyRequestOptions? _passkeyRequestOptions;
+ private PasskeyAuthenticationInfo? _passkeyInfo;
///
/// Creates a new instance of .
@@ -63,32 +61,7 @@ public SignInManager(UserManager userManager,
Logger = logger;
_schemes = schemes;
_confirmation = confirmation;
- }
-
- ///
- /// Creates a new instance of .
- ///
- /// An instance of used to retrieve users from and persist users.
- /// The accessor used to access the .
- /// The factory to use to create claims principals for a user.
- /// The accessor used to access the .
- /// The logger used to log messages, warnings and errors.
- /// The scheme provider that is used enumerate the authentication schemes.
- /// The used check whether a user account is confirmed.
- /// The used when performing passkey attestation and assertion.
- public SignInManager(UserManager userManager,
- IHttpContextAccessor contextAccessor,
- IUserClaimsPrincipalFactory claimsFactory,
- IOptions optionsAccessor,
- ILogger> logger,
- IAuthenticationSchemeProvider schemes,
- IUserConfirmation confirmation,
- IPasskeyHandler passkeyHandler)
- : this(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
- {
- ArgumentNullException.ThrowIfNull(passkeyHandler);
-
- _passkeyHandler = passkeyHandler;
+ _passkeyHandler = userManager.ServiceProvider?.GetService>();
}
///
@@ -465,27 +438,68 @@ public virtual async Task CheckPasswordSignInAsync(TUser user, str
}
///
- /// Performs passkey attestation for the given and .
+ /// Generates passkey creation options for the specified .
///
+ /// The user entity for which to create passkey options.
+ /// A JSON string representing the created passkey options.
+ public virtual async Task MakePasskeyCreationOptionsAsync(PasskeyUserEntity userEntity)
+ {
+ ThrowIfNoPasskeyHandler();
+ ArgumentNullException.ThrowIfNull(userEntity);
+
+ var result = await _passkeyHandler.MakeCreationOptionsAsync(userEntity, Context);
+ await StorePasskeyAuthenticationInfoAsync(PasskeyOperations.Attestation, result.AttestationState);
+ return result.CreationOptionsJson;
+ }
+
+ ///
+ /// Creates passkey assertion options for the specified .
+ ///
+ /// The user for whom to create passkey assertion options.
+ /// A JSON string representing the created passkey assertion options.
+ public virtual async Task MakePasskeyRequestOptionsAsync(TUser? user)
+ {
+ ThrowIfNoPasskeyHandler();
+
+ var result = await _passkeyHandler.MakeRequestOptionsAsync(user, Context);
+ await StorePasskeyAuthenticationInfoAsync(PasskeyOperations.Assertion, result.AssertionState);
+ return result.RequestOptionsJson;
+ }
+
+ ///
+ /// Performs passkey attestation for the given .
+ ///
+ ///
+ /// The should be obtained by JSON-serializing the result of the
+ /// navigator.credentials.create() JavaScript API. The argument to navigator.credentials.create()
+ /// should be obtained by calling .
+ ///
/// The credentials obtained by JSON-serializing the result of the navigator.credentials.create() JavaScript function.
- /// The original passkey creation options provided to the browser.
///
/// A task object representing the asynchronous operation containing the .
///
- public virtual async Task PerformPasskeyAttestationAsync(string credentialJson, PasskeyCreationOptions options)
+ public virtual async Task PerformPasskeyAttestationAsync(string credentialJson)
{
ThrowIfNoPasskeyHandler();
ArgumentException.ThrowIfNullOrEmpty(credentialJson);
- ArgumentNullException.ThrowIfNull(options);
- var context = new PasskeyAttestationContext
+ var passkeyInfo = await RetrievePasskeyAuthenticationInfoAsync()
+ ?? throw new InvalidOperationException(
+ "No passkey attestation is underway. " +
+ $"Make sure to call '{nameof(SignInManager<>)}.{nameof(MakePasskeyCreationOptionsAsync)}()' to initiate a passkey attestation.");
+ if (!string.Equals(PasskeyOperations.Attestation, passkeyInfo.Operation, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException(
+ $"Expected passkey operation '{PasskeyOperations.Attestation}', but got '{passkeyInfo.Operation}'. " +
+ $"This may indicate that you have not previously called '{nameof(SignInManager<>)}.{nameof(MakePasskeyCreationOptionsAsync)}()'.");
+ }
+ var context = new PasskeyAttestationContext
{
CredentialJson = credentialJson,
- OriginalOptionsJson = options.AsJson(),
- UserManager = UserManager,
+ AttestationState = passkeyInfo.State,
HttpContext = Context,
};
- var result = await _passkeyHandler.PerformAttestationAsync(context).ConfigureAwait(false);
+ var result = await _passkeyHandler.PerformAttestationAsync(context);
if (!result.Succeeded)
{
Logger.LogDebug(EventIds.PasskeyAttestationFailed, "Passkey attestation failed: {message}", result.Failure.Message);
@@ -495,26 +509,38 @@ public virtual async Task PerformPasskeyAttestationAsy
}
///
- /// Performs passkey assertion for the given and .
+ /// Performs passkey assertion for the given .
///
+ ///
+ /// The should be obtained by JSON-serializing the result of the
+ /// navigator.credentials.get() JavaScript API. The argument to navigator.credentials.get()
+ /// should be obtained by calling .
+ /// Upon success, the should be stored on the
+ /// using .
+ ///
/// The credentials obtained by JSON-serializing the result of the navigator.credentials.get() JavaScript function.
- /// The original passkey creation options provided to the browser.
///
/// A task object representing the asynchronous operation containing the .
///
- public virtual async Task> PerformPasskeyAssertionAsync(string credentialJson, PasskeyRequestOptions options)
+ public virtual async Task> PerformPasskeyAssertionAsync(string credentialJson)
{
ThrowIfNoPasskeyHandler();
ArgumentException.ThrowIfNullOrEmpty(credentialJson);
- ArgumentNullException.ThrowIfNull(options);
- var user = options.UserId is { Length: > 0 } userId ? await UserManager.FindByIdAsync(userId) : null;
- var context = new PasskeyAssertionContext
+ var passkeyInfo = await RetrievePasskeyAuthenticationInfoAsync()
+ ?? throw new InvalidOperationException(
+ "No passkey assertion is underway. " +
+ $"Make sure to call '{nameof(SignInManager<>)}.{nameof(MakePasskeyRequestOptionsAsync)}()' to initiate a passkey assertion.");
+ if (!string.Equals(PasskeyOperations.Assertion, passkeyInfo.Operation, StringComparison.Ordinal))
+ {
+ throw new InvalidOperationException(
+ $"Expected passkey operation '{PasskeyOperations.Assertion}', but got '{passkeyInfo.Operation}'. " +
+ $"This may indicate that you have not previously called '{nameof(SignInManager<>)}.{nameof(MakePasskeyRequestOptionsAsync)}()'.");
+ }
+ var context = new PasskeyAssertionContext
{
- User = user,
CredentialJson = credentialJson,
- OriginalOptionsJson = options.AsJson(),
- UserManager = UserManager,
+ AssertionState = passkeyInfo.State,
HttpContext = Context,
};
var result = await _passkeyHandler.PerformAssertionAsync(context);
@@ -526,30 +552,24 @@ public virtual async Task> PerformPasskeyAssertion
return result;
}
- [MemberNotNull(nameof(_passkeyHandler))]
- private void ThrowIfNoPasskeyHandler()
- {
- if (_passkeyHandler is null)
- {
- throw new InvalidOperationException(
- $"This operation requires an {nameof(IPasskeyHandler<>)} service to be registered.");
- }
- }
-
///
/// Performs a passkey assertion and attempts to sign in the user.
///
+ ///
+ /// The should be obtained by JSON-serializing the result of the
+ /// navigator.credentials.get() JavaScript API. The argument to navigator.credentials.get()
+ /// should be obtained by calling .
+ ///
/// The credentials obtained by JSON-serializing the result of the navigator.credentials.get() JavaScript function.
- /// The original passkey request options provided to the browser.
///
/// The task object representing the asynchronous operation containing the
/// for the sign-in attempt.
///
- public virtual async Task PasskeySignInAsync(string credentialJson, PasskeyRequestOptions options)
+ public virtual async Task PasskeySignInAsync(string credentialJson)
{
ArgumentException.ThrowIfNullOrEmpty(credentialJson);
- var assertionResult = await PerformPasskeyAssertionAsync(credentialJson, options);
+ var assertionResult = await PerformPasskeyAssertionAsync(credentialJson);
if (!assertionResult.Succeeded)
{
return SignInResult.Failed;
@@ -566,250 +586,52 @@ public virtual async Task PasskeySignInAsync(string credentialJson
return await SignInOrTwoFactorAsync(assertionResult.User, isPersistent: false, bypassTwoFactor: true);
}
- ///
- /// Generates a and stores it in the current for later retrieval.
- ///
- /// Args for configuring the .
- ///
- /// The returned options should be passed to the navigator.credentials.create() JavaScript function.
- /// The credentials returned from that function can then be passed to the .
- ///
- ///
- /// A task object representing the asynchronous operation containing the .
- ///
- public virtual async Task ConfigurePasskeyCreationOptionsAsync(PasskeyCreationArgs creationArgs)
- {
- ArgumentNullException.ThrowIfNull(creationArgs);
-
- var options = await GeneratePasskeyCreationOptionsAsync(creationArgs);
-
- var props = new AuthenticationProperties();
- props.Items[PasskeyCreationOptionsKey] = options.AsJson();
- var claimsIdentity = new ClaimsIdentity(IdentityConstants.TwoFactorUserIdScheme);
- claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, options.UserEntity.Id));
- claimsIdentity.AddClaim(new Claim(ClaimTypes.Email, options.UserEntity.Name));
- claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, options.UserEntity.DisplayName));
- var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
- await Context.SignInAsync(IdentityConstants.TwoFactorUserIdScheme, claimsPrincipal, props);
-
- return options;
- }
-
- ///
- /// Generates a to create a new passkey for a user.
- ///
- /// Args for configuring the .
- ///
- /// The returned options should be passed to the navigator.credentials.create() JavaScript function.
- /// The credentials returned from that function can then be passed to the .
- ///
- ///
- /// A task object representing the asynchronous operation containing the .
- ///
- public virtual async Task GeneratePasskeyCreationOptionsAsync(PasskeyCreationArgs creationArgs)
+ [MemberNotNull(nameof(_passkeyHandler))]
+ private void ThrowIfNoPasskeyHandler()
{
- ArgumentNullException.ThrowIfNull(creationArgs);
-
- var excludeCredentials = await GetExcludeCredentialsAsync();
- var serverDomain = Options.Passkey.ServerDomain ?? Context.Request.Host.Host;
- var rpEntity = new PublicKeyCredentialRpEntity
- {
- Name = serverDomain,
- Id = serverDomain,
- };
- var userEntity = new PublicKeyCredentialUserEntity
- {
- Id = BufferSource.FromString(creationArgs.UserEntity.Id),
- Name = creationArgs.UserEntity.Name,
- DisplayName = creationArgs.UserEntity.DisplayName,
- };
- var challenge = RandomNumberGenerator.GetBytes(Options.Passkey.ChallengeSize);
- var options = new PublicKeyCredentialCreationOptions
- {
- Rp = rpEntity,
- User = userEntity,
- Challenge = BufferSource.FromBytes(challenge),
- Timeout = (uint)Options.Passkey.Timeout.TotalMilliseconds,
- ExcludeCredentials = excludeCredentials,
- PubKeyCredParams = PublicKeyCredentialParameters.AllSupportedParameters,
- AuthenticatorSelection = creationArgs.AuthenticatorSelection,
- Attestation = creationArgs.Attestation,
- Extensions = creationArgs.Extensions,
- };
- var optionsJson = JsonSerializer.Serialize(options, IdentityJsonSerializerContext.Default.PublicKeyCredentialCreationOptions);
- return new(creationArgs.UserEntity, optionsJson);
-
- async Task GetExcludeCredentialsAsync()
+ if (_passkeyHandler is null)
{
- var existingUser = await UserManager.FindByIdAsync(creationArgs.UserEntity.Id);
- if (existingUser is null)
- {
- return [];
- }
-
- var passkeys = await UserManager.GetPasskeysAsync(existingUser);
- var excludeCredentials = passkeys
- .Select(p => new PublicKeyCredentialDescriptor
- {
- Type = "public-key",
- Id = BufferSource.FromBytes(p.CredentialId),
- Transports = p.Transports ?? [],
- });
- return [.. excludeCredentials];
+ throw new InvalidOperationException(
+ $"This operation requires an {nameof(IPasskeyHandler<>)} service to be registered.");
}
}
- ///
- /// Generates a and stores it in the current for later retrieval.
- ///
- /// Args for configuring the .
- ///
- /// The returned options should be passed to the navigator.credentials.get() JavaScript function.
- /// The credentials returned from that function can then be passed to the or
- /// methods.
- ///
- ///
- /// A task object representing the asynchronous operation containing the .
- ///
- public virtual async Task ConfigurePasskeyRequestOptionsAsync(PasskeyRequestArgs requestArgs)
+ private async Task StorePasskeyAuthenticationInfoAsync(string operation, string? state)
{
- ArgumentNullException.ThrowIfNull(requestArgs);
-
- var options = await GeneratePasskeyRequestOptionsAsync(requestArgs);
-
var props = new AuthenticationProperties();
- props.Items[PasskeyRequestOptionsKey] = options.AsJson();
+ props.Items[PasskeyOperationKey] = operation;
+ props.Items[PasskeyStateKey] = state;
var claimsIdentity = new ClaimsIdentity(IdentityConstants.TwoFactorUserIdScheme);
-
- if (options.UserId is { } userId)
- {
- claimsIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userId));
- }
-
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await Context.SignInAsync(IdentityConstants.TwoFactorUserIdScheme, claimsPrincipal, props);
- return options;
}
- ///
- /// Generates a to request an existing passkey for a user.
- ///
- /// Args for configuring the .
- ///
- /// The returned options should be passed to the navigator.credentials.get() JavaScript function.
- /// The credentials returned from that function can then be passed to the method.
- ///
- ///
- /// A task object representing the asynchronous operation containing the .
- ///
- public virtual async Task GeneratePasskeyRequestOptionsAsync(PasskeyRequestArgs requestArgs)
+ private async Task RetrievePasskeyAuthenticationInfoAsync()
{
- ArgumentNullException.ThrowIfNull(requestArgs);
-
- var allowCredentials = await GetAllowCredentialsAsync();
- var serverDomain = Options.Passkey.ServerDomain ?? Context.Request.Host.Host;
- var challenge = RandomNumberGenerator.GetBytes(Options.Passkey.ChallengeSize);
- var options = new PublicKeyCredentialRequestOptions
- {
- Challenge = BufferSource.FromBytes(challenge),
- RpId = serverDomain,
- Timeout = (uint)Options.Passkey.Timeout.TotalMilliseconds,
- AllowCredentials = allowCredentials,
- UserVerification = requestArgs.UserVerification,
- Extensions = requestArgs.Extensions,
- };
- var userId = requestArgs?.User is { } user
- ? await UserManager.GetUserIdAsync(user).ConfigureAwait(false)
- : null;
- var optionsJson = JsonSerializer.Serialize(options, IdentityJsonSerializerContext.Default.PublicKeyCredentialRequestOptions);
- return new(userId, optionsJson);
+ return _passkeyInfo ??= await RetrievePasskeyInfoCoreAsync();
- async Task GetAllowCredentialsAsync()
+ async Task RetrievePasskeyInfoCoreAsync()
{
- if (requestArgs?.User is not { } user)
+ var result = await Context.AuthenticateAsync(IdentityConstants.TwoFactorUserIdScheme);
+ await Context.SignOutAsync(IdentityConstants.TwoFactorUserIdScheme);
+
+ if (result.Properties is not { } properties)
{
- return [];
+ return null;
}
- var passkeys = await UserManager.GetPasskeysAsync(user);
- var allowCredentials = passkeys
- .Select(p => new PublicKeyCredentialDescriptor
- {
- Type = "public-key",
- Id = BufferSource.FromBytes(p.CredentialId),
- Transports = p.Transports ?? [],
- });
- return [.. allowCredentials];
- }
- }
-
- ///
- /// Retrieves the stored in the current .
- ///
- ///
- /// A task object representing the asynchronous operation containing the .
- ///
- public virtual async Task RetrievePasskeyCreationOptionsAsync()
- {
- if (_passkeyCreationOptions is not null)
- {
- return _passkeyCreationOptions;
- }
-
- var result = await Context.AuthenticateAsync(IdentityConstants.TwoFactorUserIdScheme);
- await Context.SignOutAsync(IdentityConstants.TwoFactorUserIdScheme);
-
- if (result?.Principal == null || result.Properties is not { } properties)
- {
- return null;
- }
-
- if (!properties.Items.TryGetValue(PasskeyCreationOptionsKey, out var optionsJson) || optionsJson is null)
- {
- return null;
- }
-
- if (result.Principal.FindFirstValue(ClaimTypes.NameIdentifier) is not { Length: > 0 } userId ||
- result.Principal.FindFirstValue(ClaimTypes.Email) is not { Length: > 0 } userName ||
- result.Principal.FindFirstValue(ClaimTypes.Name) is not { Length: > 0 } userDisplayName)
- {
- return null;
- }
-
- var userEntity = new PasskeyUserEntity(userId, userName, userDisplayName);
- _passkeyCreationOptions = new(userEntity, optionsJson);
- return _passkeyCreationOptions;
- }
-
- ///
- /// Retrieves the stored in the current .
- ///
- ///
- /// A task object representing the asynchronous operation containing the .
- ///
- public virtual async Task RetrievePasskeyRequestOptionsAsync()
- {
- if (_passkeyRequestOptions is not null)
- {
- return _passkeyRequestOptions;
- }
-
- var result = await Context.AuthenticateAsync(IdentityConstants.TwoFactorUserIdScheme);
- await Context.SignOutAsync(IdentityConstants.TwoFactorUserIdScheme);
-
- if (result?.Principal == null || result.Properties is not { } properties)
- {
- return null;
- }
+ if (!properties.Items.TryGetValue(PasskeyOperationKey, out var operation) ||
+ !properties.Items.TryGetValue(PasskeyStateKey, out var state))
+ {
+ return null;
+ }
- if (!properties.Items.TryGetValue(PasskeyRequestOptionsKey, out var optionsJson) || optionsJson is null)
- {
- return null;
+ return new()
+ {
+ Operation = operation,
+ State = state,
+ };
}
-
- var userId = result.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
- _passkeyRequestOptions = new(userId, optionsJson);
- return _passkeyRequestOptions;
}
///
@@ -1403,4 +1225,17 @@ internal sealed class TwoFactorAuthenticationInfo
public required TUser User { get; init; }
public string? LoginProvider { get; init; }
}
+
+ internal sealed class PasskeyAuthenticationInfo
+ {
+ public required string? Operation { get; init; }
+ public required string? State { get; init; }
+
+ }
+
+ private static class PasskeyOperations
+ {
+ public const string Attestation = "Attestation";
+ public const string Assertion = "Assertion";
+ }
}
diff --git a/src/Identity/EntityFrameworkCore/src/UserOnlyStore.cs b/src/Identity/EntityFrameworkCore/src/UserOnlyStore.cs
index 9135b94d309f..f798c06f42c6 100644
--- a/src/Identity/EntityFrameworkCore/src/UserOnlyStore.cs
+++ b/src/Identity/EntityFrameworkCore/src/UserOnlyStore.cs
@@ -758,7 +758,8 @@ private void ThrowIfPasskeysNotSupported()
throw new InvalidOperationException(
$"This operation is not permitted because the underlying '{nameof(DbContext)}' does not include '{typeof(TUserPasskey).Name}' in its model. " +
$"When using '{nameof(IdentityDbContext)}', make sure that '{nameof(IdentityOptions)}.{nameof(IdentityOptions.Stores)}.{nameof(StoreOptions.SchemaVersion)}' " +
- $"is set to '{nameof(IdentitySchemaVersions)}.{nameof(IdentitySchemaVersions.Version3)}' or higher.");
+ $"is set to '{nameof(IdentitySchemaVersions)}.{nameof(IdentitySchemaVersions.Version3)}' or higher. " +
+ $"See https://aka.ms/aspnet/passkeys for more information.");
}
}
}
diff --git a/src/Identity/EntityFrameworkCore/src/UserStore.cs b/src/Identity/EntityFrameworkCore/src/UserStore.cs
index 0165b2fc01e4..4de45e258787 100644
--- a/src/Identity/EntityFrameworkCore/src/UserStore.cs
+++ b/src/Identity/EntityFrameworkCore/src/UserStore.cs
@@ -138,6 +138,8 @@ public class UserStore, new()
where TUserPasskey : IdentityUserPasskey, new()
{
+ private bool? _dbContextSupportsPasskeys;
+
///
/// Creates a new instance of the store.
///
@@ -160,7 +162,14 @@ public class UserStore UserRoles { get { return Context.Set(); } }
private DbSet UserLogins { get { return Context.Set(); } }
private DbSet UserTokens { get { return Context.Set(); } }
- private DbSet UserPasskeys { get { return Context.Set(); } }
+ private DbSet UserPasskeys
+ {
+ get
+ {
+ ThrowIfPasskeysNotSupported();
+ return Context.Set();
+ }
+ }
///
/// Gets or sets a flag indicating if changes should be persisted after CreateAsync, UpdateAsync and DeleteAsync are called.
@@ -879,4 +888,22 @@ public virtual async Task RemovePasskeyAsync(TUser user, byte[] credentialId, Ca
await SaveChanges(cancellationToken).ConfigureAwait(false);
}
}
+
+ private void ThrowIfPasskeysNotSupported()
+ {
+ if (_dbContextSupportsPasskeys == true)
+ {
+ return;
+ }
+
+ _dbContextSupportsPasskeys ??= Context.Model.FindEntityType(typeof(TUserPasskey)) is not null;
+ if (_dbContextSupportsPasskeys == false)
+ {
+ throw new InvalidOperationException(
+ $"This operation is not permitted because the underlying '{nameof(DbContext)}' does not include '{typeof(TUserPasskey).Name}' in its model. " +
+ $"When using '{nameof(IdentityDbContext)}', make sure that '{nameof(IdentityOptions)}.{nameof(IdentityOptions.Stores)}.{nameof(StoreOptions.SchemaVersion)}' " +
+ $"is set to '{nameof(IdentitySchemaVersions)}.{nameof(IdentitySchemaVersions.Version3)}' or higher. " +
+ $"See https://aka.ms/aspnet/passkeys for more information.");
+ }
+ }
}
diff --git a/src/Identity/Extensions.Core/src/IdentityOptions.cs b/src/Identity/Extensions.Core/src/IdentityOptions.cs
index 458d46f16a96..57ab10a6f9c1 100644
--- a/src/Identity/Extensions.Core/src/IdentityOptions.cs
+++ b/src/Identity/Extensions.Core/src/IdentityOptions.cs
@@ -32,14 +32,6 @@ public class IdentityOptions
///
public PasswordOptions Password { get; set; } = new PasswordOptions();
- ///
- /// Gets or sets the for the identity system.
- ///
- ///
- /// The for the identity system.
- ///
- public PasskeyOptions Passkey { get; set; } = new PasskeyOptions();
-
///
/// Gets or sets the for the identity system.
///
diff --git a/src/Identity/Extensions.Core/src/PasskeyOptions.cs b/src/Identity/Extensions.Core/src/PasskeyOptions.cs
deleted file mode 100644
index e274a3c3762a..000000000000
--- a/src/Identity/Extensions.Core/src/PasskeyOptions.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Microsoft.AspNetCore.Identity;
-
-///
-/// Specifies options for passkey requirements.
-///
-public class PasskeyOptions
-{
- ///
- /// Gets or sets the time that the server is willing to wait for a passkey operation to complete.
- ///
- ///
- /// The default value is 1 minute.
- /// See
- /// and .
- ///
- public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(1);
-
- ///
- /// The size of the challenge in bytes sent to the client during WebAuthn attestation and assertion.
- ///
- ///
- /// The default value is 16 bytes.
- /// See
- /// and .
- ///
- public int ChallengeSize { get; set; } = 16;
-
- ///
- /// The effective domain of the server. Should be unique and will be used as the identity for the server.
- ///
- ///
- /// If left , the server's origin may be used instead.
- /// See .
- ///
- public string? ServerDomain { get; set; }
-
- ///
- /// Gets or sets the allowed origins for credential registration and assertion.
- /// When specified, these origins are explicitly allowed in addition to any origins allowed by other settings.
- ///
- public IList AllowedOrigins { get; set; } = [];
-
- ///
- /// Gets or sets whether the current server's origin should be allowed for credentials.
- /// When true, the origin of the current request will be automatically allowed.
- ///
- ///
- /// The default value is