From f5f0aae3ca9883ad302a46bbd1c595e997f33c6d Mon Sep 17 00:00:00 2001 From: Mihaly Lengyel Date: Wed, 28 Oct 2026 14:10:35 +0000 Subject: [PATCH] fix: webauthn sign in wording from the #100 review - 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 Claude-Session: https://claude.ai/code/session_01SamDP1AS8ybBhCc1CWBWWk --- CHANGELOG.md | 2 +- api_spec.yaml | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5809109..458f895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [5.5.0] -- `/appid-//recipe/webauthn/signin` POST: sign in options are now single-use. They are consumed atomically with a successful sign in, so verifying the same assertion again returns `OPTIONS_NOT_FOUND_ERROR`. SDKs must verify each assertion exactly once (requests on CDI <= 5.4 keep the previous behaviour). +- `/appid-//recipe/webauthn/signin` POST: sign in options are now single-use. A successful sign in consumes them, so a later sign in with the same `webauthnGeneratedOptionsId` returns `OPTIONS_NOT_FOUND_ERROR`. SDKs must verify each assertion exactly once. Requests that send a `cdi-version` of 5.4 or lower keep the previous behaviour; requests without the header are treated as the latest CDI version and consume the options too. ## [5.4.0] diff --git a/api_spec.yaml b/api_spec.yaml index f42d7e6..37f977c 100644 --- a/api_spec.yaml +++ b/api_spec.yaml @@ -9333,9 +9333,10 @@ paths: 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. - Verifying the same assertion a second time returns `OPTIONS_NOT_FOUND_ERROR`. + a successful sign in consumes them, so a later sign in with the same `webauthnGeneratedOptionsId` returns `OPTIONS_NOT_FOUND_ERROR`. Failed attempts do not consume the options; they stay usable until they expire or are removed. + + Note that requests without a `cdi-version` header are treated as the core's latest CDI version, so their options are consumed too. operationId: signInWebAuthNUser parameters: - $ref: '#/components/parameters/webauthnRid' @@ -9376,6 +9377,10 @@ paths: properties: status: type: string + description: | + - `INVALID_OPTIONS_ERROR`: the options have expired (`reason`: "Options expired") + - `INVALID_AUTHENTICATOR_ERROR`: the assertion failed verification (`reason` has the details) + - `INVALID_CREDENTIALS_ERROR`: the `credential` object could not be parsed (`reason` has the details) enum: [ 'INVALID_OPTIONS_ERROR', 'INVALID_AUTHENTICATOR_ERROR', 'INVALID_CREDENTIALS_ERROR' ] reason: type: string @@ -9384,8 +9389,8 @@ paths: status: 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 + - `OPTIONS_NOT_FOUND_ERROR`: unknown options id, or options already consumed by an earlier successful sign in (expired options are reported as `INVALID_OPTIONS_ERROR` until they are cleaned up) + - `CREDENTIAL_NOT_FOUND_ERROR`: no credential with this id is registered for the options' relying party, or the credential's user is not in this tenant - `UNKNOWN_USER_ID_ERROR`: the credential's user no longer exists enum: [ 'OPTIONS_NOT_FOUND_ERROR', 'CREDENTIAL_NOT_FOUND_ERROR', 'UNKNOWN_USER_ID_ERROR' ] '400':