{"record":{"id":"9b9651494a1c578f","repo":"zeroclaw-labs/zeroclaw","slug":"credential-id-not-in-allowed-list","errorCode":null,"errorMessage":"Credential ID not in allowed list","messagePattern":"Credential ID not in allowed list","errorType":"exception","errorClass":null,"httpStatus":401,"severity":"error","filePath":"crates/zeroclaw-runtime/src/security/webauthn.rs","lineNumber":394,"sourceCode":"        let state = AuthenticationState {\n            challenge,\n            user_id: user_id.into(),\n            allowed_credentials: allowed_ids,\n        };\n\n        Ok((request, state))\n    }\n\n    /// Complete a WebAuthn authentication ceremony.\n    /// Validates the assertion signature against the stored public key\n    /// and updates the sign counter for clone detection.\n    pub fn finish_authentication(\n        &self,\n        auth_state: &AuthenticationState,\n        response: &AuthenticateCredentialResponse,\n    ) -> Result<()> {\n        // 1. Verify credential ID is in allowed list\n        anyhow::ensure!(\n            auth_state.allowed_credentials.contains(&response.id),\n            \"Credential ID not in allowed list\"\n        );\n\n        // 2. Load the credential\n        let mut all_credentials = self.load_all_credentials()?;\n        let credential = all_credentials\n            .values()\n            .flatten()\n            .find(|c| c.credential_id == response.id)\n            .cloned()\n            .ok_or_else(|| {\n                ::zeroclaw_log::record!(\n                    WARN,\n                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)\n                        .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                        .with_attrs(::serde_json::json!({\"credential_id\": response.id})),\n                    \"webauthn verify refused: credential id not in store\"","sourceCodeStart":376,"sourceCodeEnd":412,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-runtime/src/security/webauthn.rs#L376-L412","documentation":"finish_authentication first checks the response's credential ID against allowed_credentials captured in the AuthenticationState at start_authentication time. An ID outside that list means the client authenticated with a credential that was not offered for this ceremony — a different passkey, a stale state, or a modified ID.","triggerScenarios":"The browser/auto-fill picks a different passkey than one of the offered ones (e.g. cross-device flow or multiple resident keys); replaying a finish request against an auth_state from a different start; credential re-registered while an old state was cached; ID encoding mismatch between what start returned and what the client echoes.","commonSituations":"Stale SPA state after the user re-registers; multiple devices syncing passkeys; a finish endpoint hit twice with mismatched sessions; clients that re-encode the credential ID.","solutions":["Restart authentication (new start) and let the client sign with one of the freshly offered credentials","Ensure response.id is sent back exactly as delivered in allowCredentials (same base64url string)","Never reuse an auth_state across ceremonies or sessions; store it alongside the current challenge"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// server-side pre-check before finish_authentication\nlet allowed: &[String] = &auth_state.allowed_credentials;\nif !allowed.contains(&response.id) {\n    // restart the ceremony instead of calling finish_authentication\n    return Err(anyhow::anyhow!(\"credential not offered; call auth start again\"));\n}","typeGuard":null,"tryCatchPattern":"on this error return HTTP 400 with a 'restart-authentication' code; client must call start again — never silently retry the same finish","preventionTips":["Keep the auth_state server-side keyed to the session; never let the client supply it","Echo response.id exactly as received in allowCredentials","Invalidate pending auth_state whenever a credential is registered or removed"],"tags":["webauthn","authentication","credential-id","session-state","rust"],"backgroundTag":"webauthn-credential-not-allowed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}