# MWT — Mizan Web Token *Decided 2026-04-06.* MWT is a standard JWT (RFC 7519, HMAC-SHA256) with Mizan-specific claims, traveling on the `X-Mizan-Token` header. It is the protocol's identity layer for cache keying and permission staleness detection. ## Claims | Claim | Purpose | |---|---| | `sub` | User ID — goes into HMAC cache key derivation | | `pkey` | Deterministic hash of user's permission state at issuance | | `exp` | Configurable short TTL — controls permission staleness window (Django setting) | | `iat` | Issued at | | `kid` | Key ID — for secret rotation | | `aud` | Audience binding — prevents cross-tenant replay | ## Key decisions - **Standard JWT envelope, not proprietary.** Uses standard libraries for signing and validation. - **`X-Mizan-Token` header, not `Authorization: Bearer`.** Avoids collision with DRF, allauth, and existing JWT systems. Cloudflare WAF/Access do not inspect custom headers. - **Replaces `JWTUser` + `_try_jwt_auth` entirely.** Old approach is deleted. - **App handles authentication** (session, social, etc.). Mizan issues MWT *from* the authenticated identity. - **Edge Worker** validates MWT, extracts `sub` for HMAC cache key, checks `exp`. - **`pkey` computation must be deterministic:** `sorted(user.get_all_permissions())` then hash. - **Client-side: proactive refresh before expiry.** Check TTL before dispatch, not reactively after a 401. - **Header-based, not cookie-based.** A cookie would force `Vary: Cookie`, destroying PSR cache. ## HMAC canonical form JSON with sorted keys: ``` HMAC(secret, JSON.stringify({"c": context, "p": sorted_params, "u": user_id})) ``` ## What this solves - DRF token collision - `JWTUser`-too-thin problem - Permission staleness race condition - Single validation path across Python and TypeScript Edge ## Usage rule All cache-layer auth code uses MWT, not Django session or raw JWT. The `@client(auth=...)` parameter gates on MWT validity.