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

  1. Obtain a fresh token from the control plane and retry - expiry is validated and is the most common cause
  2. Confirm the token was issued by the same environment whose JWKS this compute was started with; restart compute_ctl to refetch rotated keys
  3. Check warn-level logs for per-key decode errors to distinguish signature vs algorithm vs expiry
  4. 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

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


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/7e55695cb6caa9c0. Report an issue: GitHub.