Session Fixation in Axum with Jwt Tokens
Session Fixation in Axum with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Session fixation in Axum when JWT tokens are used occurs when an application accepts an attacker-provided token value or allows a predictable token to be set before authentication. With JWT-based sessions, the token itself often functions as the session identifier. If Axum endpoints accept a JWT from query parameters, headers, or cookies without validating provenance, an attacker can fixate a token on a victim’s browser and later use it after the victim authenticates.
Consider a login flow where Axum issues a JWT but does not rotate or explicitly bind the token to a freshly authenticated principal. If the application previously allowed unauthenticated requests to include a token query parameter that is later accepted post-login, an attacker can craft a link like https://api.example.com/login?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.... After the victim logs in, the same token is used to access protected routes, and because the server trusts the token without additional binding (e.g., to a nonce or session metadata), the authentication is effectively hijacked.
JWTs in Axum are often validated using middleware that checks signatures and claims. If the validation logic does not enforce strict issuer, audience, and jti (JWT ID) checks, or if tokens are accepted without verifying a one-time use or per-session binding, the fixed token remains valid across authentication boundaries. This is particularly risky when tokens contain embedded user identifiers and are stored in cookies with lax path/domain settings, enabling cross-origin leakage via referrers or insecure JavaScript access.
Common misconfigurations include:
- Accepting JWTs from URL query strings, which can be leaked in logs, Referer headers, or browser history.
- Not setting the
HttpOnlyandSecureflags on cookies storing tokens, enabling XSS-assisted fixation. - Failing to associate JWTs with a server-side session context or a per-authentication nonce, making token replay feasible after login.
Real-world analogs align with OWASP API Top 10 2023 A07:2023 — Identification and Authentication Failures, where broken session management enables takeover. Unlike traditional cookie-based sessions, JWTs require explicit rotation and binding to mitigate fixation; otherwise, the token value remains static across the authentication transition.
Jwt Tokens-Specific Remediation in Axum — concrete code fixes
Remediation focuses on ensuring JWTs are not predictable, not leakable via URLs, and strictly bound to authenticated sessions. In Axum, implement the following patterns using the jsonwebtoken crate and secure cookie handling.
1. Do not accept JWTs from query parameters
Reject or ignore JWTs passed via URLs. Configure your extractor to only read tokens from Authorization: Bearer headers or secure, HttpOnly cookies.
use axum::headers::authorization::{Authorization, Bearer};
use axum::extract::Request;
use axum::async_trait;
// Custom extractor that only allows Authorization header
pub struct JwtToken(String);
#[axum::async_trait]
impl FromRequest<S> for JwtToken
where
S: Send + Sync,
{
type Rejection = (axum::http::StatusCode, String);
async fn from_request(req: Request, _state: &S) -> Result {
let auth = req.headers().get(axum::http::header::AUTHORIZATION)
.ok_or((axum::http::StatusCode::UNAUTHORIZED, "Missing Authorization header".to_string()))?;
let bearer = auth.parse::<Bearer>().map_err(|_| (axum::http::StatusCode::UNAUTHORIZED, "Invalid Authorization format".to_string()))?;
Ok(JwtToken(bearer.token().to_string()))
}
}
2. Rotate tokens upon login and bind to user context
Issue a new JWT after successful authentication and avoid reusing pre-authentication tokens. Include a jti (JWT ID) claim and optionally bind to the user’s session fingerprint.
use jsonwebtoken::{encode, Header, EncodingKey, Algorithm};
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct Claims {
sub: String,
jti: String, // unique per authentication event
iss: String,
aud: String,
exp: usize,
iat: usize,
// optional: bind to a device fingerprint or nonce
nonce: String,
}
fn issue_token(user_id: &str, nonce: &str) -> String {
let claims = Claims {
sub: user_id.to_string(),
jti: uuid::Uuid::new_v4().to_string(),
iss: "api.example.com".to_string(),
aud: "api.example.com".to_string(),
exp: (chrono::Utc::now() + chrono::Duration::hours(1)).timestamp() as usize,
iat: chrono::Utc::now().timestamp() as usize,
nonce: nonce.to_string(),
};
encode(
&Header::new(Algorithm::HS256),
&claims,
&EncodingKey::from_secret("super-secret-key-change-in-prod".as_ref()),
).expect("Failed to encode token")
}
3. Store tokens securely in cookies with strict attributes
If using cookies to store JWTs, set HttpOnly, Secure, SameSite=Strict, and limit the path.
use axum::response::Response;
use axum::http::{Cookie, CookieJar};
pub fn set_secure_token_cookie(mut response: Response, token: &str) -> Response {
let cookie = Cookie::build(("token", token))
.http_only(true)
.secure(true)
.same_site(axum::http::cookie::SameSite::Strict)
.path("/")
.max_age(time::Duration::hours(1).to_std().unwrap())
.finish();
response.headers_mut().insert(
axum::http::header::SET_COOKIE,
cookie.to_string().parse().unwrap(),
);
response
}
4. Validate claims rigorously and enforce short lifetimes
Use strong validation including issuer, audience, and jti checks. Short-lived tokens reduce the window for fixation and replay.
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
fn validate_token(token: &str) -> Result<jsonwebtoken::TokenData<Claims>, jsonwebtoken::errors::Error> {
let validation = Validation::new(Algorithm::HS256);
let mut validation = validation;
validation.validate_exp = true;
validation.validate_iss = true;
validation.validate_aud = true;
validation.required_spec_claims = vec!["iss".into(), "aud".into(), "exp".into(), "jti".into()];
decode::<Claims>(
token,
&DecodingKey::from_secret("super-secret-key-change-in-prod".as_ref()),
&validation,
)
}