managing-sessions-tokens

star 1

JWT access tokens, refresh tokens, cookie management, token rotation, and revocation strategies.

7a336e6e By 7a336e6e schedule Updated 2/5/2026

name: managing-sessions-tokens description: JWT access tokens, refresh tokens, cookie management, token rotation, and revocation strategies.

Managing Sessions and Tokens

Goal

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.

When to Use

  • After implementing local auth or OAuth, to manage authenticated sessions.
  • When transitioning from server-side sessions to stateless JWT-based auth.
  • When building an API that needs to support both browser clients (cookies) and mobile clients (bearer tokens).
  • When adding logout or "sign out everywhere" functionality.

Instructions

JWT Structure

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 Token

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 Token

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",
    )

Token Rotation

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

Token Revocation

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

Token Verification Decorator

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

Constraints

✅ Do

  • Use short expiry (15 minutes) for access tokens.
  • Set httpOnly, Secure, and SameSite=Strict flags on refresh token cookies.
  • Rotate refresh tokens on every use, invalidating the previous token.
  • Revoke both access and refresh tokens on logout.
  • Scope the refresh cookie path to the refresh endpoint only.
  • Store refresh tokens as hashed values in the database.

❌ Don't

  • Store tokens in localStorage or sessionStorage; use httpOnly cookies for refresh tokens.
  • Use long-lived access tokens (more than 30 minutes).
  • Skip token revocation on logout; users expect logout to be immediate.
  • Embed sensitive data (passwords, PII) in the JWT payload.
  • Use the same secret key for access tokens and other application secrets.
  • Skip blocklist checks when verifying access tokens.

Output Format

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" }

Dependencies

Install via CLI
npx skills add https://github.com/7a336e6e/skills --skill managing-sessions-tokens
Repository Details
star Stars 1
call_split Forks 2
navigation Branch main
article Path SKILL.md
More from Creator