Auth & Brand -- Spec

Complete validation specification for the API Gateway auth and brand-context layer

Phase 1
Spec
API Gateway

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

1.

Validation Checklist

JWT -- Structure & Signature

Validate the token before touching any payload data.

4 required · 4 total checks

Authorization header is present

Required

Request must include Authorization: Bearer <token>. Missing header -- reject immediately.

401MISSING_TOKEN

Token has valid JWT format

Required

Must consist of exactly three Base64URL segments separated by dots: header.payload.signature. Malformed string -- reject.

401MALFORMED_TOKEN

Algorithm matches expected (alg claim)

Required

Accept only configured algorithm (e.g. HS256 or RS256). Reject tokens with alg: none or unexpected algorithm -- this prevents algorithm-confusion attacks.

401INVALID_TOKEN_ALG

Signature is cryptographically valid

Required

Verify HMAC or RSA signature against the secret/public key. A single bit difference -- reject.

401INVALID_TOKEN_SIGNATURE

JWT -- Claims Validation

Standard claims must be present and within valid ranges.

4 required · 6 total checks

Token is not expired (exp claim)

Required

exp must be in the future. Allow a small clock-skew tolerance (up to 60 seconds) for distributed systems.

401TOKEN_EXPIRED

Token is active (nbf claim)

Required

If nbf (not before) is present, current time must be >= nbf. Reject tokens used before their activation time.

401TOKEN_NOT_YET_VALID

Issuer matches expected (iss claim)

Required

iss must match the configured issuer identifier. Prevents accepting tokens from foreign auth services.

401INVALID_TOKEN_ISSUER

Audience is correct (aud claim)

Recommended

If aud is present, it must include the payment service identifier (e.g. "payment-api"). Prevents using tokens issued for other services.

401INVALID_TOKEN_AUDIENCE

Subject (user ID) is present (sub claim)

Required

sub must be a non-empty string. This is the player's identifier -- required to attach the transaction to a user.

401MISSING_SUBJECT

Token ID is unique (jti claim) -- revocation

Recommended

If jti is present, check it against a revocation list (Redis). Allows immediate token invalidation on logout or security event.

401TOKEN_REVOKED

JWT -- Payload Contents

Business-level fields required for downstream processing.

1 required · 3 total checks

user_id is a valid, non-empty identifier

Required

Must be a non-empty string or positive integer. Reject obviously invalid values (empty string, "null", 0).

401INVALID_USER_ID

session_id is present

Recommended

Required for idempotency and fraud correlation. Pass downstream to orchestrator.

401MISSING_SESSION_ID

User has payment permission

Recommended

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).

403INSUFFICIENT_PERMISSIONS

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

Required

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.

400UNRESOLVABLE_BRAND

brand_id exists in the database

Required

Load brand config from DB (or cache). If brand_id is not found -- reject. Do not silently fall back to a default brand.

400UNKNOWN_BRAND

Brand is active (not suspended)

Required

Brand may be temporarily disabled (maintenance, legal hold). If brand.status != active -- reject with clear error.

403BRAND_SUSPENDED

JWT sub (user) belongs to the resolved brand

Required

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.

403USER_BRAND_MISMATCH

Brand has at least one active PSP configured

Recommended

If the brand has no active PSP config, payment cannot proceed. Fail fast here rather than inside the orchestrator.

503NO_PSP_CONFIGURED

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

Required

Set on internal request object / middleware context. Never re-read from JWT downstream -- use the validated value.

brand_id is attached to request context

Required

All downstream layers (orchestrator, adapters, wallet) receive brand_id from context, not from raw request.

Loaded brand config is attached to request context

Recommended

Include PSP configs, limits, allowed currencies. Avoids redundant DB lookups in orchestrator.

session_id is attached to request context

Recommended

Used by downstream layers for idempotency key construction and fraud correlation.

request_id is generated and attached

Required

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

Required

Reject plain HTTP. If behind a load balancer, verify X-Forwarded-Proto: https.

403HTTPS_REQUIRED

Content-Type is application/json for POST requests

Required

Reject requests with incorrect Content-Type to prevent parser confusion attacks.

415UNSUPPORTED_MEDIA_TYPE

Request body does not exceed size limit

Required

Enforce a body size limit (e.g. 64 KB for payment requests). Prevents memory exhaustion.

413PAYLOAD_TOO_LARGE

CORS: Origin is in the allowed list

Required

Only origins matching a configured whitelist (brand domains) receive CORS headers. Do not use wildcard (*) for authenticated endpoints.

403ORIGIN_NOT_ALLOWED

All auth failures are logged with context

Required

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

Required

Return only the error code and a human-readable message. Stack traces, DB errors, and internal field names must never appear in responses.

2.

Edge Cases

Token issued for a different brand

High

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

High

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

High

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

High

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

High

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

High

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

Medium

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

Medium

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.

3.

Error Code Reference

HTTPError KeyWhen
401MISSING_TOKENNo Authorization header
401MALFORMED_TOKENToken is not a valid JWT string
401INVALID_TOKEN_ALGAlgorithm is none or unexpected
401INVALID_TOKEN_SIGNATURESignature does not match
401TOKEN_EXPIREDexp claim is in the past
401TOKEN_NOT_YET_VALIDnbf claim is in the future
401INVALID_TOKEN_ISSUERiss does not match expected
401INVALID_TOKEN_AUDIENCEaud does not include payment service
401MISSING_SUBJECTsub claim is absent or empty
401TOKEN_REVOKEDjti found in revocation list
401INVALID_USER_IDuser_id is empty or invalid
400UNRESOLVABLE_BRANDbrand_id cannot be determined
400UNKNOWN_BRANDbrand_id not found in DB
403BRAND_SUSPENDEDBrand status is not active
403USER_BRAND_MISMATCHUser does not belong to resolved brand
403INSUFFICIENT_PERMISSIONSUser role does not allow payments
403ORIGIN_NOT_ALLOWEDCORS origin not in whitelist
403HTTPS_REQUIREDRequest arrived over HTTP
413PAYLOAD_TOO_LARGERequest body exceeds size limit
415UNSUPPORTED_MEDIA_TYPEContent-Type is not application/json
503NO_PSP_CONFIGUREDBrand has no active PSP
DEPO44 | AUTH & BRAND GATEWAY | SPEC v1 | ALL REQUIRED CHECKS MUST PASS BEFORE PHASE 1 SIGN-OFF