RFC-002 — Identity-Aware Proxy: Koder ID SSO at the Koder Jet edge
Author: Rodrigo (with Claude as scribe) Date: 2026-05-30 Status: Accepted (2026-05-30, owner-approved). Implementation in progress — the capability ships opt-in (mode="sso", no vhost flipped); the global default-flip remains gated on the homologação signal (§7). Modules:
infra/net/jet(the edge proxy — enforces auth)services/foundation/id(IdP: OAuth2/OIDC, sessions, thedev-accessclaim)
Origin: ad-hoc conversation 2026-05-30, after the jet#188 post-mortem (the auth-gate email-approval channel was found broken AND structurally noisy). Anchored on policies/environments.kmd § "Lifecycle of the email-approval gate" (which already names "pure Koder ID auth" as the successor) and specs/auth/oauth-flow.kmd.
1. Summary
Replace email-approval-per-IP with an Identity-Aware Proxy (IAP) as the primary access-control mechanism for environment = dev | staging vhosts. Instead of allowlisting IPs after an out-of-band email approval, Koder Jet enforces a valid Koder ID session at the edge, before the request reaches the app. This is the BeyondCorp / Google IAP / Cloudflare Access / Pomerium / oauth2-proxy pattern, built from machinery the Stack already owns: Koder ID (OAuth2/OIDC + PKCE) as the IdP and Koder Jet (which gained an OAuth client + Bearer introspection in jet#188) as the proxy.
The email-approval gate is not deleted — it is demoted to a narrow guest-pass for external visitors without a Koder ID account.
2. Problem (why the email gate is structurally wrong as the default)
The email-approval-per-IP model has three defects that no amount of tuning fixes — they are inherent to the design:
- Noise scales with crawler traffic. dev/staging vhosts are publicly
resolvable + TLS-terminated, so the entire internet reaches them. The gate does work (enqueue pending, send email/push) for every hostile or automated hit. The jet#188 post-mortem: bots (bingbot, OAI-SearchBot, Censys, …) sweeping ~20+ gated
*.koder.devhosts exhausted the shared Stalwart(sender_domain, rcpt)rate bucket, so the owner's own approval mail was dropped — and the owner approved the wrong host's email instead. jet#190 (bot-UA suppression) and the Stalwart rate raise treat the symptom; they don't remove the class of problem.
- IP allowlisting is brittle for the legitimate user. Mobile carrier
IPs rotate; the owner re-approves constantly, and
permanent_whitelistwith a rotating /32 is a band-aid (observed live, 2026-05-30).
- Weak authorization (jet#189). The confirm callback accepts any
active Koder ID session and never binds the approver to the vhost's
admin. There is no concept of "who is allowed into dev".
3. Goals / Non-goals
Goals
- A team member with a Koder ID reaches any authorized dev/staging host
with zero per-host friction — log in once, SSO across all
*.koder.dev. - Bots/scanners get a login redirect and generate zero notifications.
- Authorization is identity/group based, not IP based.
- Reuse the jet#188 OAuth-client + introspection machinery; no new IdP.
Non-goals
- Replacing the VPN tier (truly-internal services stay VPN-only, per
environments.kmdline 43 — "VPN-only internal services not bound to public IPs"). - Deleting the email/guest-pass flow.
- Changing
prdbehavior (prd vhosts are never gated).
4. Design
4.1 Request flow (the IAP path)
request → *.koder.dev (dev/staging)
│
├─ has valid jet session cookie? ── yes ──► authz check (4.3) ──► app
│ no
▼
302 → https://id.koder.dev/oauth/v2/authorize
?client_id=koder-jet-iap & redirect_uri=https://<host>/__koder_auth__/callback
& response_type=code & scope=openid profile email & PKCE(S256) & state
│
▼ (user authenticates at Koder ID — never an embedded form, per oauth-flow.kmd R6)
302 → https://<host>/__koder_auth__/callback?code&state
│
▼ jet: exchange code (PKCE) → id_token+access_token; validate; resolve dev-access (4.3)
Set-Cookie: koder_edge_sess=<jwt>; Domain=.koder.dev; Secure; HttpOnly; SameSite=Lax
302 → original URLAfter the first login, the .koder.dev-scoped cookie means every dev host is reached without another round-trip (SSO).
4.2 Session
- Cookie:
koder_edge_sess, `Domain.koder.dev; Path/; Secure;HttpOnly; SameSite=Lax
(peroauth-flow.kmd` R8). - Value: a short-lived JWT signed by jet (HS/EdDSA, key in jet's
env), carrying
sub,email,dev_access,exp. jet validates it locally on every request — no introspection on the hot path. - Refresh: jet keeps the Koder ID refresh_token server-side (keyed by
an opaque session id in the cookie, OR encrypted in the cookie) and re-mints the edge JWT when it nears expiry. TTLs per
oauth-flow.kmdR8 (access 15m, refresh 180d, idle 90d). For an MVP, edge JWTexp = 12hwith silent re-auth is acceptable. - Callback path: `/_oderauth_