Repository navigation
feat(auth0-server-js): keep the passkey authSession on the server - #312
Draft
nandan-bhat wants to merge 2 commits into
Draft
nandan-bhat wants to merge 2 commits into
nandan-bhat wants to merge 2 commits into
Conversation
Add passkey.startSignup(), passkey.startLogin() and passkey.complete(). The start methods keep the Auth0 authSession, the challenge, the domain, the realm and the organization in the transaction store under __a0_passkey_tx (option passkeyTransactionIdentifier), and return only what the browser needs. complete() takes those values from the store, checks that the credential was created for the stored challenge, exchanges it and persists the session. register(), challenge() and getToken() are unchanged. - Expire the entry with the Auth0 auth session. CookieTransactionStore sets Max-Age and the JWE exp from the new TransactionData.expiresAt. - Delete the entry once Auth0 has used the auth session and keep it on every other failure, so complete() can be retried. Store errors after the exchange are not reported. - Remove the authSession from errors thrown by complete(). - Reject a passkeyTransactionIdentifier that shares a prefix with another store identifier, and treat a passkey entry as missing in the redirect-login and magic-link callbacks. - Raise @auth0/auth0-auth-js to ^1.17.0. - Document the new methods, the TransactionStore contract and the flow in EXAMPLES.md, and add tests. Co-Authored-By: Claude <noreply@anthropic.com>
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
When passkey.startSignup() returns verificationRequired, keep the list with the stored flow. passkey.complete() then throws a MissingRequiredArgumentError without calling Auth0 when none of the listed identifiers has a code, instead of sending an empty verification payload. The flow is kept, so the app can send the codes. A code that is empty once trimmed counts as missing, as it does for Auth0. Also reword the comment on removing the authSession from complete() errors: the auth session stays inside the SDK, it is not a secret. Co-Authored-By: Claude <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
passkey.register()andpasskey.challenge()return the Auth0authSession, and the app has to pass it back topasskey.getToken()together with the samerealmandorganization. So apps either send theauthSessionto the browser and back, or build their own storage for it. This PR adds three methods that keep the whole flow on the server, in the transaction store that the redirect login already uses. The browser only gets the WebAuthn options and sends back the credential.New methods
passkey.startSignup(options, storeOptions?, requestOptions?)requests a signup challenge, stores the flow and returns{ authnParamsPublicKey, verificationRequired? }.passkey.startLogin(options?, storeOptions?, requestOptions?)does the same for a login and returns{ authnParamsPublicKey }.passkey.complete({ credential, verification?, scope?, audience? }, storeOptions?, requestOptions?)exchanges the credential and persists the session, likegetToken(). It takes theauthSession,realm,organizationand domain from the stored flow, never from the request.register(),challenge()andgetToken()are unchanged. They stay documented as the lower-level path for apps that keep theauthSessionthemselves.How the stored flow works
__a0_passkey_txby default (new optionpasskeyTransactionIdentifier). WithCookieTransactionStoreit is an encryptedhttpOnlycookie next to__a0_tx.timeoutAuth0 returns with the challenge (60 seconds, or 15 minutes with identifier verification), up to 1 hour.CookieTransactionStorenow sets the cookie'sMax-Ageand the expiry of the encrypted value from the new optionalTransactionData.expiresAt. Entries without it keep the usual hour.complete()checks that the credential was created for the stored challenge (read fromclientDataJSON). A credential from an older start, for example in another tab, gets aMissingTransactionError, and the current flow is not used up. A credential without aclientDataJSONgets aMissingRequiredArgumentError.verificationRequiredlist. When none of the listed identifiers has a code,complete()throws aMissingRequiredArgumentErrorwithout calling Auth0, instead of sending an emptyverification. The flow is kept, so the app can send the codes.completeInteractiveLogin()does), onmfa_required, on aninvalid_grantthat is not retryable, and on an organization mismatch. It is kept on every other failure (a wrong or missing code, 429, 5xx, a network error), so the app can callcomplete()again with the same credential.TransactionStoredocs tell custom stores to log their own failures.complete()never carry theauthSession: it is removed from thePasskeyGetTokenErrorand itscause.getToken()errors are unchanged.passkeyTransactionIdentifiermust not share a prefix withtransactionIdentifier,stateIdentifieror, when ananonymousStoreis configured,anonymousSessionIdentifier. The new methods check this and throwInvalidConfigurationErrorbefore calling Auth0. Apps that do not call them are not affected.completeInteractiveLogin()andcompletePasswordlessMagicLink()treat a passkey entry as missing. This only matters for a custom store that ignores the identifier.Other changes
@auth0/auth0-auth-jsto^1.17.0, forisRetryableandauthSessiononPasskeyGetTokenError. feat(auth0-server-js): support identifier verification in passkey signup #307 raises it the same way.PasskeyStartSignupResult,PasskeyStartLoginResultandPasskeyCompleteOptionstypes.TransactionStoremust do, and pointers from the old methods.EXAMPLES.md: the passkey section now uses the new methods, with error handling and retries,StoreOptions, using a custom transaction store, and the old methods under "Keeping theauthSessionyourself". The anonymous sessions note inREADME.mdlistspasskey.complete().Choices to check
startSignup,startLoginandcomplete.Impact on other packages
CookieTransactionStoreonly changes the cookie lifetime of entries that carryexpiresAt, which only the new methods write.auth0-express,auth0-fastify,auth0-nuxtandauth0-tanstack-start-reactbuild onCookieTransactionStoreand keep working without changes. They can expose the new methods in follow-up PRs. Against a build of this branch, theauth0-fastify(69/69) andauth0-tanstack-start-react(311/311) tests pass, andauth0-expressgives the same result as withmain(349/350).References
Testing
New tests:
server-passkey-client.transaction.spec.ts: signup and login end to end with a cookie jar; what is stored and what reaches the browser; each error outcome and whether the flow is kept or deleted; retries after a wrong code; expiry; a credential from another start; a missingclientDataJSON; a signup without one-time codes; the prefix check; separation from the redirect login; resolver mode;storeOptionsand per-request options; store errors after the exchange; and that errors carry noauthSession.passkey-transaction.spec.ts: the shape check of the stored entry, the one-time code check, the challenge comparison (base64 and base64url, with or without padding), and that it runs in linear time on hostile input.transaction-slot.spec.tsandcookie-transaction-store.spec.ts: reading, expiry, and the cookie lifetime fromexpiresAt.server-passkey-client.workers.spec.ts: the flow against the built package in the workers test.I checked that the tests fail when the code is wrong. I made 58 deliberate mistakes in the code, one at a time, and the tests catch each of them. For example: skipping the challenge check or the one-time code check, keeping the flow after
invalid_grant, always deleting it, ignoringexpiresAt, a one-way prefix check, letting values from the request replace the stored ones, deleting the flow after writing the session, leaving theauthSessionon the error, and bringing back a regex that is slow on long input.A consumer type check compiles the new methods and types against the built package. It also confirms that the start results have no
authSessionand thatcomplete()accepts neither arealmnor anauthSession.Run locally on Node 22.21.1 with
auth0-auth-jsbuilt first:build,lintandtest:workerspass forauth0-server-js, and 767 of its 771 tests pass. The 4 that fail are the "forwards an already-aborted per-request signal" tests for passkey (3) and database sign-up (1). They also fail on a clean checkout ofmainin my environment, so this change does not cause them.Checklist
🤖 Generated with Claude Code