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