Files
mizan/docs/MWT_SPEC.md

71 lines
2.7 KiB
Markdown

# 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. Carried in the JOSE header (RFC 7515), not the payload |
| `aud` | Audience binding — prevents cross-tenant replay |
| `nbf` | Not-before — tolerates clock skew |
| `staff` / `super` | `is_staff` / `is_superuser`, used to build `MWTUser` without a DB query |
## 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.
- **`MWTUser`** is a minimal, DB-free request user built from the
token claims (`cores/mizan-python/src/mizan_core/mwt.py`).
> A separate JWT module (`mizan/jwt/`) still exists for standard
> user-auth access/refresh tokens; MWT is the cache-keying identity
> layer, not a replacement for that module.
- **App handles authentication** (session, social, etc.). Mizan
issues MWT *from* the authenticated identity
(`create_mwt(user, secret, ttl, audience, kid)`).
- **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:
```
ctx:{context}:HMAC(secret, JSON.stringify({"c": context, "p": sorted_params, "r": rev, "u": user_id}))
```
See [CACHE_KEYING.md](CACHE_KEYING.md) for the full derivation.
## What this solves
- DRF token collision
- `JWTUser`-too-thin problem
- Permission staleness race condition
- Single validation path across Python and TypeScript Edge
## Usage rule
MWT is the identity Edge/cache layers key on. The `@client(auth=...)`
parameter is enforced server-side in `mizan/client/executor.py`
(`_check_auth_requirement`), which checks `request.user` against the
auth requirement (`required` / `staff` / `superuser` / callable);
`request.user` may be an `MWTUser` (stateless) or a session user.