Skip to content

JWT Authentication

CMS API uses JSON Web Tokens (JWT) for stateless authentication with a dual-token strategy.

Token Types

Access Token

Short-lived token sent with every API request.

Property Value
Lifetime 15 minutes (configurable)
Sent in Authorization: Bearer header
Stored in Memory (client-side)
Contains user_id, permissions, token_version

Refresh Token

Long-lived token used only to obtain a new access token.

Property Value
Lifetime 14 days (configurable)
Sent in Request body
Stored in refresh_tokens table (DB)
Contains user_id only

Token Structure

Access Token Payload

{
  "sub": "user-uuid",
  "type": "access",
  "exp": 1234567890,
  "permissions": [
    "users:read",
    "users:update"
  ],
  "token_version": 1
}

Refresh Token Payload

{
  "sub": "user-uuid",
  "type": "refresh",
  "exp": 1234567890
}

Authentication Flow

1. Client sends credentials
2. Server validates email + password
3. Server issues access token (15 min) + refresh token (14 days)
4. Client stores tokens
   - access token  → memory
   - refresh token → httpOnly cookie or secure storage
5. Client sends access token with every request
   Authorization: Bearer <access_token>
6. Access token expires after 15 minutes
7. Client sends refresh token to POST /auth/refresh
8. Server validates refresh token → issues new token pair
9. Old refresh token is revoked (token rotation)

Token Validation

Every protected request goes through the following checks in dependencies.py:

1. Extract token from Authorization header
2. Verify JWT signature
3. Check token type == "access"
4. Check token not expired
5. Load user from database
6. Check token_version matches user.token_version
7. Check user is active

If any check fails, 401 Unauthorized is returned.

Token Rotation

Every call to POST /auth/refresh invalidates the old refresh token and issues a new one. This means:

  • A stolen refresh token can only be used once
  • If an attacker uses it first, the legitimate user's next refresh will fail
  • Short access token lifetime (15 min) limits the damage window

Security Considerations

Never store access tokens in localStorage

localStorage is accessible via JavaScript and vulnerable to XSS attacks. Store access tokens in memory and refresh tokens in httpOnly cookies.

Keep access token lifetime short

15 minutes is a good balance between security and user experience. The client should silently refresh before expiry.

Rotate JWT_SECRET_KEY carefully

Changing JWT_SECRET_KEY immediately invalidates all active tokens. All users will be logged out. Plan accordingly.