zeroclaw-labs/zeroclaw · warning

Credential '{credential_id}' not found for user '{user_id}'

Error message

Credential '{credential_id}' not found for user '{user_id}'

What it means

remove_credential loads all stored credentials, finds the user's list, and retains everything not matching the given credential_id. If the retain left the list unchanged (the ID was not present), the removal failed and the error names the credential and user. A missing user list raises a separate error.

Source

Thrown at crates/zeroclaw-runtime/src/security/webauthn.rs:498

            cred.sign_count = new_count;
        }
        self.save_all_credentials(&all_credentials)?;

        Ok(())
    }

    /// List all credentials for a user.
    pub fn list_credentials(&self, user_id: &str) -> Result<Vec<WebAuthnCredential>> {
        self.load_credentials_for_user(user_id)
    }

    /// Remove a credential by ID.
    pub fn remove_credential(&self, user_id: &str, credential_id: &str) -> Result<()> {
        let mut all = self.load_all_credentials()?;
        if let Some(user_creds) = all.get_mut(user_id) {
            let before = user_creds.len();
            user_creds.retain(|c| c.credential_id != credential_id);
            anyhow::ensure!(
                user_creds.len() < before,
                "Credential '{credential_id}' not found for user '{user_id}'"
            );
        } else {
            anyhow::bail!("No credentials found for user '{user_id}'");
        }
        self.save_all_credentials(&all)
    }

    // ── Private helpers ─────────────────────────────────────────

    fn generate_challenge(&self) -> Result<String> {
        let mut buf = [0u8; CHALLENGE_LEN];
        self.rng.fill(&mut buf).map_err(|_| {
            ::zeroclaw_log::record!(
                ERROR,
                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
                    .with_outcome(::zeroclaw_log::EventOutcome::Failure),

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Refresh the credential list after any delete and before the next one
  2. Treat delete-not-found as success (idempotent delete) in the calling UI or wrapper
  3. Pass the credential_id exactly as returned by the list/registration API, unmodified
Defensive patterns

Strategy: validation

Validate before calling

// idempotent delete: only call remove_credential when present
let creds = service.list_credentials(user_id)?;
if creds.iter().any(|c| c.credential_id == cred_id) {
    service.remove_credential(user_id, cred_id)?;
}
Ok(())

Try / catch

catch the not-found message and return success (idempotent delete semantics) or 404 to let the UI refresh its list — do not surface it as an unexpected error

Prevention

When it happens

Trigger: Deleting a credential that was already removed (double-click, retrying a successful delete); an ID with different encoding or whitespace than stored; a UI list that is stale after another session removed the credential.

Common situations: Dashboard delete buttons firing twice; retry-after-timeout resubmits; multiple browser tabs both managing credentials; IDs copied with altered case or padding.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/c1aa304535241067. Report an issue: GitHub.