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

> Discover how RomM secures API authentication with a robust token refresh flow. Learn about their Redis-backed JWT storage and single-use token strategy to prevent replay attacks.

- Repository: [The RomM Project/romm](https://github.com/rommapp/romm)
- Tags: how-to-guide
- Published: 2026-07-05

---

**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`](https://github.com/rommapp/romm/blob/main/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`](https://github.com/rommapp/romm/blob/main/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`](https://github.com/rommapp/romm/blob/main/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:

```python

# 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:

```python

# 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.