Passkeys in ASP.NET Core Identity: Common Mistakes with Relying Party IDs, Schema Migrations, Secure Contexts & Preview APIs

Verified against .NET 10 and the RTM ASP.NET Core Identity passkey APIs. Last reviewed: July 23, 2026. Preview-era names and copied WebAuthn configuration are a common source of broken passkey flows.

The Browser Supports Passkeys—So Why Does the App Still Fail?

Passkey failures are often blamed on the browser or the user's authenticator. In ASP.NET Core Identity, the more common cause is server configuration that does not match the environment where the credential is being used. The browser is enforcing WebAuthn rules correctly; the application has supplied the wrong identity, origin, schema, or API flow.

The usual symptoms are confusing: registration works on localhost but sign-in fails in production, the passkey UI appears but storage throws a database error, navigator.credentials.create() is rejected on an HTTP test host, or copied server code references methods that no longer exist. This article explains the mistakes behind those failures and the exact direction for correcting them.

Mistake 1: Treating the Relying Party ID Like an Ordinary Setting

The Relying Party ID is the identity of your web application in WebAuthn. ASP.NET Core exposes it through IdentityPasskeyOptions.ServerDomain. A passkey is cryptographically scoped to that domain, so a credential created for localhost, app.example.com, or example.com is not freely portable to an unrelated hostname.

A common mistake is enrolling passkeys before the production domain is settled, then changing ServerDomain later. Existing credentials can become unusable because the browser correctly refuses to present a credential outside its RP-ID scope. Another mistake is leaving the value unset behind a proxy that accepts arbitrary host headers. When ASP.NET Core infers the domain from the host header, weak host validation can widen the credential scope in unsafe ways.

Set the RP ID explicitly when strict domain control matters, validate host headers, and keep the value stable after users begin registering passkeys. Use environment-specific configuration rather than hard-coding localhost in production.

Program.cs — Relying Party ID
builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    // WRONG in production:
    // options.ServerDomain = "localhost";

    // FIX: use the stable registrable domain for this environment.
    options.ServerDomain =
        builder.Configuration["Passkeys:ServerDomain"];

    options.AuthenticatorTimeout = TimeSpan.FromMinutes(3);
    options.ChallengeSize = 32;
});

// appsettings.Production.json
// {
//   "Passkeys": {
//     "ServerDomain": "accounts.example.com"
//   }
// }

Mistake 2: Enabling the UI but Forgetting Identity Schema Version 3

An upgraded Identity application can compile and display passkey controls while the database is still on the older schema. ASP.NET Core Identity passkey storage requires IdentitySchemaVersions.Version3, followed by an EF Core migration that creates the passkey table and related mapping.

Changing options.Stores.SchemaVersion only changes the model used by the application. It does not alter an existing database. If the migration is skipped, registration eventually fails when Identity tries to persist the credential. This is especially easy to miss when code is copied from the .NET 10 Blazor template, because a newly created project already contains the expected setup.

Set the schema version, create a named migration such as AddPasskeySupport, inspect it, and apply it to every environment before exposing the passkey UI.

Upgrade warning: Do not deploy the passkey UI before the database migration. A partially upgraded application creates a sign-up path that users can enter but cannot complete.
IdentitySchemaUpgrade.cs
builder.Services
    .AddIdentityCore<ApplicationUser>(options =>
    {
        options.SignIn.RequireConfirmedAccount = true;

        // Required for ASP.NET Core Identity passkey storage.
        options.Stores.SchemaVersion =
            IdentitySchemaVersions.Version3;
    })
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddSignInManager()
    .AddDefaultTokenProviders();

// Then create and apply the database migration:
//
// dotnet ef migrations add AddPasskeySupport
// dotnet ef database update
//
// Inspect the migration and confirm that it creates
// the AspNetUserPasskeys table for your provider.

Mistake 3: Testing WebAuthn on an Insecure Origin

Passkeys depend on the browser's WebAuthn API, and WebAuthn requires a secure context. Production passkey operations must run over HTTPS. localhost is normally treated as a secure development exception, but an arbitrary HTTP hostname, a raw LAN address, or a poorly configured staging URL can be rejected before the request reaches your ASP.NET Core endpoint.

This often looks like a JavaScript problem because navigator.credentials.create() or navigator.credentials.get() fails in the browser. Changing the C# endpoint cannot fix an origin the browser considers insecure. Use the development HTTPS certificate locally, deploy staging and production behind valid TLS, and avoid mixing HTTP and HTTPS origins during registration and sign-in.

Also ensure the public origin seen by the browser matches the domain the server uses for passkey options. Reverse proxies must forward the correct scheme and host, and the application must process forwarded headers safely.

SecureContextCheck.js
if (!window.isSecureContext) {
    throw new Error(
        "Passkeys require HTTPS. Use localhost for local development " +
        "or serve this application through a valid TLS endpoint.");
}

if (!window.PublicKeyCredential) {
    throw new Error("This browser does not expose the WebAuthn API.");
}

const credential = await navigator.credentials.create({
    publicKey: creationOptions
});

Mistake 4: Following Preview-Era Method Names

Passkey support changed during the .NET 10 preview cycle, and older articles can show method names that never reached the stable API. The clearest example is ConfigurePasskeyCreationOptionsAsync, which was replaced by MakePasskeyCreationOptionsAsync before RTM.

The current registration flow creates options with MakePasskeyCreationOptionsAsync, verifies the browser response with PerformPasskeyAttestationAsync, and stores the resulting passkey with AddOrUpdatePasskeyAsync. The authentication flow uses MakePasskeyRequestOptionsAsync and then PasskeySignInAsync for the complete sign-in operation.

If a copied sample adds an AddPasskeys() extension or calls SignInWithPasskeyAsync, confirm that it is not a third-party library or an older preview. Compare it with the .NET 10 API reference before adapting the rest of the page.

CurrentPasskeyApi.cs
// STALE PREVIEW NAME
// await signInManager.ConfigurePasskeyCreationOptionsAsync(userEntity);

// CURRENT REGISTRATION FLOW
string creationOptionsJson =
    await signInManager.MakePasskeyCreationOptionsAsync(userEntity);

PasskeyAttestationResult attestation =
    await signInManager.PerformPasskeyAttestationAsync(credentialJson);

IdentityResult stored =
    await userManager.AddOrUpdatePasskeyAsync(
        user,
        attestation.Passkey);

// CURRENT AUTHENTICATION FLOW
string requestOptionsJson =
    await signInManager.MakePasskeyRequestOptionsAsync(user);

SignInResult result =
    await signInManager.PasskeySignInAsync(assertionJson);

Mistake 5: Assuming Built-In Passkeys Are a Complete WebAuthn Platform

ASP.NET Core Identity's passkey support is deliberately focused on authentication. It covers common registration and sign-in scenarios, but it is not a general-purpose FIDO2 governance framework. In particular, attestation statements are not validated by default.

For a normal consumer application, that default may be appropriate. For an enterprise environment that must allow only approved authenticators, verify AAGUIDs, maintain certificate trust stores, or enforce metadata policies, the built-in flow is not enough by itself. Use the provided validation extensibility carefully or add a maintained full WebAuthn library after a security review.

Do not advertise an authenticator allow-list simply because the application can register and verify a passkey. Authentication and authenticator governance are separate capabilities.

Scope reminder: Only the Blazor Web App template currently supplies passkey endpoints and UI by default. MVC and Razor Pages applications must port or implement those pieces explicitly.
PasskeyOptionsLimits.cs
builder.Services.Configure<IdentityPasskeyOptions>(options =>
{
    options.ServerDomain = "accounts.example.com";

    // Default ASP.NET Core Identity behavior does not validate
    // authenticator attestation statements.
    //
    // Add custom validation only when your security policy
    // genuinely requires authenticator verification.
    options.VerifyAttestationStatement = async context =>
    {
        bool trusted = await attestationPolicy
            .ValidateAsync(context, context.HttpContext.RequestAborted);

        return trusted;
    };
});

How to Tell If a Passkey Setup Is Ready

Before asking users to enroll credentials, verify the whole path rather than only the browser prompt. The production RP ID must be final, HTTPS must be working, forwarded host and scheme information must be correct, Identity schema version 3 must be migrated, and the server code must use the stable .NET 10 APIs.

Test registration and sign-in on the actual deployment hostname. Then add a second credential, sign out, and repeat the assertion flow. A local-only test does not reveal RP-ID or proxy mistakes that appear after deployment.

PasskeyReadinessChecklist.txt
[ ] IdentitySchemaVersions.Version3 is configured
[ ] AddPasskeySupport migration is applied
[ ] AspNetUserPasskeys exists in the target database
[ ] Production uses HTTPS
[ ] ServerDomain matches the deployed RP ID
[ ] Host headers and forwarded headers are validated
[ ] Code uses MakePasskeyCreationOptionsAsync
[ ] Code uses PerformPasskeyAttestationAsync
[ ] Code uses MakePasskeyRequestOptionsAsync / PasskeySignInAsync
[ ] Recovery method exists before passwords are removed
[ ] Attestation limitations are documented

What Developers Want to Know

Why does my passkey work on localhost but fail after deployment?

The most common cause is a Relying Party ID mismatch. A credential created for localhost or one domain cannot be used for an unrelated production domain. Configure IdentityPasskeyOptions.ServerDomain correctly for each environment before users enroll credentials.

Why is the AspNetUserPasskeys table missing?

Existing Identity applications must opt into IdentitySchemaVersions.Version3 and create an EF Core migration. Changing the option alone does not modify the database. Run dotnet ef migrations add AddPasskeySupport and then update the database.

Do passkeys work over plain HTTP during development?

WebAuthn requires a secure context. Production must use HTTPS. Browsers normally treat localhost as a secure development exception, but an arbitrary HTTP host or LAN IP may be rejected.

Why does ConfigurePasskeyCreationOptionsAsync not exist?

That name appeared in preview-era material. The .NET 10 RTM API uses MakePasskeyCreationOptionsAsync. Current flows also use PerformPasskeyAttestationAsync, MakePasskeyRequestOptionsAsync, and PasskeySignInAsync.

Back to Articles