Auth & Brand -- Spec
Complete validation specification for the API Gateway auth and brand-context layer
This spec covers the API Gateway auth middleware only -- the layer between the frontend and the internal orchestrator. It does not cover PassimPay authentication (that belongs to the Adapter Layer spec) or player registration/login flows.
22 required checks · 7 recommended/optional · 29 total
Validation Checklist
JWT -- Structure & Signature
Validate the token before touching any payload data.
4 required · 4 total checks
Authorization header is present
Request must include Authorization: Bearer <token>. Missing header -- reject immediately.
Token has valid JWT format
Must consist of exactly three Base64URL segments separated by dots: header.payload.signature. Malformed string -- reject.
Algorithm matches expected (alg claim)
Accept only configured algorithm (e.g. HS256 or RS256). Reject tokens with alg: none or unexpected algorithm -- this prevents algorithm-confusion attacks.
Signature is cryptographically valid
Verify HMAC or RSA signature against the secret/public key. A single bit difference -- reject.
JWT -- Claims Validation
Standard claims must be present and within valid ranges.
4 required · 6 total checks
Token is not expired (exp claim)
exp must be in the future. Allow a small clock-skew tolerance (up to 60 seconds) for distributed systems.
Token is active (nbf claim)
If nbf (not before) is present, current time must be >= nbf. Reject tokens used before their activation time.
Issuer matches expected (iss claim)
iss must match the configured issuer identifier. Prevents accepting tokens from foreign auth services.
Audience is correct (aud claim)
If aud is present, it must include the payment service identifier (e.g. "payment-api"). Prevents using tokens issued for other services.
Subject (user ID) is present (sub claim)
sub must be a non-empty string. This is the player's identifier -- required to attach the transaction to a user.
Token ID is unique (jti claim) -- revocation
If jti is present, check it against a revocation list (Redis). Allows immediate token invalidation on logout or security event.
JWT -- Payload Contents
Business-level fields required for downstream processing.
1 required · 3 total checks
user_id is a valid, non-empty identifier
Must be a non-empty string or positive integer. Reject obviously invalid values (empty string, "null", 0).
session_id is present
Required for idempotency and fraud correlation. Pass downstream to orchestrator.
User has payment permission
If roles/permissions are encoded in JWT, verify the user is allowed to initiate payments (e.g. not a banned or restricted account at token-issuance time).
Brand Resolution
Determine which brand (tenant) is making the request.
4 required · 5 total checks
brand_id is resolved from one of the accepted sources
Resolution order (first wins): 1) JWT payload claim brand_id -- 2) Request header X-Brand-ID -- 3) Origin/Host domain mapped to brand. If none resolve -- reject.
brand_id exists in the database
Load brand config from DB (or cache). If brand_id is not found -- reject. Do not silently fall back to a default brand.
Brand is active (not suspended)
Brand may be temporarily disabled (maintenance, legal hold). If brand.status != active -- reject with clear error.
JWT sub (user) belongs to the resolved brand
Verify the user_id in the JWT was issued by the same brand. Prevents cross-brand token reuse -- a user from brand A cannot act under brand B.
Brand has at least one active PSP configured
If the brand has no active PSP config, payment cannot proceed. Fail fast here rather than inside the orchestrator.
Context Propagation
Auth & Brand layer must enrich the request before passing it downstream.
3 required · 5 total checks
user_id is attached to request context
Set on internal request object / middleware context. Never re-read from JWT downstream -- use the validated value.
brand_id is attached to request context
All downstream layers (orchestrator, adapters, wallet) receive brand_id from context, not from raw request.
Loaded brand config is attached to request context
Include PSP configs, limits, allowed currencies. Avoids redundant DB lookups in orchestrator.
session_id is attached to request context
Used by downstream layers for idempotency key construction and fraud correlation.
request_id is generated and attached
Generate a unique request_id (UUID) at the gateway. Pass as X-Request-ID in all outbound calls and include in all error responses. Essential for log tracing.
Security Hardening
Gateway-level protections that do not belong in business logic.
6 required · 6 total checks
All requests arrive over HTTPS only
Reject plain HTTP. If behind a load balancer, verify X-Forwarded-Proto: https.
Content-Type is application/json for POST requests
Reject requests with incorrect Content-Type to prevent parser confusion attacks.
Request body does not exceed size limit
Enforce a body size limit (e.g. 64 KB for payment requests). Prevents memory exhaustion.
CORS: Origin is in the allowed list
Only origins matching a configured whitelist (brand domains) receive CORS headers. Do not use wildcard (*) for authenticated endpoints.
All auth failures are logged with context
Log: timestamp, IP, user_id (if extractable), brand_id (if resolved), error code, request_id. Never log full JWT or secret.
Error responses do not leak internal details
Return only the error code and a human-readable message. Stack traces, DB errors, and internal field names must never appear in responses.
Edge Cases
Token issued for a different brand
Scenario
User authenticates on brand-A, then sends the same JWT to the brand-B payment endpoint.
Expected behavior
Reject 403 USER_BRAND_MISMATCH. Brand resolution detects the mismatch between JWT brand_id and the resolved request brand.
Token expires mid-session
Scenario
User opens checkout, JWT expires while they are filling in the form. Payment request arrives with expired token.
Expected behavior
Reject 401 TOKEN_EXPIRED. Frontend must handle this gracefully -- redirect to re-auth or silently refresh if refresh token is available.
Brand exists but has no active PSP
Scenario
Brand config is present in DB but all PSP configs are disabled (e.g. pending onboarding).
Expected behavior
Reject 503 NO_PSP_CONFIGURED at gateway. Do not let the request reach the orchestrator.
Missing X-Brand-ID with no domain mapping
Scenario
Request comes from an API client (not a browser), no brand_id in JWT, no domain-to-brand mapping exists.
Expected behavior
Reject 400 UNRESOLVABLE_BRAND. Resolution chain exhausted with no result.
JWT replayed after logout
Scenario
User logs out (jti added to revocation list), but attacker replays the old token.
Expected behavior
Reject 401 TOKEN_REVOKED. Requires jti revocation check against Redis or equivalent fast store.
alg: none attack
Scenario
Attacker crafts a JWT with alg: none and no signature, hoping the server skips verification.
Expected behavior
Reject 401 INVALID_TOKEN_ALG. Server only accepts the pre-configured algorithm.
Brand suspended mid-session
Scenario
Brand is suspended by ops while player has an active session. Player tries to deposit.
Expected behavior
Reject 403 BRAND_SUSPENDED. Brand config is re-loaded per request (or cache TTL is short enough to catch this within seconds).
Request arrives over HTTP
Scenario
Client (or misconfigured proxy) sends request over plain HTTP.
Expected behavior
Reject 403 HTTPS_REQUIRED or redirect 301. Never process payment data over HTTP.
Error Code Reference
| HTTP | Error Key | When |
|---|---|---|
| 401 | MISSING_TOKEN | No Authorization header |
| 401 | MALFORMED_TOKEN | Token is not a valid JWT string |
| 401 | INVALID_TOKEN_ALG | Algorithm is none or unexpected |
| 401 | INVALID_TOKEN_SIGNATURE | Signature does not match |
| 401 | TOKEN_EXPIRED | exp claim is in the past |
| 401 | TOKEN_NOT_YET_VALID | nbf claim is in the future |
| 401 | INVALID_TOKEN_ISSUER | iss does not match expected |
| 401 | INVALID_TOKEN_AUDIENCE | aud does not include payment service |
| 401 | MISSING_SUBJECT | sub claim is absent or empty |
| 401 | TOKEN_REVOKED | jti found in revocation list |
| 401 | INVALID_USER_ID | user_id is empty or invalid |
| 400 | UNRESOLVABLE_BRAND | brand_id cannot be determined |
| 400 | UNKNOWN_BRAND | brand_id not found in DB |
| 403 | BRAND_SUSPENDED | Brand status is not active |
| 403 | USER_BRAND_MISMATCH | User does not belong to resolved brand |
| 403 | INSUFFICIENT_PERMISSIONS | User role does not allow payments |
| 403 | ORIGIN_NOT_ALLOWED | CORS origin not in whitelist |
| 403 | HTTPS_REQUIRED | Request arrived over HTTP |
| 413 | PAYLOAD_TOO_LARGE | Request body exceeds size limit |
| 415 | UNSUPPORTED_MEDIA_TYPE | Content-Type is not application/json |
| 503 | NO_PSP_CONFIGURED | Brand has no active PSP |