Skip to main content

Passkeys with the WebAuthn Extension

The partner sign-in page is the AccessOps · Partner sign-in: "Sign in to AccessOps", "Partner route · approved partner tenants only", the blue Sign in with a passkey button, Sign in with your organization (the Module 5 federation) and Use your AccessOps password (the Module 4 form, service accounts only), the grey "NO PASSKEY YET?" panel on the right with the lost-device note, a banner and the status strip STATUS … reference xxxx-xxxx. A passkey sign-in that passes every server-side check shows the records page with "Signed in as Dana Ellis. Your session was renewed." and "Passkey verified · grant issued"; every refusal shows "That passkey could not sign you in. Try again or use another sign-in method." (banner and an AlertBox with allowHtml: false). My passkeys (AccessOps · My passkeys) lists the signed-in user's own passkeys - Label · Added · Last used · Backup eligible - with Add a passkey, Rename, Delete, "Signed in as …" and the platform-authenticator hint.

Solution AccessOps.slnx = AccessOps/ (net10.0, Wisej-4 4.1.7, Wisej-4-WebAuthn 4.1.7, Microsoft.AspNetCore.Authentication.OpenIdConnect 10.0.14, Microsoft.Data.Sqlite 10.0.14) + AccessOps.TestIdentityProvider/ (Contoso Identity on 6416, test double) + AccessOps.Tests/ + PolicyHarness/. Started from a copy of Module 5. Data in AccessOpsData/ (git-ignored).

Run it​

cd "Security and Auth Patterns Course\Module 6"
$secret = "lab-only-" + [guid]::NewGuid().ToString("N")
dotnet user-secrets set "Clients:accessops:Secret" $secret --project AccessOps.TestIdentityProvider
dotnet user-secrets set "Oidc:ClientSecret" $secret --project AccessOps
dotnet build AccessOps.slnx
dotnet run --project AccessOps.TestIdentityProvider --urls http://localhost:6416 # terminal 1
dotnet run --project AccessOps --urls https://localhost:6406 # terminal 2
dotnet test AccessOps.slnx
dotnet run --project PolicyHarness

WebAuthn needs a secure context: https://localhost:6406 (with a trusted dev certificate) or http://localhost. The relying party is in appsettings.json → WebAuthn (RP ID localhost, exact origins). If your browser does not trust the dev certificate, use the Development verification listener of Modules 4–5 and add its origin to the passkey list for that run only:

$env:AccessOps__LocalHttpVerification = "true"; $env:AccessOps__AllowedOrigins__1 = "http://localhost:6446"
$env:WebAuthn__AllowedOrigins__1 = "http://localhost:6446"
dotnet run --project AccessOps --urls "https://localhost:6406;http://localhost:6446"
# provider terminal: Clients__accessops__RedirectUris__1 / PostLogoutRedirectUris__1 = http://localhost:6446/...

Without a real authenticator (Windows Hello, Touch ID, a security key) you can drive the ceremonies with Chrome's virtual authenticator (DevTools → More tools → WebAuthn → "Enable virtual authenticator environment", CTAP2, internal, resident key, user verification) - that is how the ceremonies were run on the course machine.

Demo accounts (invented, local only)​

AccountHow to sign inNotes
Dana Ellis - partner account A, tenant AContoso Identity dana.ellis@contoso.example / Demo-IdP-2026, then a passkeyDebug builds seed three display fixtures for her ("Work laptop · Windows Hello", "Security key · USB", "Phone · synced passkey"): their private keys were thrown away at seeding, so they can be renamed and deleted but can never sign in
Jo Newman - partner account B, tenant AContoso Identity jo.newman@contoso.example / Demo-IdP-2026account B of the A-for-B test
Kai Berg - tenant C (not approved)Contoso Identity kai.berg@northwind.examplerefused (Module 5, F08)
svc.reportsUse your AccessOps password → Reports-demo-31service account (Module 4 form)

What to try​

ActionExpected result
Open the sign-in pagethree buttons, "NO PASSKEY YET?", status "Ready · partner route"
Sign in with your organization → Dana → My passkeysthe three display fixtures; the hint says whether the browser offers a built-in authenticator (WebAuthn.IsUserVerifyingPlatformAuthenticatorAvailableAsync)
Add a passkey → name it → OK → authenticator promptnew row; banner Passkey "…" added.; log passkey.create … alg ES256 … stored
Select a row → Rename → "Old phone" → OKbanner Passkey renamed to "Old phone".; audit passkey.rename allowed
Select "Old phone" → Delete → Yesbanner Passkey "Old phone" deleted. It can no longer sign in.; audit passkey.delete allowed
Records → Sign out → Sign in with a passkey (outcome 1)"Signed in as Dana Ellis. Your session was renewed.", "Passkey verified · grant issued"; log UV=True (extension property UV=False) · counter 1→2 · commit=Updated and RefreshSessionId() · rotated=True · method=passkey
Sign in with a passkey with the page's origin removed from WebAuthn:AllowedOrigins (outcome 3)fixed sentence, reason=OriginRejected, no grant
Sign in with a passkey and cancel the prompt / no authenticator (outcome 5)fixed sentence (banner + AlertBox), reason=BrowserFailed, no grant
A's credential for B (outcome 2), replayed response (outcome 4), store racesdotnet test - PasskeyTests (see docs/passkeys.md)
An hour-old session → My passkeysno "Add a passkey"; hint "Sign in again to add a passkey"

Where this sample differs from the lesson or video​

  • UserVerified: the lesson's code reads r.AuthenticatorData.UserVerified. In Wisej-4-WebAuthn 4.1.7 that property comes from the wrong bit (bit 1 instead of bit 2) and is false for real authenticators; AccessOps reads UV (and UP) from the signed AuthenticatorData.Base64 flags byte.
  • WebAuthn.Validate for ES256: the lesson calls WebAuthn.Validate(c.Key, …, r.Signature) directly. The 4.1.7 extension rejects genuine ES256 signatures whose r or s is shorter than 32 bytes (about 1 in 128); AccessOps passes the same r and s re-encoded (Identity/Es256Signature.cs, see docs/passkeys.md).
  • "Partner account A": the banner says "Signed in as partner account A"; the sample's partner A is Dana Ellis (Module 5's partner), so the banner names her.
  • Banner after a passkey sign-in: it on the sign-in page; the Module 4 hand-off reloads the tab, so it appears on the records page after the session ID was renewed.
  • "Use your AccessOps password" opens the Module 4 form, which since Module 5 accepts service accounts only (svc.reports); the wording suggests partners use it too.
  • My passkeys: a Records button (to go back) was added to the mock's toolbar; the last grid column (free notes in the video: "this device", "lost on …") is empty - AccessOps stores no notes.
  • Display fixtures: the review page's three rows are Debug-only fixtures whose private keys do not exist (see Demo accounts).
  • ResidentKey "preferred" / attachment Platform are passed to CreateAsync (seven required arguments); how the extension serializes the attachment enum is not verified, nothing depends on it.