CRUMB a card from devarno-cloud

JWT RS256 & Tier Middleware

skyflow intermediate 5 min read

ELI5

Skyflow signs JWTs with an RS256 private key kept only by Core API; every other service verifies with the public key. Every authenticated request goes through three Fiber middlewares in order: JWT verify → tier check → rate limit. Logout deletes the Redis session, instantly invalidating subsequent requests even if the token’s exp is still in the future.

Technical Deep Dive

Key Material

FileHolderPurpose
/secrets/jwt-private.pemcore-apisign access + refresh tokens
/secrets/jwt-public.pemcore-api, realtime-gatewayverify signatures

Asymmetric (RS256) so the verifier never holds the signing key — minimises blast radius if a downstream service is compromised.

Token Lifecycle

sequenceDiagram
autonumber
participant C as Client
participant API as core-api
participant RD as Redis
participant RT as realtime-gateway
C->>API: POST /api/v1/auth/login
API->>API: bcrypt compare
API->>API: sign JWT (RS256, 1h exp)
API->>RD: SET session:{user_id} TTL=1h
API-->>C: {access_token, refresh_token, expires_at}
C->>API: Authorization: Bearer <jwt>
API->>API: JWTAuth middleware (verify)
API->>RD: EXISTS session:{user_id}
API->>API: RequireTier / RateLimit
API-->>C: 200
C->>API: POST /api/v1/auth/logout
API->>RD: DEL session:{user_id}
API-->>C: 204
C->>RT: GET /sse?token=<jwt>
RT->>RT: verify with jwt-public.pem
RT-->>C: SSE stream

Middleware Chain

flowchart LR
R[Request] --> M1["JWTAuth — verify RS256, set user_id"]
M1 -->|"missing/invalid"| E1[401 unauthorized]
M1 --> M2["RequireTier — fetch users.tier vs minTier"]
M2 -->|"tier < min"| E2[403 upgrade_required]
M2 --> M3["RateLimit — INCR ratelimit:{u}:{path}"]
M3 -->|"count > limit"| E3[429 rate_limit_exceeded]
M3 --> H[Handler]

Status Codes

CodeTriggerBody field
401missing / bad / expired token, or revoked session
403tier below minTiererror: "upgrade_required", required_tier: …
429per-user-per-path counter exceedederror: "rate_limit_exceeded", limit, reset_at

Defence-in-Depth

SurfaceMechanism
Password storagebcrypt cost 12
API key storageSHA-256 hash, plaintext shown once on creation
Session revocationRedis DEL session:{user_id} on logout
Webhook authenticityHMAC verification (Polar)
TransportTLS terminated at Caddy
Headershelmet middleware (CSP, HSTS)
SQL injectionpgx parameterised queries

Refresh Flow

POST /api/v1/auth/refresh exchanges a refresh token for a new short-lived access token. The refresh token lifetime is longer than 1h (exact value not in spec); access token TTL is 1h to bound damage from leak.

Key Terms

  • RS256 → RSA-SHA256 asymmetric signature; only the holder of the private key can sign, anyone with the public key can verify
  • Session in Redis → enables instant revocation that JWT alone cannot provide (since JWTs are stateless)
  • Bearer tokenAuthorization: Bearer <jwt> header form; on SSE, falls back to ?token= query param
  • RequireTier(minTier) → Fiber middleware factory; Tier enum comparison is integer ordering (DRIFT<LIFT<JET<ORBIT)

Q&A

Q: Why does the realtime-gateway only get the public key? A: It only needs to verify, not issue. Withholding the private key means a compromised gateway cannot forge tokens.

Q: Why a Redis session on top of a JWT? A: To support immediate logout / revocation. A pure JWT is valid until exp; the Redis check converts the JWT into a session-bound token so logout actually does something.

Q: How is the SSE endpoint authenticated when the browser cannot set custom headers on EventSource? A: The token is passed as ?token= query param. This is acceptable because the connection is TLS-encrypted and the URL is not logged at the request-line level by Caddy in the dev config — but it does land in browser history. Treat as documented trade-off.

Q: What happens on token expiry while an SSE connection is open? A: The current spec does not document mid-connection re-validation; the connection persists until the client disconnects or the gateway restarts. Refresh-while-streaming is a known gap, not a guarantee.

Examples

Like a museum wristband. The cashier (core-api) issues a holographic band signed with their stamp (RS256 private key). Every gallery guard (other services / middleware) holds a UV light (public key) to verify, then checks your tier sticker (RequireTier) before letting you into the planetarium. Cutting the wristband at the exit (logout / Redis DEL) invalidates it even though the date printed on it is still good.

neighbors on the map