zeroclaw-labs/zeroclaw · error
Credential ID not in allowed list
Error message
Credential ID not in allowed list
What it means
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.
Source
Thrown at crates/zeroclaw-runtime/src/security/webauthn.rs:394
let state = AuthenticationState {
challenge,
user_id: user_id.into(),
allowed_credentials: allowed_ids,
};
Ok((request, state))
}
/// Complete a WebAuthn authentication ceremony.
/// Validates the assertion signature against the stored public key
/// and updates the sign counter for clone detection.
pub fn finish_authentication(
&self,
auth_state: &AuthenticationState,
response: &AuthenticateCredentialResponse,
) -> Result<()> {
// 1. Verify credential ID is in allowed list
anyhow::ensure!(
auth_state.allowed_credentials.contains(&response.id),
"Credential ID not in allowed list"
);
// 2. Load the credential
let mut all_credentials = self.load_all_credentials()?;
let credential = all_credentials
.values()
.flatten()
.find(|c| c.credential_id == response.id)
.cloned()
.ok_or_else(|| {
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(::serde_json::json!({"credential_id": response.id})),
"webauthn verify refused: credential id not in store"View on GitHub (pinned to 88bb9c8533)
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
Defensive patterns
Strategy: validation
Validate before calling
// server-side pre-check before finish_authentication
let allowed: &[String] = &auth_state.allowed_credentials;
if !allowed.contains(&response.id) {
// restart the ceremony instead of calling finish_authentication
return Err(anyhow::anyhow!("credential not offered; call auth start again"));
} Try / catch
on this error return HTTP 400 with a 'restart-authentication' code; client must call start again — never silently retry the same finish
Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Challenge mismatch in authentication response
- Expected type 'webauthn.create', got '{cd_type}'
- Challenge mismatch in registration response
- Credential ID too long ({} bytes, max {MAX_CREDENTIAL_ID_LEN
- No registered credentials for user '{user_id}'
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/9b9651494a1c578f.
Report an issue: GitHub.