Skip to content

Token Management

DaraMex uses a secure JWT-based authentication system with automatic refresh token rotation, strict session validation, and real-time reuse detection.

Token Pair Properties

Each session is represented by a pair of JSON Web Tokens (JWTs):

TokenAccess TokenRefresh Token
DurationShort-lived (e.g., 1h).Long-lived (e.g., 7d).
PurposeAuthenticates individual requests via Authorization: Bearer <token>.Used once to request a new token pair.
StorageClient memory (recommended) or short-lived cookie.Persistent client storage (e.g., localStorage) or long-lived cookie.
Payloadsub, type (user|client), agencyId, sessionId (required).sub, type, agencyId, sessionId (required), jti (UUIDv7 unique token ID).

Session Validation (Real-Time Revocation)

Access tokens contain a sessionId. On every protected request, the API verifies that the associated session is still active (session.isActive === true). This ensures that if a session is revoked (e.g., due to a password change, manual revocation, or suspected token theft), the access token is immediately invalidated, even if its JWT expiration time has not yet passed.


Refresh Token Rotation & Reuse Detection

To minimize the risk of token theft, we implement Refresh Token Rotation. When a refresh token is used, it is invalidated and a brand-new pair is issued.

  1. Stored Token: Each issued refresh token's jti (generated via UUIDv7 for sequential indexing) is hashed and stored in the database.
  2. Rotation: Upon request to /auth/refresh, the system verifies the provided token's signature, expiration, and associated session validity.
  3. Atomic Invalidation: The system atomically attempts to mark the token as revokedAt.
  4. Reuse Detection: If the database reports the token was already revoked, a token theft event is assumed. The system immediately invalidates all active sessions for that subject, logs a warning, and returns a dedicated error code (IDENTITY.REFRESH_TOKEN_REUSE_DETECTED) with a neutral re-authentication message.
  5. Session-Preserving Rotation: Successful refresh rotation always issues the new token pair under the same session referenced by the incoming refresh token.

The token service (TokenPort / TokenAdapter) uses Result<T> across all operations (generateTokenPair, generateTokenPairForSession, refreshTokens, revokeSession, revokeAllForSubject). Refresh logic does not throw custom exceptions for reuse detection; instead it returns Result.fail(IdentityErrors.refreshTokenReuseDetected(...)), and the command handler forwards that structured app error.

sequenceDiagram
    participant Client as Frontend
    participant API as AuthController
    participant DB as Identity Database

    Client->>API: POST /auth/refresh {refreshToken}
    API->>API: Verify signature & Session Active status
    API->>DB: Atomic Update: set revokedAt where tokenHash = ...
    alt Token already revoked (Reuse Detected)
        API->>DB: REVOKE ALL sessions for subject
        API-->>Client: 401 Unauthorized (Reuse Detected)
    else Token validly rotated
        API->>API: Generate NEW Token Pair (UUIDv7 jti)
        API->>DB: Save NEW RefreshToken hash
        API-->>Client: 200 OK (New Tokens)
    end

Logout Logic

Logging out is session-centered and authenticated.

  1. The client calls POST /auth/logout using a valid access token.
  2. The API reads sessionId from authenticated context.
  3. The API revokes the current session.
  4. The API revokes all active refresh tokens linked to that session.

Password Change Token Rotation

Password changes preserve the current session but rotate its refresh credentials.

  1. The API updates the password hash.
  2. The API revokes all other active sessions and their refresh tokens.
  3. The API revokes refresh tokens for the current session.
  4. The API issues a new token pair for the same current session ID.
  5. The endpoint returns the fresh accessToken and refreshToken to the caller.

Data Structure: Refresh Token Entity

typescript
type RefreshTokenProps = {
  tokenHash: string; // Hash of the jti (UUIDv7)
  subjectId: string; // User ID or Client ID
  subjectType: SubjectType; // 'user' | 'client'
  sessionId: string; // Required link to the active session
  agencyId: string | null;
  expiresAt: Date;
  revokedAt: Date | null;
};