Authentication Middleware and Token Normalization
Authentication Middleware and Token Normalization
Relevant source files
The following files were used as context for generating this wiki page:
The authentication system in Primebrick v3 is designed to be identity-provider (IDP) agnostic, supporting both direct JWT validation and upstream API gateway trust modes. It ensures that regardless of the authentication source, downstream business logic receives a consistent AuthUser object mapped to an internal system UUID.
Authentication Modes
The middleware supports two distinct execution paths configured via the environment, converging into a single internal identity model src/modules/auth/auth.middleware.ts:20-21.
1. STANDALONE Mode
In this mode, the backend is responsible for verifying the authenticity of the credentials. It attempts to extract a JWT from two locations:
- Authorization Header:
Bearer <jwt>src/modules/auth/auth.middleware.ts:99-102. - HttpOnly Cookie:
access_token(used primarily by the Svelte frontend) src/modules/auth/auth.middleware.ts:105-107.
The token is verified using openid-client and JWKS (JSON Web Key Sets) fetched from the IDP's OIDC discovery endpoint src/modules/auth/auth.middleware.ts:8-9, src/modules/auth/auth.middleware.ts:118-119.
2. GATEWAY Mode
In this mode, the backend trusts an upstream gateway (e.g., Kong, Tyk, APISIX) that has already performed authentication.
- Anti-Spoofing: To prevent direct access to the API bypassing the gateway, a shared secret (
X-Gateway-Secret) must be provided in the headers src/modules/auth/auth.middleware.ts:147-154. - Header Mapping: User identity, roles, and organization info are read directly from headers injected by the gateway src/modules/auth/auth.middleware.ts:156-176.
Auth Flow and Entity Mapping
The following diagram illustrates how external credentials (JWT or Headers) are transformed into the internal AuthUser type and propagated through the system.
Auth Data Flow: From Request to Session
Code
Token Normalization
Different IDPs (Casdoor, Keycloak, Entra ID) structure JWT claims differently, especially roles. The token-normalizer.ts provides a robust mechanism to flatten these differences.
AUTH_ROLES_PATH
The system uses a configurable dotted path (e.g., realm_access.roles for Keycloak or roles for Casdoor) to locate role data within the JWT src/modules/auth/token-normalizer.ts:10-12. The readPath function traverses the JSON payload to extract this data src/modules/auth/token-normalizer.ts:24-31.
Role Coercion
The coerceRoles function handles various role formats:
- Flat string arrays:
["admin", "user"] - Object arrays:
[{ "name": "admin" }] - Mixed or null values are safely stringified or filtered src/modules/auth/token-normalizer.ts:40-51.
Internal UUID Resolution
A critical security boundary is the separation of the IDP's sub (subject) claim from the internal system ID.
idp_code: The original JWTsubis stored for traceability src/modules/auth/types.ts:19-20.id: TheresolveInternalUuidfunction maps theidp_codeto a localuser_profiles.uuidsrc/modules/auth/auth.middleware.ts:130-136. If the user does not exist, they are Just-In-Time (JIT) provisioned.
Sources: src/modules/auth/token-normalizer.ts:1-134, src/modules/auth/auth.middleware.ts:129-142.
Session Context Propagation
Primebrick uses AsyncLocalStorage (ALS) to provide the authenticated context to downstream services without polluting every function signature with a user parameter src/modules/auth/session-context.ts:4-9.
AuthUser vs Session
AuthUser: Attached toreq.userfor Express-level access src/modules/auth/types.ts:16-32.Session: A subset ofAuthUsermirrored into ALS src/modules/auth/session-context.ts:42-76.
Usage in Code
- Middleware Setup: The
authMiddlewarecallsrunWithSession(session, () => next())to wrap the entire request lifecycle src/modules/auth/auth.middleware.ts:81-88. - DAL/Repository Access: Data access layers call
requireActor()to retrieve the current user's UUID for audit columns likecreated_byorupdated_bysrc/modules/auth/session-context.ts:104-114. - System Operations: For background jobs or migrations where no HTTP request exists,
runAsSystem()provides a synthetic session with the actor set to"system"src/modules/auth/session-context.ts:130-135.
Entity Mapping: Code Structures
| Code Entity | Purpose | File |
|---|---|---|
AuthUser | Interface for the user attached to req.user | src/modules/auth/types.ts:16 |
Session | Interface for data stored in AsyncLocalStorage | src/modules/auth/session-context.ts:42 |
normalizeIdpToken | Logic for extracting user data from IDP-specific JWTs | src/modules/auth/token-normalizer.ts:69 |
loadRoleMappings | Loads role_mappings table into a memory cache | src/modules/auth/auth.middleware.ts:51 |
requireActor | Helper to get the current actor UUID from the session | src/modules/auth/session-context.ts:104 |
Sources: src/modules/auth/session-context.ts:1-136, src/modules/auth/auth.middleware.ts:1-93