Authentication
Token Refresh and Revocation
Token refresh and revocation mechanisms are critical architectural patterns for managing user sessions securely in stateless environments. By decoupling short-lived access tokens from longer-lived refresh tokens, you limit the window of opportunity for an attacker if a credential is compromised. These strategies are essential when implementing robust authentication flows that prioritize both user experience and the ability to terminate active sessions immediately.
The Necessity of Dual-Token Architecture
To build a secure system, you must accept that tokens will eventually be leaked. If you use a single long-lived access token, a compromised token grants an attacker indefinite access until the secret key is rotated. By introducing a dual-token system, we split the responsibility. The Access Token, typically valid for 15-30 minutes, is sent with every API request to authorize access to protected routes. Because it is short-lived, the impact of a leak is minimized. The Refresh Token is stored securely, perhaps in an HttpOnly cookie, and is used solely to request a new pair of tokens when the access token expires. This design effectively forces a recurring 'heartbeat' check against your database, allowing you to verify that the user account has not been disabled or had its password changed, ensuring security remains synchronized with your backend state.
from fastapi import FastAPI, Depends
from datetime import datetime, timedelta
import jwt
# Short-lived access token (15 mins)
# Long-lived refresh token (7 days)
SECRET = "super-secure-key"
def create_tokens(user_id: str):
access = jwt.encode({"sub": user_id, "exp": datetime.utcnow() + timedelta(minutes=15)}, SECRET)
refresh = jwt.encode({"sub": user_id, "exp": datetime.utcnow() + timedelta(days=7)}, SECRET)
return {"access": access, "refresh": refresh}Implementing the Token Refresh Flow
The refresh flow is essentially a specialized login endpoint. When the frontend detects an expired access token, it must not ask the user for their credentials again, as that provides a poor experience. Instead, it hits a dedicated /refresh endpoint, sending the valid Refresh Token. Your FastAPI application validates the refresh token’s signature and expiration. Crucially, you must then look up the user in your database to ensure their account is still active—for instance, checking if the user was deleted or if the session was explicitly revoked. If valid, you issue a fresh access token (and optionally a new refresh token, known as refresh token rotation). This flow prevents the need for persistent re-authentication while maintaining the ability to cut off access by invalidating tokens stored in your database or by checking a 'last login' timestamp against the token's 'issued at' claim.
from fastapi import HTTPException, Header
@app.post("/refresh")
def refresh_token(refresh_token: str = Header(...)):
try:
payload = jwt.decode(refresh_token, SECRET, algorithms=["HS256"])
# Verify user is still allowed to access the system in the DB
new_access = jwt.encode({"sub": payload["sub"], "exp": datetime.utcnow() + timedelta(minutes=15)}, SECRET)
return {"access": new_access}
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Refresh token expired")Strategies for Token Revocation
Revocation is the 'kill switch' for stateless tokens. Since JWTs are self-contained, they are natively valid until expiration; the server does not usually consult the database to authorize every request. To bypass this, we use a blocklist. When a user logs out or if an admin bans an account, you store the token's unique identifier (jti) in a fast, distributed key-value store like Redis, setting the expiration to match the remaining life of the token. In your dependency chain, you check the blocklist before processing the request. While this adds a small latency penalty, it is the only way to achieve granular session control. Alternatively, you can store a 'token_version' or 'issued_after' timestamp on the user model, rejecting any tokens issued before that timestamp, which effectively revokes all previously issued tokens for that specific user.
import redis
# Connect to Redis to track revoked tokens
revoked_tokens = redis.Redis(host='localhost', port=6379, db=0)
def is_revoked(jti: str):
return revoked_tokens.exists(jti)
# Usage in dependency
def get_current_user(token: str):
payload = jwt.decode(token, SECRET, algorithms=["HS256"])
if is_revoked(payload.get("jti")):
raise HTTPException(status_code=401, detail="Token has been revoked")
return payloadRefresh Token Rotation for Enhanced Security
Refresh token rotation takes the security of the refresh flow to the next level by ensuring that every time a refresh token is used, it is consumed and replaced by a brand-new one. This is highly effective against token theft. If an attacker manages to steal a refresh token and uses it to obtain a new access token, the legitimate user will later attempt to use their own (now-stale) refresh token. Your server will detect that a 'used' token is being presented, which is a major red flag indicating a potential security breach. In response, you can immediately invalidate the entire refresh family for that user, forcing them to authenticate via credentials again. This creates a high-stakes environment for attackers, as any misuse or racing condition will likely result in the session being terminated, protecting the user's account.
# Concept: Mark old refresh token as used in DB
# If a token is presented that is marked as 'used', revoke all tokens for this user
def rotate_refresh_token(old_token_jti: str, user_id: str):
if db.is_token_used(old_token_jti):
db.revoke_all_sessions(user_id)
raise HTTPException(status_code=403, detail="Replay attack detected")
db.mark_as_used(old_token_jti)
return create_tokens(user_id)Handling Expiration and User Experience
Managing the expiration of tokens requires a thoughtful balance between security and user convenience. If access tokens are too short (e.g., 1 minute), your backend will be overwhelmed by constant refresh requests. If they are too long, the revocation window remains wide open for attackers. A practical strategy involves a silent background refresh: the frontend monitors the expiration claim of the JWT and triggers the /refresh endpoint automatically just before the token expires. If the refresh request itself fails—perhaps because the refresh token has also expired—the application must gracefully redirect the user to the login page. By implementing this 'silent refresh', the user stays authenticated as long as they are active, but remains subject to security policies, such as maximum session duration or account deactivation, without needing to manually re-login frequently.
from fastapi import Depends, HTTPException
def verify_access_token(token: str):
try:
return jwt.decode(token, SECRET, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
# Trigger client to call /refresh
raise HTTPException(status_code=401, headers={"WWW-Authenticate": "Bearer error=\"token_expired\""})
except jwt.InvalidTokenError:
raise HTTPException(status_code=401)Key points
- Access tokens should be short-lived to limit the damage from potential leaks.
- Refresh tokens must be stored securely and handled only by a specific refresh endpoint.
- Revocation is achieved by checking tokens against a database or high-speed cache.
- The /refresh endpoint validates user status before issuing new access credentials.
- Refresh token rotation detects theft by invalidating tokens that are used more than once.
- Storing a 'token_version' on the user object allows for instant global session revocation.
- A dual-token architecture balances security needs with a seamless user experience.
- Frontend applications should handle silent refresh requests to avoid manual re-authentication.
Common mistakes
- Mistake: Storing refresh tokens in browser localStorage. Why it's wrong: It is vulnerable to Cross-Site Scripting (XSS) attacks. Fix: Store refresh tokens in HTTP-only, secure, same-site cookies.
- Mistake: Keeping refresh tokens indefinitely. Why it's wrong: If a token is stolen, the attacker has permanent access. Fix: Implement a hard expiration date for refresh tokens even if they are rotated.
- Mistake: Not rotating refresh tokens upon use. Why it's wrong: It prevents the detection of token theft or reuse attacks. Fix: Issue a new refresh token every time a valid one is used to exchange for a new access token.
- Mistake: Relying on the client to delete tokens for revocation. Why it's wrong: Clients can be compromised or malicious, ignoring revocation logic. Fix: Maintain a server-side denylist or database flag to invalidate specific token IDs.
- Mistake: Using access tokens for revocation checks. Why it's wrong: Access tokens should be stateless to keep APIs fast; checking them against a database on every request adds too much latency. Fix: Revoke the refresh token to stop new access token generation, and let the short-lived access token expire naturally.
Interview questions
What is the basic purpose of a refresh token in a FastAPI authentication flow?
In FastAPI, access tokens are designed to be short-lived to minimize the security impact if they are intercepted. The refresh token serves as a long-lived credential that allows the client to request a new, valid access token without forcing the user to re-enter their credentials. This improves user experience while maintaining robust security, as you can continuously rotate access tokens while keeping the underlying authentication session valid for a longer duration.
How do you typically implement token revocation in a FastAPI application?
FastAPI does not have built-in stateful token revocation by default because JWTs are stateless. To implement revocation, you must maintain a blocklist, typically using a high-speed key-value store like Redis. When a user logs out, you store the token's unique identifier (jti) in Redis with an expiration time equal to the token's remaining lifespan. In your FastAPI dependency, you check this blocklist on every request; if the jti exists in Redis, you raise an HTTPException to deny access.
Compare the 'Allowlist' versus 'Blocklist' approach for token management in FastAPI.
The blocklist approach involves invalidating specific tokens, which is reactive and allows for immediate logout functionality. However, it requires a database check on every protected FastAPI route, potentially increasing latency. The allowlist approach, conversely, only considers a token valid if its identifier is currently present in a database table. This is more secure as it permits instant session termination and global 'log out everywhere' features, but it places a heavier load on your database compared to the simpler blocklist method.
Explain how to structure a FastAPI dependency to validate both the token and its revocation status simultaneously.
You should create a dependency function decorated with FastAPI's Depends. Inside, use PyJWT to decode the token. Once validated for signature and expiration, extract the 'jti' claim. Then, perform an asynchronous call to your cache (like Redis) to see if that 'jti' has been revoked. If the check passes, return the user object or the payload. This ensures your endpoint logic remains clean and decoupled from the security verification process, making the code highly reusable across your application.
Why is token rotation essential when using refresh tokens in a FastAPI backend?
Token rotation is a security best practice where every time a refresh token is used to obtain a new access token, the old refresh token is invalidated and a brand-new one is issued. This mitigates the risk of token theft. If an attacker manages to steal a refresh token and uses it, the legitimate user will eventually attempt to use their own (now invalidated) token. This mismatch alerts your FastAPI backend that a potential breach has occurred, allowing you to revoke all sessions associated with that user immediately.
How would you handle race conditions in FastAPI when multiple concurrent requests attempt to use the same refresh token?
Race conditions often occur in distributed FastAPI environments when a client sends parallel requests using the same refresh token. To prevent this, you must implement atomic operations in your database or cache. When a refresh request arrives, use a 'set-if-not-exists' command or a Lua script in Redis to lock the token identifier. This ensures only one process can successfully exchange the refresh token, while subsequent identical requests are rejected. This prevents an attacker from abusing stolen tokens while maintaining high availability for your users.
Check yourself
1. When implementing refresh token rotation in FastAPI, what is the primary benefit of issuing a new refresh token with every request?
- A.It reduces the size of the JWT payload
- B.It allows the server to detect if a stolen refresh token has been reused
- C.It eliminates the need for database lookups entirely
- D.It forces the user to re-authenticate every time they refresh
Show answer
B. It allows the server to detect if a stolen refresh token has been reused
Rotating refresh tokens allows you to detect reuse. If a refresh token is used twice, the server knows it was leaked and can invalidate the entire session. Option 0 is unrelated, 2 is false as you need a DB to track the tokens, and 3 defeats the purpose of the refresh mechanism.
2. A developer wants to revoke a user's access immediately. Given that access tokens are stateless JWTs, how should they handle this in FastAPI?
- A.Immediately delete the user from the database
- B.Include the user's ID in an in-memory set that is checked on every request
- C.Invalidate the refresh token in the database to prevent future access token generation
- D.Force the client to overwrite the access token with an empty string
Show answer
C. Invalidate the refresh token in the database to prevent future access token generation
Since JWTs are stateless, you cannot 'delete' an active one. Invalidating the refresh token ensures no further access tokens can be obtained. Option 0 is overkill, 1 causes performance bottlenecks, and 3 is a client-side action that the server cannot trust.
3. Why is it recommended to store refresh tokens in an HTTP-only cookie rather than the request body?
- A.It makes the FastAPI dependency injection system faster
- B.It automatically prevents the token from being accessed by JavaScript via document.cookie
- C.It allows the browser to automatically refresh the token without calling an API endpoint
- D.It is the only way to support OAuth2 password flows
Show answer
B. It automatically prevents the token from being accessed by JavaScript via document.cookie
HTTP-only cookies prevent XSS scripts from reading the token. Option 0 is false, 2 is false because a refresh endpoint is still required, and 3 is false as cookies are just a transport mechanism.
4. If you are using a database to track refresh tokens for revocation, what is the best strategy to handle the performance impact?
- A.Use a high-speed cache like Redis to check token validity
- B.Perform a full table scan on every request
- C.Only verify the token's signature and ignore the database status
- D.Increase the FastAPI response timeout to allow for slow queries
Show answer
A. Use a high-speed cache like Redis to check token validity
Redis provides sub-millisecond latency for token lookups, minimizing the impact on API performance. 1 is too slow, 2 ignores security requirements, and 3 results in a poor user experience.
5. Which of the following is the most secure expiration strategy for access and refresh tokens in FastAPI?
- A.Short-lived access tokens and long-lived refresh tokens
- B.Long-lived access tokens and short-lived refresh tokens
- C.Both set to expire at the same time to ensure consistency
- D.No expiration for either to ensure a seamless user experience
Show answer
A. Short-lived access tokens and long-lived refresh tokens
Short-lived access tokens limit the window of risk if stolen, while long-lived refresh tokens ensure the user doesn't have to log in frequently. Other options either decrease security or force bad user experiences.