Appearance
Authorization And Token Refresh Flow
This document captures the current authorization and token refresh flow in apps/panel and the API behavior verified in runtime.
Source files for the current implementation:
src/features/auth/stores/index.tssrc/features/auth/utils/index.ts
Frontend Flow (apps/panel)
1. Login
- User submits login form.
- Frontend calls
POST /users/auth. - On success it stores:
access_tokenrefresh_token- the current public API prefix associated with that token pair
- Tokens are persisted in the auth feature store using cookie storage (
@pinia-plugin-persistedstate/nuxtdefault for Nuxt integration).
2. Request Interceptor
All HTTP requests use a shared $fetchApi interceptor:
- For
POST /users/auth:- token checks are skipped.
- For
GET /users/refresh/{token}:check()is skipped to avoid refresh recursion,Authorization: Bearer <current access token>is attached only when the current access token is still locally valid,- an already expired access token is not attached deliberately.
- For all other requests:
auth.check()runs,- then
Authorization: Bearer <access token>is attached.
3. auth.check() (proactive refresh)
check() performs:
- Hydration of persisted state.
- Validation that the persisted token pair belongs to the current public API prefix.
- If the stored prefix differs from the current prefix, auth state is reset.
- Legacy local fixture tokens without a stored prefix are also reset outside
/local-apimode.
- Decoding
expfor access and refresh tokens. - Refresh is triggered if any condition is true:
accessTokenis missing,accessTokenis already expired,accessTokenexpires in <= 5 minutes.
- If runtime JWT metadata shows
refreshExp <= accessExp, frontend emits a contract-warning debug event but still assumes the backend contract should be fixed server-side.
4. refreshAccessToken()
- Uses a single-flight promise so concurrent callers await the same in-flight refresh.
- Keeps
processingRefreshonly as observable state for UI / diagnostics. - Validates
refreshTokenpresence and expiration. - Calls
GET /users/refresh/{refreshToken}. - On success replaces both tokens (
accessandrefresh). - On failure logs user out (
$reset()store).
5. Auth Debugging
Auth refresh diagnostics are opt-in and disabled by default.
Enable them in DevTools:
js
localStorage.setItem('authRefreshDebug', '1');Disable them:
js
localStorage.removeItem('authRefreshDebug');When enabled, frontend writes structured auth events to:
- console: prefixed with
[auth debug] - browser buffer:
window.__MES_AUTH_LOGS__
The in-memory buffer is capped to the latest 200 entries.
Tracked event groups include:
- login submit / success / failure,
- hydration and
check()decisions, - refresh start / wait-existing / success / failure,
- request classification and bearer attachment decisions,
401and other response errors,- logout reasons,
- TTL contract warnings.
Useful fields in each event:
sessionId,requestId,refreshIdpath,method,reasonprocessingRefresh,awaitedExistingRefreshaccessExpMs,refreshExpMs,accessTtlMs,refreshTtlMs,ttlDeltaMshttpStatus,fetchErrorName,fetchErrorMessageresponseDataPreview(sanitized, token fields redacted)
API Behavior (validated in runtime)
Verified manually in browser on 2026-02-26:
/users/refresh/{token}requires valid path token:- invalid token returns
400 Invalid refresh token.
- invalid token returns
/users/refresh/{token}requires Bearer:- without valid Bearer it may return
401.
- without valid Bearer it may return
- On successful refresh API returns a new token pair:
access_tokenchanges,refresh_tokenchanges.
- Previous refresh token remains valid until its own
exp:- refresh with old token may still return
200, - strict one-time invalidation is not enabled.
- refresh with old token may still return
- Observed TTL policy:
refreshExp - accessExp = -7200(refresh expires 2 hours earlier than access).
Contract Gaps In Swagger
In current Swagger, /users/refresh/{token} does not explicitly define:
- required
Authorizationheader, - success response schema (
access_token,refresh_token,expires_in,refresh_expires_in), - error models (
400,401) and return conditions.
Recommended API Changes
- Set
refreshTTL greater thanaccessTTL. - Move to strict one-time refresh token rotation:
- issue a new refresh token on refresh,
- invalidate the previous refresh token immediately.
- Explicitly document
/users/refreshcontract in Swagger.