Skip to content

Cloudflare Access JWT trust

When you put Breeze behind Cloudflare Access (or any Zero Trust gateway that mints CF Access JWTs) and use the same identity provider that you’ve configured users for in Breeze, the user ends up authenticating twice: once at the CF Access edge, once at the Breeze login form. Enabling Cloudflare Access JWT trust lets a valid Cf-Access-Jwt-Assertion header short-circuit POST /api/v1/auth/login and mint a Breeze session directly.

This is an opt-in, deployment-wide flag. It does not affect any deployment that does not set CF_ACCESS_TRUST_ENABLED=true.

  1. The browser hits the Breeze SPA. Cloudflare Access authenticates the user against your IdP and attaches a signed CF_Authorization cookie plus the Cf-Access-Jwt-Assertion request header.
  2. The SPA POSTs /api/v1/auth/login. The CF Access middleware runs first.
  3. The middleware verifies the JWT using the team’s JWKS at https://<team-domain>/cdn-cgi/access/certs. It checks:
    • signature (RS256 only)
    • issuer = https://<team-domain>
    • audience = the configured AUD tag for your application
    • exp, iat, email, aud, iss, sub claims all present
  4. On a verified JWT, it looks up the user by email claim, confirms the account is active, resolves the user’s partner/org context, mints a token pair, and returns the same shape POST /login returns on a successful password login.
  5. On any failure — flag off, header absent, invalid signature, expired token, wrong audience, JWKS unreachable, user not in Breeze, user inactive — it falls through to the existing password handler. The browser then sees the normal login form.
FailureBehaviourReason
CF_ACCESS_TRUST_ENABLED=false or unsetNo JWT path; password handler runs as beforeDefault off
Header absentFall through to passwordNot every browser session has a CF Access JWT
Invalid signature, wrong issuer/aud, expired, missing claimFall through; logged as [cf-access-login] rejected JWT with the jose error codeFail-closed on trust: we never mint a session from an unverified JWT
JWKS fetch / network failureFall through; logged as [cf-access-login] JWKS unavailable...Fail-open on availability: a transient CF outage shouldn’t wedge /login
User from JWT email not present in BreezeFall throughPassword handler will 401 with the same generic error, no email enumeration
User present but status != 'active'Fall through; failure audited as account_inactive with method: cf_access_jwtSame denial path as password login

The Cloudflare Access JWT does not carry an MFA claim — it tells you the user satisfied your CF Access policy, but not how. Whether your CF Access policy required step-up (hardware key, WARP attestation, etc.) is an operator-level assertion. CF_ACCESS_TRUSTS_MFA is the knob:

  • CF_ACCESS_TRUSTS_MFA=false (default) — even on a valid JWT, if the matched Breeze user has MFA enrolled, the middleware issues a tempToken and the SPA’s normal MFA challenge runs. The user does not have to re-enter their password, but they do enter their TOTP code.
  • CF_ACCESS_TRUSTS_MFA=true — the minted Breeze session is marked as MFA-satisfied. Only turn this on if your CF Access policy actually requires step-up. The setting is honoured deployment-wide; it does not vary per partner or per user.
VariableRequired when trust is onDescription
CF_ACCESS_TRUST_ENABLEDtrue to enable. Boolean (true/false/1/0/yes/no/on/off). Default off.
CF_ACCESS_TEAM_DOMAINYesBare hostname of your Cloudflare team domain, e.g. example.cloudflareaccess.com. No https:// scheme.
CF_ACCESS_AUDYesThe AUD tag for the Cloudflare Access application that protects Breeze. Get it from the Cloudflare Zero Trust dashboard → Access → Applications → your app → AUD.
CF_ACCESS_TRUSTS_MFABoolean. Default false. See MFA above.

The config validator refuses to boot if CF_ACCESS_TRUST_ENABLED=true but CF_ACCESS_TEAM_DOMAIN or CF_ACCESS_AUD is empty, or if the team domain looks like a URL instead of a hostname.

  • Application path: cover the SPA root, /api/v1/auth/login, /api/v1/auth/cf-access-login, and /api/v1/auth/cf-access-logout. Leave bypass rules on /api/* agent paths, /health, /installers/*, and any installer short-links (the agent fleet does not have a CF Access session). If you use a blanket bypass rule on /api/*, make sure it does not swallow /api/v1/auth/cf-access-login or /api/v1/auth/cf-access-logout — those two endpoints must be enforced by the Access application (more-specific paths win), otherwise the JWT never reaches the redirect login handler and sign-out cannot clear the CF Access session.
  • Identity provider: pick the same IdP whose email claim is the same one your Breeze users are provisioned with. If a Breeze user’s email is [email protected] and your IdP issues the JWT with [email protected], you’re set.
  • Session duration: anything you like. Breeze mints its own refresh token independently of the CF Access cookie.
  • Application AUD: copy from the dashboard once the application is created. This is stable for the life of the application.

Set CF_ACCESS_TRUST_ENABLED=false (or remove the variable) and restart breeze-api. The middleware short-circuits to next() before reading any other env var, so disabling is instant.