CRUMB a card from devarno-cloud

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 safeNext

Token Roles

TokenAlgorithmTTLVerifierWhy
Airlock handoffEdDSA (Ed25519)60sJWKS at airlockAsymmetric, no shared secret across apexes
Meridian sessionHS2568h (MERIDIAN_SESSION_TTL_SECONDS)local secretFast, 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.delete clears any stale local cookie.
  • Open-redirect guard: next must 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