HIGH time of check time of useaxumjwt tokens

Time Of Check Time Of Use in Axum with Jwt Tokens

Time Of Check Time Of Use in Axum with Jwt Tokens — how this specific combination creates or exposes the vulnerability

Time Of Check Time Of Use (TOCTOU) is a class of race condition where the state of a resource changes between a security check and the use of that resource. In Axum, when JWT tokens are used for authorization, a common pattern is to validate the token and extract claims early in the request lifecycle—often in a middleware or extractor—and then rely on those claims later in a handler. If the authorization logic that uses those claims is not re-validated at the point of action, and if the application state (such as user roles, permissions, or resource ownership) can change after the initial check, a TOCTOU vulnerability can occur.

Consider an endpoint that first validates a JWT token and confirms a user is an "admin," then later performs a sensitive operation such as deleting a record. If an attacker can cause the user’s role to change in the backend between the token validation and the delete action—perhaps by calling another endpoint that updates their role, or by exploiting weak session/claim caching—a race condition arises. The initial JWT validation (check) is stale by the time the operation executes (use). Because JWTs are typically stateless and self-contained, developers may assume the claims cannot be tampered with, but they may overlook mutable backend state or asynchronous propagation of privilege changes. In Axum, if authorization is implemented by storing claims in request extensions after initial validation and then reading them directly in handlers without re-authorizing against the current state, the window for TOCTOU is open.

Additionally, if JWT validation is performed in a middleware that runs before routing, and the route handler assumes the claims are immutable, an attacker who can trigger side effects that alter authorization-relevant data may exploit timing differences. For example, a handler might check a database flag to determine if a feature is enabled; if that flag changes after the JWT validation but before the feature is used, the check becomes unreliable. This is especially relevant when using JWTs for scope-based authorization without coupling token validity to real-time permissions stored in a database or distributed cache.

TOCTOU with JWT tokens in Axum is less about the cryptographic validity of the token and more about the consistency of authorization state between verification and execution. Because JWTs can carry roles or permissions, developers must ensure that any sensitive action re-validates those permissions against the current authoritative source at the moment of use, rather than relying solely on the token payload that was checked earlier in the request lifecycle.

Jwt Tokens-Specific Remediation in Axum — concrete code fixes

To mitigate TOCTOU in Axum when using JWT tokens, re-validate authorization at the point of action and avoid relying on request extensions or cached claims for critical decisions. Prefer direct permission checks against a current data source within handlers or use layered authorization that ties token validity to backend state.

Example 1: Re-fetch permissions in the handler

Instead of trusting claims stored in request extensions after middleware validation, fetch the latest user permissions inside the handler. This ensures the authorization state reflects the current system state.

use axum::{routing::get, Router};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    roles: Vec,
    exp: usize,
}

async fn validate_token(token: &str) -> Result {
    let decoding_key = DecodingKey::from_secret("secret".as_ref());
    let mut validation = Validation::new(Algorithm::HS256);
    validation.validate_exp = true;
    let token_data = decode::(token, &decoding_key, &validation)?;
    Ok(token_data.claims)
}

// Middleware that attaches claims to request extensions (for non-critical use only)
// Do not use these extensions for sensitive authorization decisions that require current state.

async fn delete_resource_handler(
    // Do not rely on extensions for critical authorization
    claims: axum::extract::Extension,
    user_id: axum::extract::Path,
) -> Result {
    // Re-fetch current permissions from a data source
    let current_permissions = fetch_current_permissions(&claims.sub).await;
    if !current_permissions.can_delete(&user_id) {
        return Err((axum::http::StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
    }
    // Proceed with deletion
    Ok("Resource deleted".to_string())
}

async fn fetch_current_permissions(user_id: &str) -> UserPermissions {
    // Simulate a database or cache lookup
    UserPermissions {
        can_delete: true,
    }
}

struct UserPermissions {
    can_delete: bool,
}

impl UserPermissions {
    fn can_delete(&self, _resource_id: &str) -> bool {
        self.can_delete
    }
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/resource/:id", get(|| async { "ok" }))
        .layer(axum::middleware::from_fn_with_state(
        (),
        |_state, request, extension| async move {
            // Example: validate token and attach claims — but do not authorize here for sensitive ops
            if let Some(token) = request.headers().get("authorization") {
                if let Ok(token_str) = token.to_str() {
                    if let Ok(claims) = validate_token(token_str.trim_start_matches("Bearer ")).await {
                        return Ok(request.extensions(claims));
                    }
                }
            }
            Ok(request)
        },
    ));

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Example 2: Use a permission service that checks current state

Introduce a permission service that is called at the point of use, taking the user identifier from the JWT claims and checking the latest state. This pattern keeps token validation separate from authorization decisions that require up-to-date data.

use axum::{routing::post, Extension, Router};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::Deserialize;
use std::sync::Arc;

struct PermissionService;

impl PermissionService {
    async fn can_update(&self, user_id: &str, resource_id: &str) -> bool {
        // Replace with actual database or policy check
        user_id == resource_id // Simplified example
    }
}

#[derive(Deserialize)]
struct UpdateRequest {
    resource_id: String,
}

async fn update_handler(
    Extension(claims): Extension,
    Extension(service): Extension>,
    body: axum::Json,
) -> Result {
    if service.can_update(&claims.sub, &body.resource_id).await {
        Ok("Update allowed".to_string())
    } else {
        Err((axum::http::StatusCode::FORBIDDEN, "Access denied".to_string()))
    }
}

#[tokio::main]
async fn main() {
    let service = Arc::new(PermissionService);
    let app = Router::new()
        .route("/update", post(update_handler))
        .layer(Extension(service));

    // Token validation middleware (simplified)
    let app = axum::middleware::from_fn_with_state(
        (),
        move |_state, request, next| {
            let service = service.clone();
            async move {
                if let Some(auth) = request.headers().get("authorization") {
                    if let Ok(token) = auth.to_str() {
                        if let Ok(claims) = validate_token(token.trim_start_matches("Bearer ")).await {
                            return Ok(next.run(axum::http::Request::try_from(request).unwrap()).await?);
                        }
                    }
                }
                Err((axum::http::StatusCode::UNAUTHORIZED, "Invalid token".into()))
            }
        },
    );

    axum::Server::bind(&SocketAddr::from(([127, 0, 0, 1], 3000)))
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Key practices to prevent TOCTOU with JWTs in Axum

  • Do not use request extensions populated in middleware as the sole source of truth for sensitive authorization checks.
  • Re-validate permissions at the point of action by querying a current permissions store or policy engine.
  • Treat JWT claims as identity and scope assertions at validation time, but verify operational permissions separately and synchronously.
  • Avoid long-lived cached permission mappings that can become stale; prefer short-lived or event-driven permission refreshes.

Frequently Asked Questions

Can middleware validation of JWT tokens alone prevent TOCTOU in Axum?
No. Middleware validation confirms token integrity and identity at the start of the request, but it does not guarantee that the authorization state remains valid at the point of sensitive operations. Always re-check permissions immediately before performing actions that affect security-sensitive resources.
How does re-fetching permissions mitigate TOCTOU when using JWT tokens in Axum?
Re-fetching permissions at the point of action ensures that the decision is based on the current authoritative state rather than a potentially stale snapshot captured in the JWT or in request extensions. This closes the race window between check and use by coupling authorization to real-time data.