Skip to content

feat(auth): Cloudflare Access JWT trust + SSO redirect login (#702)#1058

Merged
ToddHebebrand merged 1 commit into
LanternOps:mainfrom
bdunncompany:feat/cf-access-jwt-sso
Jun 8, 2026
Merged

feat(auth): Cloudflare Access JWT trust + SSO redirect login (#702)#1058
ToddHebebrand merged 1 commit into
LanternOps:mainfrom
bdunncompany:feat/cf-access-jwt-sso

Conversation

@bdunncompany

Copy link
Copy Markdown
Collaborator

What

Optional support for self-hosters who put Breeze behind Cloudflare Access (Zero Trust). Today such a deployment forces a double login: Cloudflare Access auth, then the Breeze login form. This adds two opt-in, off-by-default capabilities so the CF Access identity can satisfy Breeze:

  1. Trust a verified CF Access JWT on /auth/login — when enabled, a request carrying a valid Cf-Access-Jwt-Assertion (verified against the team's JWKS, with AUD + issuer checks) logs the matching Breeze user in without re-prompting.
  2. Browser SSO redirect-login flow — a redirect endpoint + a small web bootstrap (bootstrapFromCfAccessRedirect) that mints a Breeze session from the CF Access identity and a logout chain that breaks the SSO redirect loop on sign-out.

Addresses Discussion #702.

Design / safety

  • Off by default. Requires CF_ACCESS_TRUST_ENABLED=true and both CF_ACCESS_TEAM_DOMAIN and CF_ACCESS_AUD set; boot validation enforces this.
  • JWT verification is fixed-host against https://<team-domain>/cdn-cgi/access/certs (no SSRF surface), checks signature, aud, and issuer. An attacker-issued token from a different team domain is rejected (covered by tests).
  • CF_ACCESS_TRUSTS_MFA is a separate explicit opt-in: it marks the resulting session MFA-satisfied by operator assertion (i.e. the operator is asserting CF Access enforces MFA upstream). Default off.
  • No change to the default login path when the flags are unset.

Files

  • New: services/cfAccessJwt.ts, middleware/cfAccessLogin.ts, routes/auth/cfAccessRedirectLogin.ts (+ tests).
  • Wired: routes/auth/login.ts, routes/auth/index.ts, routes/config.ts, config/env.ts, config/validate.ts.
  • Web: stores/auth.ts (bootstrapFromCfAccessRedirect), stores/featuresStore.ts, components/auth/LoginPage.tsx, components/auth/AuthOverlay.tsx.
  • Docs: deploy/cloudflare-access-trust.mdx.

Tests

  • API tsc clean; full API vitest 5827 passed / 0 failed (32 new tests across the 3 CF files cover happy path, wrong issuer, wrong aud, missing config, MFA assertion).
  • Web astro check clean; web vitest green.
  • All example domains/emails in code and tests are generic placeholders.

…ion LanternOps#702)

For self-hosters who front Breeze with Cloudflare Access: optionally trust a
valid CF Access JWT on /auth/login to dedupe the double-login, plus a browser
SSO redirect-login flow that mints a Breeze session from the CF Access identity
and a logout chain that breaks the SSO redirect loop.

- New: services/cfAccessJwt.ts (JWKS verify, fixed-host, AUD+iss checks),
  middleware/cfAccessLogin.ts, routes/auth/cfAccessRedirectLogin.ts.
- /auth/login trusts a verified CF Access JWT when CF_ACCESS_TRUST_ENABLED
  (off by default; requires CF_ACCESS_TEAM_DOMAIN + CF_ACCESS_AUD;
  CF_ACCESS_TRUSTS_MFA marks the session MFA-satisfied by operator assertion).
- Web: bootstrapFromCfAccessRedirect() completes the SPA handshake; LoginPage +
  AuthOverlay offer the SSO entry; logout chains app+team logout.
- Config env + validation wired; docs page added. All example domains are
  generic placeholders.
@ToddHebebrand ToddHebebrand merged commit d014636 into LanternOps:main Jun 8, 2026
25 of 26 checks passed
ToddHebebrand added a commit that referenced this pull request Jun 10, 2026
…logout redirect (#1193)

**Root cause:** Verification of the v0.69.0..main Codex review confirmed
three gaps in the Cloudflare Access SSO paths added in #1058 — the CF
cohort had strictly weaker token-theft protection than password users:

1. `GET /cf-access-logout` (`cfAccessRedirectLogin.ts:230`) was
synchronous and only called `clearRefreshTokenCookie` — no
`revokeAllUserTokens`, no jti revocation. After "Sign out", an
exfiltrated access token stayed valid to expiry and the 7-day refresh
token remained fully usable. `Header.tsx` routes CF logout exclusively
through this endpoint and skips `apiLogout()`.
2. Both CF token-mint paths (`cfAccessLogin.ts:164`,
`cfAccessRedirectLogin.ts:160`) called `createTokenPair` with no
`refreshFam`, so CF-minted tokens fall into the `/refresh` handler's
legacy skip path and the family-revocation reuse-detection (RFC 9700
§4.13.2) never fires for them — exactly the invariant
`refreshTokenFamily.ts`'s docstring warns about.
3. The logout redirect origin was built from the attacker-controllable
`Host` header (open redirect off the Breeze origin).

**Fix:**
- Logout resolves the refresh cookie, verifies the JWT
(signature-checked, so a forged cookie can't trigger revocation for
another user), then `revokeAllUserTokens(sub)` +
`revokeRefreshTokenJti(jti)` — mirroring `POST /logout`. Missing/invalid
cookie or Redis failure still clears + 302s (no 500).
- Both mint paths now do `mintRefreshTokenFamily` →
`createTokenPair(..., { refreshFam })` → `bindRefreshJtiToFamily`, same
as `/login`.
- Redirect origin comes from `DASHBOARD_URL || PUBLIC_APP_URL` (the
established pattern in `login.ts`/`password.ts`); https-pinned `Host`
fallback only when neither is set.
- Docs now instruct covering `/api/v1/auth/cf-access-login` +
`/cf-access-logout` in the Access application and warn against an
`/api/*` bypass swallowing them.

**Tests:** 11 new tests (logout revocation incl.
no-cookie/invalid-cookie/Redis-failure paths, family binding on both
mint paths incl. the MFA temp-token short-circuit, spoofed-Host redirect
cases). `cfAccessRedirectLogin.test.ts` + `cfAccessLogin.test.ts`: 32/32
pass; `login.test.ts` + `helpers.test.ts` regression: 11/11 pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants