auth/managing-sessions-tokens/SKILL.md
JWT access tokens, refresh tokens, cookie management, token rotation, and revocation strategies.
npx skillsauth add 7a336e6e/skills managing-sessions-tokensInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Implement a JWT-based authentication token system with short-lived access tokens, long-lived refresh tokens stored in httpOnly cookies, token rotation on refresh, and a revocation mechanism for logout.
A JWT consists of three parts: header, payload, and signature.
Header:
{ "alg": "HS256", "typ": "JWT" }
Payload (claims):
{
"sub": "user-uuid",
"exp": 1700000000,
"iat": 1699999100,
"role": "user"
}
sub -- Subject, the user's unique identifier.exp -- Expiration time as a Unix timestamp.iat -- Issued-at time as a Unix timestamp.role -- The user's role for authorization checks.Access tokens are short-lived (15 minutes) and sent in the Authorization header.
import jwt
from datetime import datetime, timedelta, timezone
SECRET_KEY = app.config["JWT_SECRET_KEY"]
ACCESS_TOKEN_EXPIRY = timedelta(minutes=15)
def generate_access_token(user) -> str:
payload = {
"sub": str(user.id),
"role": user.role,
"iat": datetime.now(timezone.utc),
"exp": datetime.now(timezone.utc) + ACCESS_TOKEN_EXPIRY,
}
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
def decode_access_token(token: str) -> dict:
return jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
Refresh tokens are long-lived (7 days) and stored in an httpOnly, Secure, SameSite cookie. They are used to obtain new access tokens without requiring the user to log in again.
import secrets
REFRESH_TOKEN_EXPIRY = timedelta(days=7)
def generate_refresh_token(user) -> str:
"""Generate an opaque refresh token and store it in the database."""
raw_token = secrets.token_urlsafe(32)
refresh = RefreshToken(
user_id=user.id,
token_hash=hashlib.sha256(raw_token.encode()).hexdigest(),
expires_at=datetime.now(timezone.utc) + REFRESH_TOKEN_EXPIRY,
)
db.session.add(refresh)
db.session.commit()
return raw_token
def set_refresh_cookie(response, token: str):
response.set_cookie(
"refresh_token",
value=token,
httponly=True,
secure=True,
samesite="Strict",
max_age=int(REFRESH_TOKEN_EXPIRY.total_seconds()),
path="/api/v1/auth/refresh",
)
Each time a refresh token is used, issue a new refresh token and invalidate the old one. This limits the window of exposure if a refresh token is compromised.
@auth_bp.route("/refresh", methods=["POST"])
def refresh():
raw_token = request.cookies.get("refresh_token")
if not raw_token:
return jsonify({"error": "Missing refresh token"}), 401
token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
stored = RefreshToken.query.filter_by(token_hash=token_hash, revoked=False).first()
if not stored or stored.expires_at < datetime.now(timezone.utc):
return jsonify({"error": "Invalid or expired refresh token"}), 401
# Revoke the old token
stored.revoked = True
db.session.commit()
user = User.query.get(stored.user_id)
# Issue new tokens
access_token = generate_access_token(user)
new_refresh = generate_refresh_token(user)
response = jsonify({"access_token": access_token})
set_refresh_cookie(response, new_refresh)
return response, 200
Maintain a blocklist for logged-out access tokens. On logout, revoke the refresh token and add the access token's jti (or the token itself) to the blocklist until it expires.
@auth_bp.route("/logout", methods=["POST"])
def logout():
# Revoke refresh token
raw_token = request.cookies.get("refresh_token")
if raw_token:
token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
stored = RefreshToken.query.filter_by(token_hash=token_hash).first()
if stored:
stored.revoked = True
db.session.commit()
# Blocklist the access token
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
access_token = auth_header.split(" ", 1)[1]
try:
payload = decode_access_token(access_token)
ttl = payload["exp"] - int(datetime.now(timezone.utc).timestamp())
if ttl > 0:
redis_client.setex(f"blocklist:{access_token}", ttl, "1")
except jwt.InvalidTokenError:
pass
response = jsonify({"message": "Logged out"})
response.delete_cookie("refresh_token", path="/api/v1/auth/refresh")
return response, 200
from functools import wraps
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
return jsonify({"error": "Missing token"}), 401
token = auth_header.split(" ", 1)[1]
# Check blocklist
if redis_client.get(f"blocklist:{token}"):
return jsonify({"error": "Token revoked"}), 401
try:
payload = decode_access_token(token)
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
request.current_user = payload
return f(*args, **kwargs)
return decorated
Refresh success (200):
{ "access_token": "<new-jwt>" }
Set-Cookie header with new refresh token.
Logout success (200):
{ "message": "Logged out" }
Token error (401):
{ "error": "Token expired" }
development
Implement features using the Red-Green-Refactor cycle to ensure testability and correctness from the start.
data-ai
Manage the `tasks.md` ledger with strict locking and collision avoidance protocols to allow multiple agents to work in parallel safely.
development
The git-workflow skill defines branching conventions, commit message formats, and pull request standards that all agents must follow for consistent version control.
development
The environment-config skill standardizes how agents manage environment variables, secrets, and application configuration across local development and deployed environments.