How RomM Handles Token Refresh for API Authentication: Secure OAuth Implementation

RomM implements a secure, single-use refresh token flow using Redis-backed JWT storage to prevent replay attacks and enforce automatic token rotation.

RomM, the open-source ROM management system, secures its API endpoints using a custom OAuthHandler that manages token lifecycle through Redis-backed storage. When handling token refresh for API authentication, the application employs a strict rotation mechanism that guarantees each refresh token can only be used once, eliminating the risk of credential replay while maintaining seamless user sessions.

Token Creation and Redis Storage

Generating the Refresh Token

When a client authenticates with a password, RomM’s OAuthHandler creates both a short-lived access token and a long-lived refresh token. In backend/handler/auth/base_handler.py, the create_refresh_token() method (lines 82-98) handles this process:

  • It copies the authentication payload and adds a unique JWT ID (jti) Claim
  • It sets the type claim to "refresh" to distinguish it from access tokens
  • It signs the JWT using the symmetric key ROMM_AUTH_SECRET_KEY with a configurable expires_delta

This implementation ensures that every refresh token carries a unique identifier that can be tracked and invalidated independently.

Single-Use Storage Mechanism

Immediately after creation, RomM stores the token’s metadata in Redis to enforce single-use semantics. At lines 98-102 of the base handler, the system executes:

  • Storage of the key refresh-jti:<jti> with the value b"valid"
  • TTL (time-to-live) set equal to the token’s lifetime
  • Automatic expiration when the TTL elapses

This Redis entry acts as a "ticket" that must be present for the token to be valid. Once consumed, the key is permanently removed, preventing any possibility of reuse.

Token Validation and Consumption

The Consumption Process

When a client sends a request with grant_type=refresh_token to the /api/auth endpoint, RomM invokes OAuthHandler.consume_refresh_token() (lines 105-138 in backend/handler/auth/base_handler.py). This method performs rigorous validation:

  1. Signature verification – Validates the JWT signature using ROMM_AUTH_SECRET_KEY
  2. Expiry check – Compares the current UTC time against the exp claim; expired tokens raise OAuthCredentialsException
  3. Issuer validation – Confirms the iss claim equals "romm:oauth"
  4. Type confirmation – Ensures the type claim is "refresh"

Atomic Deletion and User Verification

The critical security step occurs via Redis atomic operations. The handler uses redis_client.getdel() to simultaneously retrieve and delete the refresh-jti:<jti> key:

  • If the key is missing or not equal to b"valid", the token is rejected immediately
  • If valid, the sub claim extracts the username for database lookup via db_user_handler.get_user_by_username()
  • The user must exist and be enabled; otherwise, authentication fails

This atomic getdel() operation guarantees that even concurrent requests with the same token cannot succeed twice, effectively implementing token rotation at the infrastructure level.

API Endpoint Integration

The /api/auth endpoint in backend/endpoints/auth.py orchestrates the refresh workflow. At lines 130-138, it inspects form_data.grant_type and routes refresh requests to the handler:


# From backend/endpoints/auth.py

if form_data.grant_type == "refresh_token":
    user, claims = await oauth_handler.consume_refresh_token(
        form_data.refresh_token
    )

Upon successful consumption (lines 157-168), the endpoint immediately issues:

  • A new access token with a short expiration (e.g., 5 minutes)
  • A fresh refresh token with a new jti and updated expiration

This rotation ensures that clients always receive a new credential pair, while the old refresh token becomes permanently invalid.

Code Example: Implementing the Refresh Flow

The following examples demonstrate the core implementation patterns found in RomM’s source code:


# 1️⃣ Issue a refresh token (after successful password authentication)

payload = {
    "sub": user.username,          # User identifier

    "iss": "romm:oauth",           # Issuer flag

}
refresh_token = oauth_handler.create_refresh_token(
    data=payload,
    expires_delta=timedelta(minutes=30),
)

# 2️⃣ Consume a refresh token (inside the /api/auth endpoint)

# Client sends: grant_type=refresh_token, refresh_token=<token>

user, claims = await oauth_handler.consume_refresh_token(refresh_token)

# 3️⃣ Issue new token pair after successful consumption

access_token = oauth_handler.create_access_token(
    data={"sub": user.username, "iss": "romm:oauth"},
    expires_delta=timedelta(minutes=5),
)
new_refresh_token = oauth_handler.create_refresh_token(
    data={"sub": user.username, "iss": "romm:oauth"},
    expires_delta=timedelta(minutes=30),
)

Summary

RomM’s approach to token refresh for API authentication combines cryptographic validation with database-backed single-use enforcement:

  • Unique token identification via the jti claim stored in Redis under refresh-jti:<jti>
  • Atomic consumption using redis_client.getdel() to prevent race conditions and replay attacks
  • Automatic rotation that invalidates the old token while generating a new one with each refresh cycle
  • Strict validation of issuer, type, expiration, and user status before issuing new credentials

Frequently Asked Questions

How does RomM prevent refresh token replay attacks?

RomM stores each refresh token’s jti (JWT ID) in Redis with the key pattern refresh-jti:<jti>. When consuming a token, the system uses redis_client.getdel() to atomically retrieve and delete this key. If the key is already gone, the token is rejected, ensuring that even if an attacker intercepts the token, they cannot use it after the legitimate client has refreshed.

What happens to the old refresh token after a successful refresh?

The old refresh token becomes permanently invalid immediately upon use. The consume_refresh_token() method deletes the corresponding Redis entry during validation, and the /api/auth endpoint issues a brand new refresh token with a fresh jti and expiration time. This rotation mechanism ensures no lingering valid tokens exist in the system.

Where does RomM store the refresh token metadata?

RomM stores refresh token metadata in Redis, not in the primary database. Specifically, it creates keys with the format refresh-jti:<jti> where <jti> is the unique JWT ID embedded in the token. These keys have a TTL matching the token’s expiration and hold the value b"valid" to indicate active status.

How long do refresh tokens remain valid in RomM?

The refresh token lifetime is configurable via the expires_delta parameter passed to create_refresh_token(). While the source code examples show 30-minute durations, administrators can adjust this value during token creation. Regardless of the duration, tokens automatically expire when their TTL elapses in Redis or when they are consumed, whichever comes first.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →