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