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
- Mint tokens so the JOSE header includes the kid of the signing key (standard behavior of major IdPs).
- Check the issuer's JWKS endpoint actually returns keys: `curl <jwks_uri>`.
- If you operate the IdP, publish keys with kid and ES256 as expected.
- 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
- Use the official login flow or a mature JWT library that sets kid automatically.
- Verify the IdP publishes keys with kid and ES256.
- Health-check the JWKS endpoint whenOIDC issues are suspected.
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
- Issuer mismatch: got {:?}, expected {:?}
- Error fetching public key for issuer {raw_issuer}
- Issuer too long: {:?}
- Subject too long: {:?}
- Issuer empty
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/f79b70bc4deb60ef.
Report an issue: GitHub.