Airlock Cross-Apex JWT Handoff
meridian intermediate 6 min read
ELI5
Airlock is the family’s front door, but stratt.dev is in a different building, so the front door’s keycard does not work there. Airlock instead writes a one-minute paper note that says “this person can enter Meridian”, and Meridian swaps that note for its own house key once it reads the signature.
Technical Deep Dive
Browsers refuse to share cookies across apex domains, so airlock (*.devarno.cloud) cannot give Meridian (stratt.dev) a usable session cookie directly. The handoff is a two-token bridge defined in src/lib/airlock-handoff.ts.
Execution Sequence Diagram
sequenceDiagram autonumber participant U as Browser participant M as Meridian middleware participant A as Airlock participant CB as /auth/callback U->>M: GET /units/dev/... M->>M: verifyMeridianSession(cookie) M-->>U: 302 to airlock /api/auth/handoff?return=callback U->>A: handoff request (already signed in) A-->>U: 302 to /auth/callback?token=<EdDSA JWT> U->>CB: GET /auth/callback?token=... CB->>A: fetch JWKS (cached 5 min) CB->>CB: jwtVerify alg=EdDSA, iss, aud, exp CB->>CB: signMeridianSession (HS256, 8h) CB-->>U: Set-Cookie + 302 to safeNextToken Roles
| Token | Algorithm | TTL | Verifier | Why |
|---|---|---|---|---|
| Airlock handoff | EdDSA (Ed25519) | 60s | JWKS at airlock | Asymmetric, no shared secret across apexes |
| Meridian session | HS256 | 8h (MERIDIAN_SESSION_TTL_SECONDS) | local secret | Fast, never leaves stratt.dev |
expectedAudience is the apex MERIDIAN_PUBLIC_ORIGIN (e.g. https://stratt.dev), NOT the request URL — under Vercel’s Node adapter url.host resolves to an internal proxy host and would mismatch the JWT aud.
Failure Modes
error=access_denied→ 403 page asking to add the Meridian app grant in Hatch.error=app_not_registered→ 503 (provisioning gap).- Token expired (60s budget burned by network) → 401 with a “try again” link;
cookies.deleteclears any stale local cookie. - Open-redirect guard:
nextmust start with/and not//.
Key Terms
- Apex domain → Registrable domain (
stratt.dev). Cookies cannot cross apex boundaries. - JWKS → JSON Web Key Set; airlock publishes its EdDSA public keys at
/api/auth/jwks. - F9 → The flow label airlock uses for cross-apex handoff in shared OTel dashboards.
aud→ JWT audience claim; airlock binds it to the requesting apex so a token cannot be replayed at another app.
Q&A
Q: Why HS256 locally instead of also using Ed25519? A: The local cookie never leaves stratt.dev, so symmetric HMAC is faster to verify on every request and the secret never has to be published. Asymmetric crypto only earns its cost across trust boundaries.
Q: What happens if MERIDIAN_PUBLIC_ORIGIN is unset?
A: buildHandoffRedirect falls back to ${currentUrl.protocol}//${currentUrl.host}, which under Vercel can be an internal host — the callback URL then fails airlock’s whitelist and the user gets app_not_registered.
Q: Where does the 60-second handoff JWT TTL come from?
A: Airlock issues it; meridian only enforces payload.exp ≤ now in verifyAirlockHandoff and refuses any expired token.
Examples
A user clicks a stratt.dev link from Slack. Middleware sees no __stratt_meridian_session cookie, emits an auth.decision redirect span with reason no_cookie, and 302s them to airlock’s handoff URL. Airlock signs an EdDSA JWT bound to aud=https://stratt.dev, redirects back to /auth/callback, which verifies via JWKS and mints the local 8-hour HS256 cookie before redirecting to the original path.
neighbors on the map
- Airlock JWT Handoff & Session Cookies debugging login loops or session expiry
- LORE RBAC & Airlock Auth Flow implementing authentication in a new LORE page
- Airlock Bearer Auth diagnosing a 401 from /v1/ingest/*
- Auth-First WebSocket Handshake debugging AUTH_REQUIRED errors on the relay