Skip to content

IKRC Insights

Passkeys for a Customer Portal: Choose the Domain Before Anyone Enrolls

ASP.NET Core Identity takes a passkey's relying party ID from the enrollment page's host unless ServerDomain is set, and passkeys enrolled under one ID stop working when the portal asks for another.

Take a hypothetical parts distributor whose dealers order through a customer portal at portal.example.com, built on ASP.NET Core Identity with email and password sign-in. ASP.NET Core in .NET 10 added passkey support to Identity, so the team plans an "Add a passkey" button on the account page for the next release. Later, it plans to move sign-in onto the main site at www.example.com. If the first release leaves the Identity option ServerDomain unset, every passkey a dealer enrolls takes its relying party ID from the portal's host name, and that ID decides whether the passkey survives the move.

What ServerDomain decides when nobody sets it

Every passkey belongs to one domain name, its relying party ID, or RP ID. The WebAuthn Level 3 specification, a W3C Recommendation since August 25, 2026, states: "A public key credential can only be used for authentication with the same entity (as identified by RP ID) it was registered with." By default the browser uses the domain of the page making the request, and a site may name a broader one only if it is a registrable suffix of that domain. A page on login.example.com can register passkeys under login.example.com or example.com, but cannot use m.login.example.com.

ASP.NET Core Identity exposes that choice as IdentityPasskeyOptions.ServerDomain. Microsoft's passkeys page says the default is null, and that when it is null "the implementation uses the host header to determine the domain. This means that the page on which the passkey was registered controls the domain for that credential." A passkey registered on app.contoso.com "also works on *.app.contoso.com". In the hypothetical, a dealer who adds a passkey on portal.example.com gets a credential whose RP ID is portal.example.com. Without Related Origin Requests, described below, a sign-in page on www.example.com can ask only for www.example.com or example.com, and the browser has no passkey under either name to offer.

Suppose the team then sets ServerDomain to example.com so that both hosts share one RP ID. The passkeys already enrolled keep their old one. The same Microsoft page says the setting "must match the domain where the passkey was originally registered, or authentication fails", and the specification has the server check that the signed response carries a hash of the RP ID the server expects. Once every sign-in page asks only for example.com, a dealer's phone or password manager still holds a passkey for portal.example.com that the new request cannot use. A dealer who is still signed in can add a new passkey from the account page. One whose session has ended signs in with a password or a recovery method and enrolls again, and one who has removed the password and set up no recovery method is locked out until support restores access.

The specification has one mechanism for using an RP ID from other domains. Under Related Origin Requests, the site publishes a JSON file at the RP ID's /.well-known/webauthn address listing the extra origins, and every origin in the list keeps using that one RP ID. No credential moves to a new one. A browser that supports the feature must accept at least five registrable origin labels and should cap the number by its own policy. Support is optional, and a browser without it rejects the request. The application also has to keep asking for the original RP ID and accept the listed origins. Microsoft's passkeys page does not mention the feature, so publishing the file and widening the origin check would be the portal's own work in Identity. Applied to the distributor, the portal could keep portal.example.com as its RP ID from www.example.com, for dealers whose browsers support the feature.

Leaving ServerDomain empty also ties the RP ID to whatever Host header reaches the application. Microsoft's page says the hosting environment "must validate host headers to prevent credential-scoping attacks", and asks for an explicit AllowedHosts list when forwarded headers middleware is in use. Setting ServerDomain explicitly removes the inference, and the time to set it is before the first dealer enrolls.

Should passkeys cover example.com or only the portal?

With example.com as the RP ID, a page on any subdomain of example.com can request the dealer's passkey, which is what would let sign-in move to www.example.com later. The server's origin check then decides whether to accept the result, and Identity's default check "allows requests from subdomains and disallows cross-origin iframes". The specification warns that user-submitted code hosted on a subdomain "could exercise any credentials scoped to the RP ID", and says a relying party by default "SHOULD NOT allow a subdomain origin when verifying the assertion".

In the hypothetical, www.example.com runs a hosted marketing site with third-party tag scripts, and help.example.com is a vendor's knowledge base. Microsoft's requirement covers both: "Don't serve untrusted content on any subdomain within the ServerDomain scope. If you can't guarantee this, implement custom origin validation to restrict passkey usage to specific origins." In Identity that is the ValidateOrigin callback, which reads context.Origin. For the distributor we would set ServerDomain to example.com and give ValidateOrigin an exact list holding https://portal.example.com, adding https://www.example.com only once the marketing scripts have left that host.

A portal that gives each customer company its own hostname, such as acme.portal.example.com, meets the same scope from the other side. One RP ID covers every tenant's hostname, so a successful passkey sign-in establishes which dealer is signing in. It does not establish which company's orders that dealer may open. The application still checks the dealer's company membership on every request, the boundary described in our article on multi-tenant software.

Recovery has to exist before an account goes passwordless

Microsoft's page calls account recovery "primarily a concern for apps that allow passkeys as the only authentication mechanism". The Blazor Web App template avoids the problem by requiring a password or an external login when the account is created. A portal that lets dealers delete their passwords takes on recovery itself. For that case the page suggests recovery codes, email-based recovery, requiring more than one passkey, and watching the IsBackedUp flag on UserPasskeyInfo to prompt users to add credentials.

That flag comes from WebAuthn. Each passkey reports whether it is eligible for backup, which never changes, and whether it is backed up now. A passkey that is not eligible is a single-device credential, such as one held on a hardware security key, and the specification says it "is not resilient to single device loss". A synced passkey can stop being backed up too, for example when the user turns off the backup service, and the specification asks the site to guide a user with no other credential through adding one. Identity stores both flags with each passkey, so the portal can show a dealer whose only passkey is not backed up a prompt to add a second one.

If a lost phone is answered with an emailed link, the dealer's mailbox becomes a way to sign in. For accounts that can place orders on credit, we would issue recovery codes and ask for a second passkey before the password can be removed. In the recovery flow we would build, a dealer who gets back in with a recovery code still has the lost passkey on the account and removes it from a page listing the account's credentials by the friendly names given at registration. That page is the specification's decommissioning example, and UserPasskeyInfo.Name holds the name. A dealer with no recovery code left calls support. Staff confirm the caller with the dealer company's account administrator, then send a single-use, short-lived enrollment link to the address on file, which lets the dealer register a new passkey and collect new recovery codes. Removing the lost passkey is a separate action on the staff screen, and the screen logs the link and the removal against the staff member who made each.

What ASP.NET Core Identity leaves to the application

Whether the portal must limit which authenticator models dealers can use decides how much attestation work it takes on. Identity "doesn't validate attestation statements by default", which Microsoft calls "suitable for most consumer authentication scenarios". Identity stores each credential's AAGUID so a model later found to be vulnerable can be revoked, but "AAGUID reliability depends on whether your app validates attestation statements". Validating them through the VerifyAttestationStatement callback means maintaining trust stores for authenticator certificates, which we would take on only for a portal that must require a particular key.

Identity treats a passkey as a primary sign-in method and lists "No built-in 2FA support" among its limitations. A portal that already asks for a password and an authenticator-app code cannot add a passkey as the second step without custom work. That is a limit of Identity's integration and says nothing about the strength of a passkey. For the distributor, the open question is portal policy: whether a passkey sign-in alone is enough before a dealer places an order on credit or changes the account's delivery addresses.

Only the Blazor Web App template includes passkey support. A portal built with MVC or Razor Pages has to integrate its account UI and browser code with SignInManager methods such as MakePasskeyCreationOptionsAsync and PasskeySignInAsync, and to enforce the limits on passkeys per account and display-name length that the template enforces by default.

How IKRC would add passkeys to a portal that already has users

On a portal engagement we would start with a list: every hostname that serves the portal now, every hostname it may move to, and every subdomain under the candidate domain that serves content the portal team does not control. That list picks ServerDomain and the ValidateOrigin allow-list. Both go into configuration with two tests. One reads the creation options JSON from MakePasskeyCreationOptionsAsync and asserts that rp.id equals the chosen domain, so a later hosting change cannot move it unnoticed. The other sends an origin from outside the list and asserts that sign-in fails. The host-header and AllowedHosts settings get the same security review as the rest of the proxy configuration.

Passwords would stay as the backup method in the first release, with passkeys added to existing accounts, which Microsoft lists as a supported scenario. The .NET development work in that release is the Identity configuration with its two tests, plus the IsBackedUp prompt on the account page. Passwordless accounts would follow in a later release, once recovery codes, the second-passkey requirement and the staff-assisted re-enrollment flow are in place.

Related Reading

Multi-tenant software design covers where tenant scope leaks and what support needs on screen, which a passkey does not settle.

Contact IKRC

Your next software project.

Connection Lost

Attempting to reconnect to the server...