Skip to content

feat: CDI 5.5 - single-use webauthn sign in options - #100

Merged
porcellus merged 1 commit into
5.5from
feat/5.5-webauthn-single-use-signin-options
Oct 28, 2026
Merged

porcellus merged 1 commit into
5.5from
feat/5.5-webauthn-single-use-signin-options

Conversation

@porcellus

Copy link
Copy Markdown
Collaborator

CDI 5.5 — single-use WebAuthn sign in options

Core: 12.0.8 (Adds CDI 5.5, supertokens-core#1195). Gated on SemVer.v5_5 in webauthn/SignInAPI.java; covered by TestSignInOptionsConsumption_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 returns OPTIONS_NOT_FOUND_ERROR. Failed attempts don't consume the options. Documented on the endpoint and on the OPTIONS_NOT_FOUND_ERROR / CREDENTIAL_NOT_FOUND_ERROR / UNKNOWN_USER_ID_ERROR statuses.
  • Also fixes the request schema's required list, which said email instead of webauthnGeneratedOptionsId + credential.
  • info.version → 5.5.0, cdi-version example → 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 branch 5.5 was cut from the tip of feat/saml.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SamDP1AS8ybBhCc1CWBWWk
@porcellus
porcellus marked this pull request as ready for review October 28, 2026 13:32
@porcellus
porcellus merged commit 1cf6ddf into 5.5 Oct 28, 2026
1 of 2 checks passed
@porcellus
porcellus deleted the feat/5.5-webauthn-single-use-signin-options branch October 28, 2026 13:32

@tamassoltesz tamassoltesz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 of v5_5, so nothing else 5.5-gated is missing.
  • A sequential replay returns OPTIONS_NOT_FOUND_ERROR: the options lookup is the first step of signIn, 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 required fix matches what core parses (webauthnGeneratedOptionsId string, credential object; email is never read).
  • UNKNOWN_USER_ID_ERROR description, the full status enum, TestSignInOptionsConsumption_5_5, info.version 5.5.0, the cdi-version example and the CHANGELOG section.

Comment thread api_spec.yaml
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — reworded to the observable result in #104 (ea5e8e4), and carried into #101–#103.

Comment thread api_spec.yaml
status:
type: string
description: |
- `OPTIONS_NOT_FOUND_ERROR`: unknown or expired options id, or options already consumed by an earlier successful sign in

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api_spec.yaml
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the tenant-membership case to CREDENTIAL_NOT_FOUND_ERROR in #104.

porcellus pushed a commit that referenced this pull request Oct 28, 2026
- 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
porcellus pushed a commit that referenced this pull request Oct 28, 2026
- 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
porcellus pushed a commit that referenced this pull request Oct 28, 2026
- 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
@porcellus

Copy link
Copy Markdown
Collaborator Author

Thanks — all three inline points plus the no-cdi-version-header note are applied in #104 (this PR merged before the review landed) and carried into #101–#103.

porcellus pushed a commit that referenced this pull request Oct 28, 2026
- 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
porcellus added a commit that referenced this pull request Oct 28, 2026
fix: webauthn sign in wording from the #100 review
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.

2 participants