To build robust and secure backend authentication systems using JWTs, developers often employ a dual-token strategy: an access token and a refresh token. The access token is your primary key to accessing protected resources. It's short-lived (e.g., 15 minutes) and is sent with every API request after the initial login. Its short lifespan is a crucial security measure; if an access token is intercepted, an attacker has a very limited window to misuse it, thereby minimizing potential damage.
Since re-authenticating the user every 15 minutes would be a terrible user experience, we introduce the refresh token. This token is long-lived (e.g., 7 days, 30 days) and is stored securely on the client side, typically in an HTTP-only cookie or secure storage. Its sole purpose is to obtain a new access token when the current one expires, without requiring the user to log in again. The refresh token is only sent to a specific refresh endpoint, not with every regular API request, further reducing its exposure.
Enhancing security even further is the concept of refresh token rotation. When a client uses a refresh token to request a new access token, the server not only issues a new access token but also a brand new refresh token, simultaneously invalidating the old one. This makes refresh tokens effectively "one-time use." If an attacker intercepts a refresh token, using it will render the legitimate client's token invalid, immediately alerting the system to potential compromise and making the stolen token useless for subsequent renewals. This proactive invalidation significantly mitigates the risk associated with long-lived refresh tokens.
Key Takeaways
- Access tokens are short-lived and grant access to resources.
- Refresh tokens are long-lived and grant new access tokens.
- Expiration limits the window of opportunity for stolen tokens.
- Refresh token rotation invalidates old tokens upon use, enhancing security.
- Always store refresh tokens securely (e.g., HTTP-only cookies).
Code Example
import jwt
import datetime
# In production, use a strong, environment-variable key!
SECRET = "your_super_secret_key_here"
def create_access_token(user_id: str) -> str:
payload = {
"user_id": user_id,
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=15) # Short-lived
}
return jwt.encode(payload, SECRET, algorithm="HS256")
def create_refresh_token(user_id: str) -> str:
payload = {
"user_id": user_id,
"exp": datetime.datetime.utcnow() + datetime.timedelta(days=7) # Long-lived
# For rotation, you'd add a unique ID (JTI) here and store it server-side
}
return jwt.encode(payload, SECRET, algorithm="HS256")How this code works
This code provides the core logic for generating JWT (JSON Web Token) access and refresh tokens, a common pattern for authentication in backend applications. It utilizes the jwt library for token operations and datetime for managing token lifetimes. A SECRET key is defined, which is vital for securely signing and verifying tokens; its importance is highlighted for production environments. The primary goal is to produce two types of tokens with different validities to balance security and user experience.
The create_access_token function constructs a payload dictionary with the user_id and an exp (expiration) timestamp. This token is designed to be short-lived, expiring after just 15 minutes, using datetime.timedelta. A subtle but important detail for beginners is the use of datetime.utcnow(): exp claims should always be in UTC to avoid timezone issues. The create_refresh_token function follows a similar structure but sets a much longer exp (7 days), allowing users to stay logged in without frequent credential re-entry. Both functions use jwt.encode with the SECRET and HS256 algorithm to convert the payload into a secure, signed token string.