neondatabase/neon · error
failed to verify authorization token
Error message
failed to verify authorization token
What it means
compute_ctl's HTTP authorization middleware (tower_http AsyncAuthorizeRequest) tried to verify the incoming Bearer JWT against every key in its JWKS and every attempt failed - either DecodingKey::from_jwk failed for each key or jsonwebtoken::decode rejected the token for each (bad signature, wrong algorithm, expired exp, malformed claims). The final error carries no detail; the per-key reasons only appear as warn! logs ('failed to decode authorization token using {kid}'). The middleware maps it to 401 Unauthorized for the request.
Source
Thrown at compute_tools/src/http/middleware/authorize.rs:184
continue;
}
};
match jsonwebtoken::decode::<ComputeClaims>(token, &decoding_key, validation) {
Ok(data) => return Ok(data),
Err(e) => {
warn!(
"failed to decode authorization token using {}: {}",
jwk.common.key_id.as_ref().unwrap(),
e
);
continue;
}
}
}
Err(anyhow!("failed to verify authorization token"))
}
}
View on GitHub (pinned to 8f60b04da4)
Solutions
- Obtain a fresh token from the control plane and retry - expiry is validated and is the most common cause
- Confirm the token was issued by the same environment whose JWKS this compute was started with; restart compute_ctl to refetch rotated keys
- Check warn-level logs for per-key decode errors to distinguish signature vs algorithm vs expiry
- Verify the header is exactly `Bearer <jwt>` with no extra characters, and the token has the 3 dot-separated JWT parts
Example fix
// before
let resp = client.get(url).bearer_auth(&token).send().await?;
// after
let mut resp = client.get(url).bearer_auth(&token).send().await?;
if resp.status() == StatusCode::UNAUTHORIZED {
token = refresh_token().await?; // fresh, unexpired JWT
resp = client.get(url).bearer_auth(&token).send().await?;
} Defensive patterns
Strategy: validation
Validate before calling
// client side: refuse to send an already-expired token
let payload_b64 = token.split('.').nth(1)?;
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(payload_b64)
.ok()?;
let claims: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
if claims["exp"].as_i64().unwrap_or(0) <= chrono::Utc::now().timestamp() {
token = refresh_token().await?; // expired - get a new one before the request
} Try / catch
// server side: this error already maps to 401
match Authorize::verify(&jwks, bearer.token(), &validation) {
Ok(data) => data,
Err(_) => {
return Err(JsonResponse::error(
StatusCode::UNAUTHORIZED,
"failed to verify authorization token",
))
}
} Prevention
- Refresh tokens well before exp instead of reusing long-lived JWTs
- Send exactly 'Bearer <jwt>' - no quotes or whitespace - in the Authorization header
- Issue tokens from the same environment/keys as the compute's JWKS
- On 401, re-authenticate once with a fresh token instead of retrying the same one
When it happens
Trigger: jsonwebtoken::decode::<ComputeClaims> fails for all JWKS keys: expired token (validate_exp = true), token signed by a key absent from the JWKS, algorithm mismatch (validation is EdDSA unless the JWKS contains an RS256 key - Hadron deployments), or a structurally invalid token string.
Common situations: Long-lived tokens going stale; sending a staging token to a production compute (or vice versa); JWKS rotation after compute start (keys fetched once at startup); clock skew making exp appear passed; Authorization header with stray quotes or whitespace.
Related errors
- Safekeeper set up for auth but no private key specified
- invalid compute claims scope "{s}"
- path is neither a directory or a file
- Configured for JWT auth with zero decoding keys. All JWT gat
- not implemented
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/7e55695cb6caa9c0.
Report an issue: GitHub.