HIGH jwt misconfigurationaxumapi keys

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, and nbf in 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 IDNameSeverity
CWE-287Improper Authentication CRITICAL
CWE-306Missing Authentication for Critical Function CRITICAL
CWE-307Brute Force HIGH
CWE-308Single-Factor Authentication MEDIUM
CWE-309Use of Password System for Primary Authentication MEDIUM
CWE-347Improper Verification of Cryptographic Signature HIGH
CWE-384Session Fixation HIGH
CWE-521Weak Password Requirements MEDIUM
CWE-613Insufficient Session Expiration MEDIUM
CWE-640Weak Password Recovery HIGH

Frequently Asked Questions

What is a common JWT misconfiguration in Axum when API keys are also used?
Accepting unsigned tokens or skipping issuer/audience/expiration checks while requiring only an API key, which allows a weak JWT to bypass intended authorization when paired with a valid key.
How can you bind JWTs to API keys in Axum to reduce risk?
Enforce strict JWT claim validation and require the JWT subject (sub) to match the API key identifier; verify both credentials on every request and ensure the JWT’s scope aligns with the key’s permissions.