In the ever-evolving world of authentication security, one term you've likely encountered—and possibly misunderstood—is "refresh token rotation." Especially within the NextAuth / Auth.js development community, there has been some confusion, with posts and discussions often equating refresh token rotation with merely renewing access tokens.
This mix-up can lead to security vulnerabilities and miss the true essence of refresh token rotation – to enhance security by regularly invalidating and issuing new refresh tokens alongside access tokens, as outlined in the OAuth specification. Even beyond OAuth, when implementing real refresh token rotation, it’s also a good idea to implement some kind of reuse detection with it.
In this blog, let's dive deeper into what refresh token rotation is, how it differs from reuse detection, and how you can implement it in a Python / Flask-based authentication service.
What is refresh token rotation?
Refresh token rotation is a security mechanism designed to minimize the risks associated with token theft and unauthorized use. In this process, each time a refresh token is used to acquire a new access token, a brand new refresh token is also generated and the previous one is invalidated. This strategy ensures that compromised refresh tokens lose their utility almost immediately, drastically reducing the potential for compromise.
Here’s a high level overview of how the authentication process works with refresh token rotation:
Despite its importance, refresh token rotation is often overlooked by developers crafting JWT-based stateless authentication services from scratch. Incorporating this feature is crucial in any production environment to safeguard against the hijacking and misuse of client-side refresh tokens.
Refresh token rotation vs reuse detection
As mentioned above, refresh token rotation is the process where a new refresh token is issued with each access token refresh request, with the old refresh token being rendered void immediately.
Refresh token reuse detection goes a step further by monitoring for attempts to use an already-used (or invalidated) refresh token. This can be a clear indicator of a potential security threat, such as a token being stolen and reused by an unauthorized party. Implementing reuse detection allows systems to react to such anomalies by revoking all tokens issued to the user, forcing a re-authentication and thus safeguarding the session.
Refresh token rotation in action
In this section, we’ll implement refresh token rotation with reuse detection in a Python-based authentication service to show you how it’s supposed to work. It involves creating a secure mechanism for issuing, rotating, and invalidating refresh tokens.
Architecture overview
Authentication Endpoint: To authenticate users and issue access and refresh tokens.
Token Refresh Endpoint: To exchange a valid refresh token for a new set of access and refresh tokens.
Database: To securely store user credentials, refresh tokens, and their metadata (such as expiry dates and token identifiers).
Security Layer: To handle encryption, token generation, and validation.
Below is a detailed example of the architecture and implementation steps for such a service using Python with Flask and an SQL database for storage.
1. Setup
First, ensure you have Flask installed in your Python environment:
pip install Flask Flask-SQLAlchemy
2. Define the database model
Using Flask-SQLAlchemy for simplicity, define a model for storing refresh tokens:
from flask_sqlalchemy import SQLAlchemy
from flask import Flask
from datetime import datetime
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///auth.db'
db = SQLAlchemy(app)
class RefreshToken(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, nullable=False)
token = db.Column(db.String(256), unique=True, nullable=False)
expires_at = db.Column(db.DateTime, nullable=False)
used = db.Column(db.Boolean, default=False, nullable=False)
def is_valid(self):
return datetime.utcnow() < self.expires_at and not self.used
3. User authentication
Implement an endpoint to authenticate users, issuing both access and refresh tokens upon successful authentication:
import jwt
from flask import request, jsonify
SECRET_KEY = "your_secret_key"
@app.route('/auth', methods=['POST'])
def authenticate():
# Dummy authentication logic
user_id = request.json.get('user_id')
if user_id:
access_token = jwt.encode({'user_id': user_id}, SECRET_KEY, algorithm='HS256')
refresh_token = jwt.encode({'user_id': user_id, 'type': 'refresh'}, SECRET_KEY, algorithm='HS256')
# Save the refresh token in the database with an expiry
db.session.add(RefreshToken(user_id=user_id, token=refresh_token, expires_at=datetime.utcnow() + timedelta(days=7)))
db.session.commit()
return jsonify(access_token=access_token, refresh_token=refresh_token)
else:
return jsonify(error="Authentication failed"), 401
4. Refresh token rotation with reuse detection
Create an endpoint to handle refresh token rotation. This endpoint will invalidate the old refresh token and issue new tokens:
@app.route('/refresh', methods=['POST'])
def refresh_token():
old_refresh_token = request.json.get('refresh_token')
decoded = jwt.decode(old_refresh_token, SECRET_KEY, algorithms=['HS256'])
# Verify the refresh token
stored_token = RefreshToken.query.filter_by(token=old_refresh_token, user_id=decoded['user_id']).first()
if stored_token:
if not stored_token.is_valid() or stored_token.used:
# Detected reuse of the refresh token, force logout and require re-authentication
db.session.query(RefreshToken).filter_by(user_id=decoded['user_id']).delete()
db.session.commit()
return jsonify(error="Invalid or reused refresh token, please re-authenticate"), 401
# Mark the old token as used
stored_token.used = True
db.session.commit()
# Proceed to issue new tokens
new_access_token = jwt.encode({'user_id': decoded['user_id']}, SECRET_KEY, algorithm='HS256')
new_refresh_token = jwt.encode({'user_id': decoded['user_id'], 'type': 'refresh'}, SECRET_KEY, algorithm='HS256')
db.session.add(RefreshToken(user_id=decoded['user_id'], token=new_refresh_token, expires_at=datetime.utcnow() + timedelta(days=7)))
db.session.commit()
return jsonify(access_token=new_access_token, refresh_token=new_refresh_token)
else:
return jsonify(error="Invalid or expired refresh token"), 401
This adjustment ensures that any attempt to use a refresh token more than once triggers a security response, effectively invalidating all tokens associated with the user and requiring a fresh login.
In a production environment, it's crucial to include additional security measures such as HTTPS, token signature validation, and comprehensive error handling. You should also consider using more robust authentication and database frameworks or services tailored to your specific security and scalability requirements.
Conclusion
Incorporating refresh token rotation and reuse detection into your authentication strategy makes your app more secure. This method reduces the risks associated with token theft and introduces an efficient way to monitor and act upon anomalous and malicious activities.
However, implementing such sophisticated security measures from scratch can be daunting. This is where leveraging a specialized authentication provider like Descope comes into play. For developers using Descope, activating refresh token rotation is literally a one-click checkbox.
While the guidelines provided in this blog serve as a robust starting point for refresh token rotation, it's crucial to adapt these strategies to align with your application's unique security demands and context. Opting for a comprehensive authentication solution like Descope not only simplifies this process but also guarantees adherence to the latest authentication security best practices, safeguarding your application and its users without added engineering effort.
If your curiosity is piqued, sign up for a Free Forever Descope account. If you have an active authentication project and need help, book time with our auth experts.