Skip to content

fix(webauthn)!: forward the access session at passkey enrollment - #155

Merged
Bccorb merged 2 commits into
mainfrom
fix/passkey-enrollment-access-identity
Sep 8, 2026
Merged

fix(webauthn)!: forward the access session at passkey enrollment#155
Bccorb merged 2 commits into
mainfrom
fix/passkey-enrollment-access-identity

Conversation

@Bccorb

@Bccorb Bccorb commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Coordinated with fells-code/seamless-auth-api#287. Neither can merge alone.

Summary

/webAuthn/register/start and /webAuthn/register/finish read the registration cookie and
sent the ephemeral token upstream. The auth API mints one of those for an account that
already exists, from an email address alone, so anyone who knew an address could enroll a
credential against the account and sign in as its owner. Both routes now read the access
cookie and forward the access token.

Changed in all three places the route table lives, so the adapters cannot drift:

  • packages/core/src/ensureCookies.ts cookie requirements
  • packages/express/src/createServer.ts proxy identity
  • packages/fastify/src/routes/proxyRoutes.ts proxy table

/webAuthn/login/start and /webAuthn/login/finish are unchanged and still take the
pre-auth cookie.

No shipped flow loses a step. Registration proves an address with an email OTP, and
verifying it issues a session, so the client holds an access cookie by the time it offers a
passkey. An application that offered enrollment before verifying an address has to move that
step after it.

finishRegisterHandler no longer issues session cookies

Enrolling a passkey is not a sign-in, and the caller now arrives holding a session, so
minting a second one left the first live and unrevoked while counting against the API's
concurrent session limit, which can evict the user's other devices. The auth API stopped
returning tokens here for the same reason. The route still answers 204.

Public API change: FinishRegisterOptions drops audience, cookieDomain,
accessCookieName and refreshCookieName, and FinishRegisterResult drops setCookies.
Only the two adapters in this workspace passed them and both are updated. Code calling
finishRegisterHandler directly should pass { authServerUrl } alone. Leaving required
options that do nothing seemed worse than the break, since the next reader would reasonably
assume enrollment still issues a session.

Lockstep

There is no safe release order between this and the auth API. An older adapter sends the
token the new API refuses, and this sends one an older API refuses, so enrollment answers
401 until both land. Merge and release with fells-code/seamless-auth-api#287 and
fells-code/seamless-auth-react#147.

Verification

pnpm -r build clean. Tests: core 246, express 156 (was 154), fastify 52 (was 48).

New coverage in both adapters: enrollment on an access cookie succeeds and forwards
Bearer access-token; a registration cookie is refused 401 with no upstream call. The
parity table gets both cases plus a 401 pinned by value, because a parity assertion alone
would pass just as happily if both adapters agreed on the wrong answer.

Reverting the Fastify identity and the core cookie table fails exactly those cases,
including the parity one.

Not in scope

The pre-auth routes' silent refresh still mints an access token into a cookie those routes
cannot use. That is what produced the typ mismatch in the original report, enrollment no
longer hits it, and passkey login still does. Filed separately as #154.

/webAuthn/register/start and /webAuthn/register/finish read the registration cookie and
sent the ephemeral token upstream. The auth API mints one of those for an account that
already exists, from an email address alone, so anyone who knew an address could enroll
a credential against the account and sign in as its owner. Both routes now read the
access cookie and forward the access token, in the core cookie table, the Express route
and the Fastify proxy table.

/webAuthn/login/start and /webAuthn/login/finish are unchanged and still take the
pre-auth cookie, because authenticating is what they are for.

No shipped flow loses a step. Registration proves an address with an email OTP, and
verifying it issues a session, so the client holds an access cookie by the time it
offers a passkey.

finishRegisterHandler no longer issues session cookies. Enrolling a passkey is not a
sign-in, and the caller now arrives holding a session, so minting a second one left the
first live and unrevoked while counting against the API's concurrent session limit,
which can evict the user's other devices. The route still answers 204.
FinishRegisterOptions drops the four cookie options it no longer needs and
FinishRegisterResult drops setCookies. Both adapters are updated.

This is coordinated with the matching seamless-auth-api release, which refuses a
pre-auth token on these two routes.

The pre-auth routes' silent refresh still mints an access token into a cookie those
routes cannot use. That is a separate defect and is left alone here.
…kstep

An auth-mode change has no safe release order. An older adapter sends the token the new
API refuses, and this release sends one an older API refuses, so enrollment answers 401
until both sides land. The changeset is where an adopter reads that, so it says it.
@Bccorb
Bccorb merged commit 05be331 into main Sep 8, 2026
1 of 2 checks passed
@Bccorb
Bccorb deleted the fix/passkey-enrollment-access-identity branch September 8, 2026 15:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant