Jwt Misconfiguration in Axum with Api Keys
Jwt Misconfiguration in Axum with Api Keys — how this specific combination creates or exposes the vulnerability
JWT misconfiguration in an Axum service that also uses API keys can undermine both controls and expose the API to unauthorized access. When JWT validation is incomplete—such as missing issuer (iss) checks, not verifying the audience (aud), or failing to enforce token expiration—attackers can use a stolen or forged JWT even when a valid API key is presented.
In Axum, this often occurs when JWT extraction and validation are applied as a layer but run independently or conditionally from API key checks. For example, if middleware validates the API key and skips JWT verification for certain routes, or if JWT claims are not strictly validated, an attacker who knows the API key can pair it with a weak JWT to escalate privileges or access admin endpoints. Common weaknesses include not binding JWTs to the API key subject, not enforcing HTTPS, or accepting unsigned tokens, which can enable token substitution or injection.
The risk is compounded when JWTs carry elevated permissions and API keys are treated as a simple gate. Because Axum allows flexible route and layer composition, it is possible to accidentally allow access when either credential is valid rather than requiring both to be valid and consistent. Without strict claim validation and consistent authorization logic across layers, the API’s effective security regresses to the weaker control, exposing endpoints to IDOR, privilege escalation, and unauthorized data access.
Api Keys-Specific Remediation in Axum — concrete code fixes
To securely combine API keys and JWTs in Axum, enforce both checks for every request and ensure JWT validation is strict and independent. Below are concrete, realistic code examples that demonstrate a hardened approach using jsonwebtoken and a custom API key extractor.
Strict JWT validation with required claims
use axum::{async_trait, extract::FromRequestParts, http::request::Parts};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation, TokenData};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ApiKeyValidator(Arc<std::collections::HashSet<String>>);
impl ApiKeyValidator {
pub fn new(keys: impl IntoIterator<Item = String>) -> Self {
Self(Arc::new(keys.into_iter().collect()))
}
pub fn validate(&self, key: &str) -> bool {
self.0.contains(key)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub iss: String,
pub aud: String,
pub exp: usize,
pub scope: String,
}
#[derive(Debug, Clone)]
pub struct JwtClaims(pub Claims);
#[async_trait]
impl Result<Self, Self::Rejection> {
let auth = parts.headers.get("authorization")
.and_then(|v| v.to_str().ok())
.ok_or((http::StatusCode::UNAUTHORIZED, "missing authorization header"))?;
let token = auth.strip_prefix("Bearer ")
.ok_or((http::StatusCode::UNAUTHORIZED, "invalid authorization format"))?;
let validation = Validation::new(Algorithm::HS256);
let mut validation = validation;
validation.validate_exp = true;
validation.required_spec_claims = vec!["iss", "aud", "sub", "exp"];
validation.set_issuer(&["trusted-issuer"]);
validation.set_audience(&["api.example.com"]);
let token_data: TokenData<Claims> = decode(
token,
&DecodingKey::from_secret("YOUR_SECRET_KEY".as_ref()),
&validation,
).map_err(|e| (http::StatusCode::UNAUTHORIZED, format!("invalid token: {e}")))?;
Ok(Self(token_data.claims))
}
}
pub async fn require_both(
ApiKeyValidator(keys): ApiKeyValidator,
JwtClaims(claims): JwtClaims,
) -> Result<impl IntoResponse, (http::StatusCode, String)> {
// Ensure the JWT's subject matches the API key subject or a derived principal
if !claims.sub.starts_with("key_") {
return Err((http::StatusCode::FORBIDDEN, "JWT-subject mismatch with API key".into()));
}
// Optionally bind by value, e.g., require claims.sub == key
if !keys.validate(&claims.sub) {
return Err((http::StatusCode::FORBIDDEN, "invalid key scope").into());
}
Ok(format!("authorized for subject: {}", claims.sub))
}
Axum route combining API key and JWT checks
use axum::{routing::get, Router};
pub fn app(api_key_validator: ApiKeyValidator) -> Router {
Router::new()
.route("/admin", get(require_both))
.with_state(api_key_validator)
}
Key practices
- Always validate
iss,aud,exp, andnbfin JWT validation. - Bind JWT subject to the API key (e.g.,
sub: key_abc123) and verify consistency in authorization logic. - Use HTTPS to prevent token interception and ensure API keys are transmitted securely.
- Avoid allowing either credential alone to grant access unless explicitly designed and scoped.
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |