Session Fixation in Fastapi with Jwt Tokens
Session Fixation in Fastapi with Jwt Tokens — how this specific combination creates or exposes the vulnerability
Session fixation occurs when an application assigns a user a session identifier before authentication and does not rotate that identifier after login. With FastAPI and JWT-based authentication, fixation risk arises when an unauthenticated client is allowed to embed a chosen JWT-related value (such as a "fake" access token or a predictable reference) and that value is later accepted as the authenticated session token without strict validation and rotation. Because JWT authentication in FastAPI is commonly implemented using bearer tokens passed in the Authorization header, developers may mistakenly treat any presented JWT as proof of identity without ensuring the token was issued post-authentication to the authenticated subject.
Consider a FastAPI app that allows an unauthenticated client to supply a token string via a header or cookie (for example, for testing or legacy reasons) and then uses that token to set user identity without verifying issuance after login. An attacker can craft a token with a known subject or a weak signing key and induce the victim to use it. When the victim authenticates, the server may accept the attacker-supplied JWT as the authenticated session token because it validates the signature but does not enforce token rotation or binding to the authentication event. This creates a session fixation condition where the attacker’s chosen token becomes the victim’s authenticated token, enabling unauthorized access.
A concrete attack flow: (1) Attacker obtains a JWT signed with a weak or known key or tricks the app into accepting a token it issues (e.g., via an unauthenticated endpoint that echoes a token); (2) Victim visits a malicious link that sets the token in an Authorization header or cookie; (3) Victim logs in successfully, but the server accepts the attacker-chosen JWT as the authenticated token because it does not require a fresh token to be issued after successful authentication or bind the token to the authenticated user’s session context; (4) Attacker uses the same JWT to access protected endpoints as the victim. Even when using strong signing keys, fixation can occur if the application logic conflates token possession with authentication without re-issuing a new JWT tied to the authenticated subject.
JWTs are often considered stateless, which can lead to the misconception that session fixation is not applicable. However, fixation is about the trust boundary between token assignment and authentication. If your FastAPI routes accept an Authorization bearer token before authentication and then reuse the same token identity after login without verifying that the token was minted post-authentication, you expose a fixation vector. Additionally, storing JWTs in cookies without the Secure and HttpOnly flags, or without SameSite enforcement, can facilitate token fixation via cookie manipulation.
To detect this class of issue, scanners check whether the application accepts unverified or user-supplied JWTs before login and whether the token subject (sub claim) is re-validated and re-bound after authentication. They also verify that tokens are rotated upon login and that strict issuer/audience/signature validation is enforced. Without these controls, an attacker can force a known token on a user and hijack the authenticated session despite the use of JWTs.
Jwt Tokens-Specific Remediation in Fastapi — concrete code fixes
Remediation centers on ensuring that after successful authentication, the server issues a new JWT bound to the authenticated subject and does not accept user-supplied tokens as proof of identity. Always validate signature, issuer, audience, and expiration rigorously, and rotate the token at login. Below are concrete, realistic code examples for FastAPI that demonstrate secure handling of JWTs to prevent session fixation.
First, define secure token utilities using PyJWT and enforce strict validation parameters:
import jwt
from datetime import datetime, timedelta, timezone
from typing import Optional
SECRET_KEY = 'your-strong-secret'
ALGORITHM = 'HS256'
ISSUER = 'myapi.example.com'
AUDIENCE = 'myapi-users'
def create_access_token(subject: str) -> str:
to_encode = {
'sub': subject,
'iat': datetime.now(timezone.utc),
'exp': datetime.now(timezone.utc) + timedelta(minutes=30),
'iss': ISSUER,
'aud': AUDIENCE,
'jti': str(uuid4()) # unique token ID to support rotation
}
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def decode_token(token: str) -> dict:
return jwt.decode(
token,
SECRET_KEY,
algorithms=[ALGORITHM],
issuer=ISSUER,
audience=AUDIENCE
)
Second, implement login and token rotation so that the authenticated session receives a freshly signed JWT and any pre-authentication token is discarded:
from fastapi import Depends, HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
security_scheme = HTTPBearer()
def get_current_user(credentials: HTTPAuthorizationCredentials = Security(security_scheme)):
token = credentials.credentials
try:
payload = decode_token(token)
# Ensure the token was issued after login by checking a per-user token revocation list or DB
# Example: raise if jti is in a logout denylist
return {'sub': payload['sub'], 'token_jti': payload.get('jti')}
except jwt.PyJWTError:
raise HTTPException(status_code=401, detail='Invalid token')
@app.post('/login')
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
# Validate credentials against your user store
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=401, detail='Incorrect credentials')
# Issue a fresh token after authentication — do NOT reuse form_data.token or any client-supplied token
new_token = create_access_token(subject=user.username)
return {'access_token': new_token, 'token_type': 'bearer'}
@app.get('/protected')
async def protected_route(user: dict = Depends(get_current_user)):
return {'message': f'Hello {user["sub"]}'}
Third, protect cookies and headers by enforcing strict validation and avoiding acceptance of client-chosen tokens before login. If you store JWTs in cookies, set Secure, HttpOnly, and SameSite=Strict/Lax and avoid automatically using a cookie token for authentication prior to login:
from fastapi import Response
@app.post('/login')
async def login(form_data: OAuth2PasswordRequestForm = Depends(), response: Response = None):
user = authenticate_user(form_data.username, form_data.password)
if not user:
raise HTTPException(status_code=401, detail='Incorrect credentials')
new_token = create_access_token(subject=user.username)
response.set_cookie(
key='access_token',
value=new_token,
httponly=True,
secure=True,
samesite='Strict',
max_age=1800
)
return {'message': 'Logged in'}
@app.get('/token-from-cookie')
async def read_token_cookie(token: str = Cookie(None)):
# Do not authenticate using a client-supplied cookie token before login
if token is None:
raise HTTPException(status_code=401, detail='Missing token')
# Validate strictly; do not treat this as proof of identity for login
payload = decode_token(token)
return {'sub': payload['sub']}
Finally, maintain a minimal token revocation or denylist strategy for logged-out tokens and prefer short expirations to reduce the impact of a fixed token. Combine these practices with HTTPS to protect tokens in transit and avoid exposing raw JWTs in URLs or logs.