Auth Flow & Tokens¶
Viana uses stateless JWT authentication. No session is stored on the server — the token carries all identity information. A separate opaque refresh token enables silent re-authentication without re-entering credentials.
Token types¶
| Access Token | Refresh Token | |
|---|---|---|
| Format | JWT (signed HMAC-SHA256) | Opaque UUID |
| Lifetime | 1 hour (configurable) | 30 days (configurable) |
| Stored | Client-side only | token table + client |
| Used for | API authorization | Obtaining a new access token |
| Revokable | Yes (via token table flags) |
Yes (via token table flags) |
Full token flow¶
sequenceDiagram
participant App
participant API
App->>API: POST /api/login { email, password }
API-->>App: { access_token, refresh_token }
App->>API: GET /api/... (Authorization: Bearer <access_token>)
API-->>App: 200 OK
Note over App,API: access_token expires after 1h
App->>API: POST /api/auth/refresh { refreshToken }
API-->>App: { access_token, refresh_token } (new pair — rotation)
App->>API: POST /api/logout
API-->>App: 200 OK (all tokens revoked)
JWT payload¶
json
{
"sub": "42",
"role": "DRIVER",
"iat": 1711900000,
"exp": 1711903600
}
sub— the user's numeric ID (User.id)role— one ofCUSTOMER,DRIVER,ADMIN
The token is signed with HMAC-SHA256 using the secret from JWT_SECRET. The secret must be a 256-bit (64 hex character) value.
Token rotation¶
On every call to POST /api/auth/refresh:
- The submitted refresh token is looked up in the database.
- If
expired = trueorrevoked = true→409 Conflict. - If
refreshTokenExpiresAtis in the past →409 Conflict. - The existing token row is marked
expired = true, revoked = true. - A new access token + new refresh token pair is issued and stored.
This means a refresh token can only be used once. Clients must always store and use the latest refresh token from the most recent response.
Login revokes existing tokens¶
When a user logs in via POST /api/login, all previously valid tokens for that user are revoked before the new pair is issued. This ensures a user can only have one active session at a time.
Token database schema¶
The token table stores both the JWT string and the refresh token UUID on the same row:
| Column | Type | Description |
|---|---|---|
id |
bigint | Primary key |
token |
varchar | JWT access token string |
refresh_token |
varchar (unique) | Opaque UUID refresh token |
refresh_token_expires_at |
timestamp | Expiry of the refresh token |
expired |
boolean | True if the access token has expired |
revoked |
boolean | True if explicitly revoked |
user_id |
bigint (FK) | Owner of the token |
Using the token¶
Include the access token on every protected request:
http
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
For WebSocket connections, include the same header during the STOMP handshake (not as a query parameter):
javascript
client.connect({ Authorization: `Bearer ${token}` }, onConnected);
Error responses¶
| Scenario | Status | Message |
|---|---|---|
| Missing token | 403 Forbidden |
— |
| Invalid / malformed token | 401 Unauthorized |
— |
| Expired access token | 401 Unauthorized |
— |
| Revoked refresh token | 409 Conflict |
Refresh token has been revoked |
| Expired refresh token | 409 Conflict |
Refresh token has expired |
| Wrong role | 403 Forbidden |
— |