LDS Stake Portal

Authentication

Two-token JWT strategy: how tokens are issued, stored, refreshed, and revoked.

Authentication is JWT-only and managed entirely by FastAPI. The frontend stores no credentials in localStorage or sessionStorage.


Two-token strategy

TokenLifetimeStoragePurpose
Access token15 minutesIn memory only (setAccessToken())Sent as Authorization: Bearer on every API request
Refresh token7 daysHttpOnly cookieUsed to obtain a new access token without re-logging in

Why in-memory for the access token? Storing the access token in memory (not localStorage) means JavaScript running in a third-party script cannot read it, which limits XSS exposure. The trade-off is that the token is lost on page refresh — this is intentional. AuthSync (see below) transparently restores it from the refresh cookie on every page load.

Why HttpOnly for the refresh token? An HttpOnly cookie cannot be read by JavaScript at all, making it invisible to XSS attacks. The backend rotates this cookie on every refresh call.


Login flow

Client                          FastAPI
  │── POST /api/auth/login ────▶ │  (form-encoded: email + password)
  │                              │  1. Verify credentials
  │                              │  2. Create usersession row
  │                              │  3. Issue short-lived JWT (access token)
  │                              │  4. Set HttpOnly refresh_token cookie (7 days)
  │◀── 200 { access_token } ───  │
  │                              │
  │  setAccessToken(access_token)  (stored in module-level variable, never DOM)

The login endpoint uses application/x-www-form-urlencoded encoding, not JSON.


Session restore on page load (AuthSync)

When the React app mounts, the AuthSync component (in App.tsx) fires immediately:

  1. Calls GET /api/auth/refresh.
  2. FastAPI reads the refresh_token cookie, validates the session, and returns a new access token (rotating the cookie).
  3. AuthSync calls setAccessToken() with the new token.
  4. Updates the Zustand auth store with the current user's profile.

This means a user who closed and reopened the browser is silently re-authenticated without a visible login screen, as long as the 7-day refresh cookie has not expired.


Automatic token refresh on 401

apiRequest() (in lib/queryClient.ts) handles token refresh transparently:

apiRequest() attaches the current in-memory access token as Authorization: Bearer <token>.

The access token has expired (15-minute lifetime).

Sends GET /api/auth/refresh. The HttpOnly cookie is sent automatically by the browser. FastAPI validates the session and returns a new access token.

setAccessToken() is called with the new token. The original request is retried with the fresh token. The caller never sees the 401.

If the refresh call itself returns 401 (expired or revoked session), the user is redirected to /login. This is the only case where the 401 surfaces to the UI.


force_password_reset guard

When an admin creates a user, the force_password_reset flag on the user row is set to true. Until the user changes their password, the backend returns 403 on all endpoints except:

  • GET /auth/me — so the frontend knows who is logged in and can show the change-password prompt.
  • POST /auth/logout — so the user can log out even before changing their password.
  • PATCH /users/{id}/password — the endpoint used to perform the password change.

The CallingUser dependency enforces this guard. To bypass it on a specific endpoint (e.g., /auth/me itself), pass allow_unchanged_password=True to the CallingUser constructor.


Logout flow

Client                          FastAPI
  │── POST /api/auth/logout ───▶ │  1. Delete usersession row (server-side)
  │                              │  2. Clear the refresh_token cookie
  │◀── 204 No Content ──────────  │

  Client: setAccessToken(null)
          reset Zustand auth store
          redirect to /login

Logout is complete even if the in-memory access token has not expired — the backend session is deleted, so the refresh token can no longer be used to obtain new access tokens.


Endpoints reference

MethodPathAuth requiredDescription
POST/auth/loginNoIssue tokens, start session
GET/auth/refreshCookie onlyRotate refresh token, return new access token
GET/auth/meYesReturn current user profile
POST/auth/logoutYesDelete session, clear cookie

On this page