Skip to content

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 of CUSTOMER, 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:

  1. The submitted refresh token is looked up in the database.
  2. If expired = true or revoked = true409 Conflict.
  3. If refreshTokenExpiresAt is in the past → 409 Conflict.
  4. The existing token row is marked expired = true, revoked = true.
  5. 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