Self-hosted Breeze behind Cloudflare Access: optional JWT trust on /auth/login to dedupe double-login #702
Replies: 4 comments
-
|
Thanks @bdunncompany — careful, well-scoped proposal, and the verification work (confirming inbound CF Access trust isn't implemented today, mapping the existing OIDC/JWT/agent-auth paths) is appreciated. Green light on the shape, with decisions on your three open questions: 1. The reasoning is the JWT shape you documented in point 5: the CF Access JWT carries no MFA claim ( (A per-partner setting is technically implementable — you resolve the user from the Keep your three env vars as proposed, including the explicit Net change to your env surface: just the added 2. JWKS failure — split into two rules, no separate knob. Don't model this as binary fail-open/fail-closed:
Don't add a fail-mode env var. Let the behavior follow from whether password login is enabled on that deployment: password path present → fall through; operator has disabled password auth entirely → JWKS failure blocks, which is then their explicit choice. Combined with the cached-JWKS-refresh-on- 3. Single AUD now. Have the verifier do AUD validation as an internal set-membership check (so a list is a trivial later change), but expose only a single Through-line: this is per-install infrastructure config — keep it all in env, no DB/UI surface, no speculative multi-tenant knobs. Revised file table is the four files you listed ( Open as a draft and I'll review. |
Beta Was this translation helpful? Give feedback.
-
|
Implemented and live on a self-host. Branch: Heads-up before opening the PR: the original spec (XHR header trust on What I learned by exercising the XHR pathI built the original spec exactly as agreed: verifier service, RS256 JWKS, four env vars with conditional Empirical finding: on a CF Access topology where the root app protects To make the JWT reach
The XHR-header pattern can't survive CF Access's per-app-cookie redirect when the auth endpoint itself is Access-enforced. Deleting the narrow app restored login. The XHR middleware remains correct for non-browser callers (CLI, server-to-server, MCP-style consumers presenting the header directly), it just doesn't get exercised by the SPA flow. The redirect-style endpoint that does workAdded a sibling route + SPA wiring that solves the same problem via top-level navigation, which does survive CF Access's redirects. Now also live on the same self-host. Flow:
Live result: fresh private browser → one Google sign-in → user lands logged into the dashboard. Zero Breeze password prompt. LogoutWithout something extra, "Sign out" only clears the Breeze session. CF Access holds its own cookies, so visiting Per https://developers.cloudflare.com/cloudflare-one/identity/authorization-cookie/, each domain's The Honest limitation: this is the strongest logout possible within the SSO contract. It clears all Breeze and CF Access state. But if the user's IdP (Google in my test deployment) still has an active session AND CF Access policy still passes for that identity, the very next visit to the app will mint a fresh JWT silently, with no user interaction. That's not a bug in this implementation; it's the SSO model working as designed. Fully ending the session in that browser requires also signing out at the IdP.
Open shape questions before opening the PR
Will open as draft once you confirm the shape. No client names anywhere in the PR. |
Beta Was this translation helpful? Give feedback.
-
|
Thanks @bdunncompany — that's an excellent piece of empirical work, and the writeup of why the XHR path fails for browser SPAs (Bypass policies skip header injection; narrowing the Access app triggers per-app cookie redirects that JSON fetch can't follow) is exactly the kind of finding that saves the next person a week. Glad you ran it to ground on a real deployment instead of trusting the docs. Decisions on your three questions: 1. Single PR. Land all three endpoints + the shared verifier together. The verifier service is the load-bearing piece for all three paths, the XHR middleware doesn't stand alone meaningfully (non-browser callers are a small fraction of the audience this feature is for), and the CSRF/cookie behavior of the redirect login + logout chain is best reviewed as one coherent flow. Total ~280 LOC for the auth surface is well within the scope of recent auth PRs in this repo (#781 was 9 security fixes in one). Subsection comments in the route file are fine; what I don't want is the XHR middleware merging then sitting unused for weeks waiting on the SPA-facing piece that actually solves the problem. 2. MFA handoff: defer to a follow-up PR. Keep the The reason to defer: the current MFA flow returns a tempToken in a JSON response ( When we do it, my preference is a short-lived (300s) httpOnly+sameSite=strict 3. Dedicated Concretely: new One cross-cutting flag for your PR: when the redirect endpoint sets the refresh cookie and 302s to Open as draft when ready — happy to review. |
Beta Was this translation helpful? Give feedback.
-
|
Shipped in #1058 (merged 2026-06-08). Self-hosted Breeze behind Cloudflare Access can now trust the Opt-in / fail-closed: off unless |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Self-hosted Breeze behind Cloudflare Access — login dedup proposal
Context (verified 2026-05-14)
Breeze ships a working OIDC SSO implementation (
apps/api/src/services/sso.ts:418definesPROVIDER_PRESETSincluding the Google preset; SAML presets at:817; OIDC callback + Breeze-session mint inapps/api/src/routes/sso.ts). User auth on every other request is the Breeze JWT verified inapps/api/src/middleware/auth.ts:290(authMiddleware). Agent connections are a separate path entirely —apps/api/src/middleware/agentAuth.ts:160validatesbrz_*tokens againstdevices.agentTokenHash. Mobile hits the same/auth/loginendpoint atapps/mobile/src/services/api.ts:315.Inbound Cloudflare Access JWT trust is not implemented.
grep -rE 'Cf-Access-Jwt-Assertion|cloudflare-access|cf_access' apps/api/src/returns zero hits. The onlyCF-Access-*references are outbound inapps/api/src/services/msiSigning.ts:94-95(calling an MSI signing service that's itself behind CF Access — inverse direction).The friction
Self-hosters who put Breeze behind Cloudflare Access today end up with a double login when CF Access uses the same IdP Breeze is configured with: user authenticates to their IdP at the CF Access edge, then hits Breeze's login form and authenticates to the same IdP again. Same identity, two cookies, two MFA prompts.
Proposal: opt-in CF Access JWT short-circuit on
POST /api/v1/auth/loginA small, gated middleware that activates only when the self-hoster explicitly turns it on.
CF_ACCESS_TRUST_ENABLED=true,CF_ACCESS_TEAM_DOMAIN=<team>.cloudflareaccess.com,CF_ACCESS_AUD=<application-aud>. Hosted SaaS never sets these; validator gates on the sameIS_HOSTEDpattern already used in the API config.POST /api/v1/auth/loginonly. Every other endpoint still requires a Breeze JWT fromauthMiddleware. CF Access keeps doing edge gating on UI paths;/api/v1/*and the agent WS continue to bypass CF Access at the edge as is already the deployed shape on a real install I tested against (UI paths 302 to<team>.cloudflareaccess.com; API + health + agent-WS return Hono status codes directly).https://<team>.cloudflareaccess.com/cdn-cgi/access/certs(Cloudflare's documented validation endpoint), verify RS256 signature, validateaudandiss, validateexp/nbf/iat. Match theemailclaim to an existing activeusersrow.emaildoesn't match an existing user, return 401. Edge identity is not allowed to create Breeze users.aud,email,exp,iat,nbf,iss,type,identity_nonce,sub,countryonly —auth_statuslives at/cdn-cgi/access/get-identity, not in the JWT). So this is a policy-trust decision, not a JWT parse: the self-hoster checks a partner-setting togglecfAccessTrustsMfaif their CF Access policy enforces MFA at the edge. When on, the minted Breeze JWT getsmfa: trueandrequireMfa()is satisfied without a second TOTP. When off, Breeze still requires its own MFA on protected routes — defense in depth.CF_AppSessioncookie. Documented in the deploy notes; the UI shows a hint when CF Access trust is on.brz_*tokens. CF Access service tokens for/api/v1/*are explicitly NOT proposed — the current edge bypass for the API + agent-WS paths is the right shape and avoids adding a CF-side credential the agent fleet would depend on (a CF outage would otherwise become a fleet outage).Code shape (rough)
apps/api/src/config/validate.tsapps/api/src/services/cfAccessJwt.ts(new)kidmiss), RS256 verifier viajose, claim-shape typesapps/api/src/services/cfAccessJwt.test.ts(new)apps/api/src/middleware/cfAccessLogin.ts(new)mfa: truedecisionapps/api/src/middleware/cfAccessLogin.test.ts(new)apps/api/src/routes/auth/login.tscfAccessTrustsMfaboolean column (location: wherever per-partner toggles live today — happy to take guidance)No changes to agent code, mobile, or the existing OIDC flow.
Open questions
cfAccessTrustsMfaon partner settings. If you'd rather have it on the per-orgssoProvidersrow (since CF Access conceptually maps to "this org's IdP fronts everything"), say so — same code, different home.Happy to put together the PR once these settle.
Beta Was this translation helpful? Give feedback.
All reactions