Securing Your Web Applications with JWT Authentication and Refresh Token Rotation

In today’s digital landscape, user authentication and secure access management are critical components of any web application. As developers, we strive to strike the perfect balance between security and user experience, ensuring that our users can access services seamlessly while minimizing the risk of unauthorized access. One of the most popular methods for managing authentication in modern web applications is JWT (JSON Web Token). JWTs allow developers to handle authentication in a stateless way, but with that convenience comes the need for enhanced security measures. In particular, refresh token rotation plays a crucial role in maintaining a secure environment by mitigating risks associated with token theft and replay attacks. This article will explore JWT authentication in detail, the risks associated with token-based authentication, and how refresh token rotation can bolster your app’s security. Finally, we’ll show you how to implement this strategy in FastAPI, one of the fastest-growing Python frameworks.

What is JWT Authentication?

JSON Web Tokens (JWTs) are a compact, URL-safe means of representing claims between two parties, most commonly the client (browser or mobile app) and the server (API). JWTs are popular for user authentication because they enable stateless communication, where the server doesn’t need to maintain session data, which simplifies scalability.

A JWT consists of three parts:

  1. Header: Contains the algorithm used to sign the token and the type (JWT).

  2. Payload: Contains the actual data or claims (e.g., user ID, expiration time).

  3. Signature: Ensures the token hasn’t been altered using the server’s secret key.

When a user logs in, the server generates a JWT and sends it to the client. The client then includes this token in subsequent requests, allowing the server to verify the user’s identity without needing to track sessions.

The Lifespan of Tokens: The Trade-off

To balance security and usability, we often divide tokens into two types:

  1. Access Tokens: Short-lived tokens (usually 15–30 minutes) that allow users to access protected resources.

  2. Refresh Tokens: Long-lived tokens (days or weeks) that allow the client to obtain new access tokens without forcing the user to log in again.

While short-lived access tokens provide better security (since they expire quickly), they can become a usability problem if the user is forced to log in repeatedly. On the other hand, refresh tokens help solve this issue by allowing users to stay logged in for longer without sacrificing security. 

The Problem with Refresh Tokens: The Security Risk

Though refresh tokens help solve the usability issue, they come with a security risk: What happens if a refresh token is stolen?

If an attacker steals a refresh token, they could continually request new access tokens, effectively keeping the attacker “logged in” even if the original access token expires. This is where refresh token rotation comes into play.  

What is Refresh Token Rotation?

Refresh token rotation is a security strategy designed to mitigate the impact of stolen refresh tokens. The idea is simple: every time a refresh token is used to obtain a new access token, the server also issues a new refresh token. The old refresh token is invalidated and can no longer be used.

Here’s how it works:

  1. Initial Login: The user logs in and receives both an access token (short-lived) and a refresh token (long-lived).

  2. Access Token Expiration: When the access token expires, the client sends the refresh token to the server to get a new access token.

  3. Token Rotation: The server verifies the refresh token, issues a new access token, and rotates the refresh token ,  issuing a new one and invalidating the old one.

  4. Increased Security: If a refresh token is stolen, it can only be used once. After it’s used, the token is no longer valid, minimising the risk of replay attacks. This approach ensures that even if an attacker gets hold of a refresh token, they can only use it one time before it becomes useless.

Benefits of Refresh Token Rotation

  1. Mitigates Replay Attacks: Once a refresh token is used, it’s no longer valid, making it difficult for attackers to reuse a stolen token.

  2. Improves User Experience: Users don’t need to log in frequently since the refresh token enables them to obtain new access tokens automatically.

  3. Reduces Long-Term Vulnerabilities: Even if an attacker gets hold of a refresh token, the damage is limited to a single session.

General Strategies for Securing JWT-Based Systems

When implementing JWT authentication, it’s important to adopt some best practices to minimize security risks:

1. Short Access Token Lifetimes

The shorter the access token lifetime, the smaller the window for potential misuse. However, make sure to balance this with user experience , too short a lifespan may force users to refresh tokens too often.

2. Use Secure Algorithms

Always use secure algorithms (e.g., HS256 or RS256) for signing JWTs. This ensures that tokens cannot be tampered with without detection.

3. Secure Refresh Token Storage

Store refresh tokens securely on the client side. For web applications, use HTTP-only cookies. Avoid localStorage or sessionStorage, which are vulnerable to XSS attacks.

4. Implement Token Rotation

Rotate refresh tokens every time they are used. This ensures that even if a refresh token is stolen, it cannot be reused after the first use.

5. Revoke Tokens on Logout

Always have a mechanism to revoke refresh tokens on logout or when suspicious activity is detected. This can be done by tracking tokens in a database or using a blacklist.

6. Multi-Factor Authentication (MFA)

Enhance security by implementing multi-factor authentication (MFA), especially for sensitive actions or after token expiration.

7. Use Secure Cookies

When using cookies to store tokens, make them HTTP-only and SameSite=Strict to prevent XSS and CSRF attacks.

Example: JWT Authentication with Refresh Token Rotation in FastAPI

Let’s see how all this theory translates into a real-world implementation using FastAPI.

Step 1: Install Dependencies

pip install fastapi[all] pyjwt passlib bcrypt

Step 2: Create Utility Functions for Tokens

import jwtfrom datetime import datetime, timedelta
SECRET_KEY = "your_secret_key"
ALGORITHM = "HS256"

def create_access_token(data: dict):
    expire = datetime.utcnow() + timedelta(minutes=15)
    data.update({"exp": expire})
    return jwt.encode(data, SECRET_KEY, algorithm=ALGORITHM)

def create_refresh_token(data: dict):
    expire = datetime.utcnow() + timedelta(days=7)
    data.update({"exp": expire})
    return jwt.encode(data, SECRET_KEY, algorithm=ALGORITHM)

def decode_token(token: str):
    try:
        return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    except jwt.ExpiredSignatureError:
        return None
    except jwt.InvalidTokenError:
        return None

Step 3: Implement the Token Rotation Endpoint

from fastapi import FastAPI, HTTPException, status

app = FastAPI()
used_refresh_tokens = set()

@app.post("/token/refresh")
async def refresh_token(refresh_token: str):
    payload = decode_token(refresh_token)
    if not payload or refresh_token in used_refresh_tokens:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired refresh token",
        )
    username = payload.get("sub")
    used_refresh_tokens.add(refresh_token) # Mark the token as used
    new_access_token = create_access_token({"sub": username})
    new_refresh_token = create_refresh_token({"sub": username})
    return {
        "access_token": new_access_token,
        "refresh_token": new_refresh_token,
        }

Step 4: Protect Routes with JWT Authentication

from fastapi import Depends

async def get_current_user(token: str = Depends(oauth2_scheme)):
    payload = decode_token(token)
    if not payload:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired token",
            )
    return payload["sub"]

@app.get("/users/me")
async def read_users_me(current_user: str = Depends(get_current_user)):
    return {"username": current_user}

Conclusion: Balancing Security and User Experience

JWT authentication is powerful, but like any security mechanism, it requires careful consideration to implement effectively. Refresh token rotation is a crucial tool in the fight against token theft and replay attacks, allowing you to offer a seamless user experience while maintaining a high level of security. By applying the best practices we’ve discussed ,  such as rotating refresh tokens, shortening access token lifetimes, and using secure storage,  you can build an authentication system that is robust, scalable, and secure. FastAPI provides an excellent platform for implementing these techniques, but these principles apply to any modern web framework. Whether you’re using Django, Flask, or Express, the strategies outlined here will help you build secure, user-friendly authentication systems. Happy coding, and stay secure!