clockworklabs/SpacetimeDB · error · TokenValidationError::Other

No kid found

Error message

No kid found

What it means

When a JWT header carries no kid (key id), the validator falls back to trying every key in the issuer's keyset, and 'No kid found' is the seed error for that loop. Because each failed per-key attempt overwrites it, this message survives to the caller mainly when the keyset is empty — i.e. a kid-less token met an issuer that published no usable keys.

Source

Thrown at crates/core/src/auth/token_validation.rs:303

impl TokenValidator for JwksValidator {
    async fn validate_token(&self, token: &str) -> Result<SpacetimeIdentityClaims, TokenValidationError> {
        let header = decode_header(token)?;
        if let Some(kid) = header.kid {
            let key = self
                .keyset
                .keys
                .get(&kid)
                .ok_or_else(|| TokenValidationError::KeyIDNotFound)?;
            let validator = BasicTokenValidator {
                public_key: key.decoding_key.clone(),
                issuer: Some(self.issuer.clone()),
            };
            return validator.validate_token(token).await;
        }
        log::debug!("No key id in header. Trying all keys.");
        // TODO: Consider returning an error if no kid is given?
        // For now, lets just try all the keys.
        let mut last_error = TokenValidationError::Other(anyhow::anyhow!("No kid found"));
        for (kid, key) in &self.keyset.keys {
            log::debug!("Trying key {kid}");
            let validator = BasicTokenValidator {
                public_key: key.decoding_key.clone(),
                issuer: Some(self.issuer.clone()),
            };
            match validator.validate_token(token).await {
                Ok(claims) => return Ok(claims),
                Err(e) => {
                    last_error = e;
                    log::debug!("Validating with key {kid} failed");
                    continue;
                }
            }
        }
        // None of the keys worked.
        Err(last_error)
    }

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Mint tokens so the JOSE header includes the kid of the signing key (standard behavior of major IdPs).
  2. Check the issuer's JWKS endpoint actually returns keys: `curl <jwks_uri>`.
  3. If you operate the IdP, publish keys with kid and ES256 as expected.
  4. Use the normal login flow so the provider generates well-formed tokens.

Example fix

// token header (before)
{ "alg": "ES256", "typ": "JWT" }
// after
{ "alg": "ES256", "typ": "JWT", "kid": "key-2026-01" }
Defensive patterns

Strategy: validation

Validate before calling

import { decodeProtectedHeader } from 'jose';
const header = decodeProtectedHeader(jwt);
if (!header.kid) {
  throw new Error('token header must include kid — mint it via the provider/login flow, not by hand');
}

Try / catch

try {
  claims = await validate(jwt);
} catch (e) {
  if (String(e).includes('No kid found')) {
    throw new Error('token lacks kid and issuer keyset had no usable keys — check JWKS endpoint and token minting');
  }
  throw e;
}

Prevention

When it happens

Trigger: Authenticating with a JWT whose header omits kid against an issuer whose JWKS returned an empty or unusable keyset; custom token-minting scripts that do not set the kid header; a provider publishing its keys array without kid attributes.

Common situations: Hand-built JWTs from scripts or tests; misconfigured OIDC providers; partially failed keyset fetches leaving an empty cache entry.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/f79b70bc4deb60ef. Report an issue: GitHub.