Skip to content

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.ts
  • src/features/auth/utils/index.ts

Frontend Flow (apps/panel)

1. Login

  1. User submits login form.
  2. Frontend calls POST /users/auth.
  3. On success it stores:
    • access_token
    • refresh_token
    • the current public API prefix associated with that token pair
  4. Tokens are persisted in the auth feature store using cookie storage (@pinia-plugin-persistedstate/nuxt default for Nuxt integration).

2. Request Interceptor

All HTTP requests use a shared $fetchApi interceptor:

  1. For POST /users/auth:
    • token checks are skipped.
  2. 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.
  3. For all other requests:
    • auth.check() runs,
    • then Authorization: Bearer <access token> is attached.

3. auth.check() (proactive refresh)

check() performs:

  1. Hydration of persisted state.
  2. 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-api mode.
  3. Decoding exp for access and refresh tokens.
  4. Refresh is triggered if any condition is true:
    • accessToken is missing,
    • accessToken is already expired,
    • accessToken expires in <= 5 minutes.
  5. 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()

  1. Uses a single-flight promise so concurrent callers await the same in-flight refresh.
  2. Keeps processingRefresh only as observable state for UI / diagnostics.
  3. Validates refreshToken presence and expiration.
  4. Calls GET /users/refresh/{refreshToken}.
  5. On success replaces both tokens (access and refresh).
  6. 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,
  • 401 and other response errors,
  • logout reasons,
  • TTL contract warnings.

Useful fields in each event:

  • sessionId, requestId, refreshId
  • path, method, reason
  • processingRefresh, awaitedExistingRefresh
  • accessExpMs, refreshExpMs, accessTtlMs, refreshTtlMs, ttlDeltaMs
  • httpStatus, fetchErrorName, fetchErrorMessage
  • responseDataPreview (sanitized, token fields redacted)

API Behavior (validated in runtime)

Verified manually in browser on 2026-02-26:

  1. /users/refresh/{token} requires valid path token:
    • invalid token returns 400 Invalid refresh token.
  2. /users/refresh/{token} requires Bearer:
    • without valid Bearer it may return 401.
  3. On successful refresh API returns a new token pair:
    • access_token changes,
    • refresh_token changes.
  4. Previous refresh token remains valid until its own exp:
    • refresh with old token may still return 200,
    • strict one-time invalidation is not enabled.
  5. 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:

  1. required Authorization header,
  2. success response schema (access_token, refresh_token, expires_in, refresh_expires_in),
  3. error models (400, 401) and return conditions.
  1. Set refresh TTL greater than access TTL.
  2. Move to strict one-time refresh token rotation:
    • issue a new refresh token on refresh,
    • invalidate the previous refresh token immediately.
  3. Explicitly document /users/refresh contract in Swagger.