JWT Flow
Monozu Cloud uses a two-token JWT strategy: a short-lived access token held in browser memory and a long-lived refresh token stored in an HttpOnly cookie.
Token properties
Section titled “Token properties”| Token | Storage | Default TTL | Env var |
|---|---|---|---|
| Access token | JavaScript memory (Zustand store) | 60 minutes | ACCESS_TOKEN_EXPIRE_MINUTES |
| Refresh token | HttpOnly cookie (refresh_token) | 7 days | REFRESH_TOKEN_EXPIRE_DAYS |
The access token is never stored in localStorage or sessionStorage — it lives only in the Zustand authStore and is lost on hard page refresh. The refresh token cookie handles rehydration.
Login flow
Section titled “Login flow”sequenceDiagram
participant Browser
participant SPA as Cloud SPA
participant API as Cloud Backend API
participant DB as Azure SQL
Browser->>SPA: Enter email + password
SPA->>API: POST /api/v1/auth/login
Note right of SPA: body: { email, password }
API->>DB: SELECT user WHERE email = ? AND tenant active
DB-->>API: User row
API->>API: bcrypt.Compare(password, hash)
alt MFA enabled
API-->>SPA: 200 { mfa_required: true, session_token }
SPA->>API: POST /api/v1/auth/mfa/verify
Note right of SPA: body: { totp_code, session_token }
API->>API: Verify TOTP
end
API->>API: Sign access JWT (HS256, 60 min)
API->>API: Sign refresh JWT (HS256, 7 days)
API-->>SPA: 200 { access_token } + Set-Cookie: refresh_token (HttpOnly)
SPA->>SPA: Store access_token in authStore (memory)
SPA-->>Browser: Redirect to dashboard
Silent refresh (page reload)
Section titled “Silent refresh (page reload)”When the user reloads the page, the access token in memory is lost. The SessionRestore component runs before any protected route renders:
sequenceDiagram
participant Browser
participant SPA as Cloud SPA (SessionRestore)
participant API as Cloud Backend API
Browser->>SPA: Full page reload
SPA->>SPA: authStore hydrated — user exists but access_token missing
SPA->>API: POST /api/v1/auth/refresh (cookie sent automatically)
alt Cookie valid + not expired
API->>API: Validate refresh JWT
API->>API: Issue new access JWT
API-->>SPA: 200 { access_token }
SPA->>SPA: Update authStore with new access_token
SPA-->>Browser: Render protected page
else Cookie missing or expired
API-->>SPA: 401
SPA-->>Browser: Redirect to /login
end
Token renewal (proactive)
Section titled “Token renewal (proactive)”The SPA refreshes the access token before it expires. TanStack Query’s background refetch and Axios interceptors handle 401 responses by attempting a refresh and retrying the original request.
Logout
Section titled “Logout”sequenceDiagram
participant SPA as Cloud SPA
participant API as Cloud Backend API
SPA->>API: POST /api/v1/auth/logout
API->>API: Invalidate refresh token (DB blacklist or rotation)
API-->>SPA: 200 + Set-Cookie: refresh_token= (cleared)
SPA->>SPA: Clear authStore (access_token, user)
SPA-->>SPA: Redirect to /login
Cross-origin considerations
Section titled “Cross-origin considerations”When the SPA (cloud.monozu.io) and API (api.cloud.monozu.io) are on different origins, the refresh cookie must be sent cross-origin. Required backend configuration:
REFRESH_COOKIE_SAMESITE=none # Requires HTTPSALLOW_ORIGINS=https://cloud.monozu.ioThe API must respond with Access-Control-Allow-Credentials: true and a specific (not wildcard) Access-Control-Allow-Origin.
JWT payload structure
Section titled “JWT payload structure”The access token payload contains at minimum:
{ "sub": "<user_uuid>", "tenant_id": "<tenant_uuid>", "exp": 1234567890}The backend extracts tenant_id from the token to set SESSION_CONTEXT for RLS, and sub (user ID) for audit logging and RBAC resolution.