Repository navigation
feat: CDI 5.5 - single-use webauthn sign in options - #100
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SamDP1AS8ybBhCc1CWBWWk
tamassoltesz
left a comment
There was a problem hiding this comment.
Checked the spec against core 12.0.8 (first release declaring 5.5) and 12.4, plus the pg plugin queries. Inline comments cover the wording issues.
One more, outside the diff: requests without a cdi-version header resolve to the latest CDI (WebserverAPI.getVersionFromRequest returns maxCDIVersion), so SemVer.v5_5 is true for them and their options are consumed too. The CHANGELOG line "requests on CDI <= 5.4 keep the previous behaviour" holds only for requests that send the header; worth one sentence.
Verified correct:
- 5.5 is first declared in 12.0.8 (
coreDriverInterfaceSupported.json, CHANGELOG[12.0.8]); still declared through 12.4. - The gate is
SignInAPI.java:71(getVersionFromRequest(req).greaterThanOrEqualTo(SemVer.v5_5)), and that is the only production use ofv5_5, so nothing else 5.5-gated is missing. - A sequential replay returns
OPTIONS_NOT_FOUND_ERROR: the options lookup is the first step ofsignIn, before the credential and signature checks. - Failed attempts don't consume: signature / format / expiry failures throw before
removeOptions_Transaction; unknown user and the tenant check throw after it inside the same transaction, which rolls back (also undoing the counter update). - The
requiredfix matches what core parses (webauthnGeneratedOptionsIdstring,credentialobject;emailis never read). UNKNOWN_USER_ID_ERRORdescription, the full status enum,TestSignInOptionsConsumption_5_5,info.version5.5.0, thecdi-versionexample and the CHANGELOG section.
| Sign in the user using the WebAuthn credential. | ||
|
|
||
| The sign in options (`webauthnGeneratedOptionsId`, created with `/recipe/webauthn/options/signin`) are single-use: | ||
| they are consumed atomically with a successful verification, so the same assertion cannot be replayed. |
There was a problem hiding this comment.
Suggest narrowing this to what the test suite pins: a later sign-in with the same webauthnGeneratedOptionsId returns OPTIONS_NOT_FOUND_ERROR (TestSignInOptionsConsumption_5_5 covers that sequential case). "Consumed atomically" is a stronger guarantee than the spec needs to make; I'd drop "atomically" and describe the observable result instead.
| status: | ||
| type: string | ||
| description: | | ||
| - `OPTIONS_NOT_FOUND_ERROR`: unknown or expired options id, or options already consumed by an earlier successful sign in |
There was a problem hiding this comment.
Expired options don't return OPTIONS_NOT_FOUND_ERROR: verifyAuthenticationData throws InvalidWebauthNOptionsException("Options expired") (WebAuthN.java:256-257 @ 12.4), which SignInAPI maps to INVALID_OPTIONS_ERROR with that reason. The load query has no expiry filter, so expired options only become "not found" after CleanUpWebauthNExpiredDataCron (runs every 24h) deletes them.
Suggest dropping "or expired" here and documenting "options expired" under INVALID_OPTIONS_ERROR.
There was a problem hiding this comment.
Right, missed that the load query has no expiry filter. Expired → INVALID_OPTIONS_ERROR ("Options expired") with a note that they become "not found" only after the cleanup cron; fixed in #104.
| type: string | ||
| description: | | ||
| - `OPTIONS_NOT_FOUND_ERROR`: unknown or expired options id, or options already consumed by an earlier successful sign in | ||
| - `CREDENTIAL_NOT_FOUND_ERROR`: no credential with this id is registered for the options' relying party |
There was a problem hiding this comment.
Also returned when the credential exists but its user is not a member of the requested tenant (the GHSA-6v34 defense-in-depth check in WebAuthN.signIn: if (fullyLoadedUserInfo.tenantIds == null || !…contains(tenantId)) throw … WebauthNCredentialNotExistsException). Suggest "…or the credential's user is not in this tenant".
There was a problem hiding this comment.
Added the tenant-membership case to CREDENTIAL_NOT_FOUND_ERROR in #104.
- approximate means 'served from the ledger', not 'not fresh' - asOf can be older than the request time; for exact counts it is the request time Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SamDP1AS8ybBhCc1CWBWWk
- built-in rules (no mapping) take the first present claim as is; a mapping skips blank values - described separately on sub/email - only a claim's first value is considered - acsURLs entries: strings, not empty or whitespace-only; Destination/Recipient checked when present - 512 is UTF-16 code units Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SamDP1AS8ybBhCc1CWBWWk
- verify: lineage check is current-or-previous refresh token, not time bounded - refresh: displaced tokens are ORPHANED_BRANCH reuse even inside the grace window; STALE_LINEAGE is always TOKEN_THEFT_DETECTED; SDKs should serialise refreshes per session - no jitter when accessTokenValidity is given; accessTokenValidity is an integer - allowApproximate: snapshot may lag, asOf can be older than the request - CHANGELOG: 403 is scoped to verify Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SamDP1AS8ybBhCc1CWBWWk
- describe the observable result (later sign in with the same options id -> OPTIONS_NOT_FOUND_ERROR) instead of 'consumed atomically' - expired options are INVALID_OPTIONS_ERROR, not OPTIONS_NOT_FOUND_ERROR - CREDENTIAL_NOT_FOUND_ERROR also covers a credential whose user is not in the tenant - requests without a cdi-version header resolve to the latest CDI and consume too Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SamDP1AS8ybBhCc1CWBWWk
fix: webauthn sign in wording from the #100 review
CDI 5.5 — single-use WebAuthn sign in options
Core: 12.0.8 (
Adds CDI 5.5, supertokens-core#1195). Gated onSemVer.v5_5inwebauthn/SignInAPI.java; covered byTestSignInOptionsConsumption_5_5.Spec changes
POST /appid-<appId>/<tenantId>/recipe/webauthn/signin: the sign in options are consumed atomically with a successful verification, so replaying the same assertion returnsOPTIONS_NOT_FOUND_ERROR. Failed attempts don't consume the options. Documented on the endpoint and on theOPTIONS_NOT_FOUND_ERROR/CREDENTIAL_NOT_FOUND_ERROR/UNKNOWN_USER_ID_ERRORstatuses.requiredlist, which saidemailinstead ofwebauthnGeneratedOptionsId+credential.info.version→ 5.5.0,cdi-versionexample →5.5, CHANGELOG entry.SDK impact
SDKs declaring 5.5 must verify each assertion exactly once (the node SDK currently verifies twice per sign in, which is why the behaviour is gated).
Stacked on
feat/saml→5.4(#99). Base branch5.5was cut from the tip offeat/saml.