Skip to content
downpipes docs

Single sign-on overview: bring your own identity provider

downpipes can let your team sign in with the identity provider you already run. The engine is its own native OpenID Connect relying party, its own native OAuth2 client, and its own native SAML 2.0 service provider, all running as a Worker inside your own Cloudflare account. There is no Cloudflare Access in the path: Access stays additive, never a prerequisite. You point a connection at your provider, the engine completes the federated sign-in itself, and a verified person lands in the console.

This page is for a self-hoster deciding whether and how to wire single sign-on. It sets the no-custody and optional-and-additive expectations, lays out the exact menu of providers, and states the v1 scope limits plainly so nothing surprises you on the step-by-step pages. The procedure itself lives on Connect OIDC or OAuth2 and Connect SAML.

Optional, and never a lockout

Single sign-on is something you add, not something you depend on. The passkey owner front door and the break-glass shared token stay independent of any connection. So an identity provider outage is a greyed-out sign-in button, not a way to be locked out of your own backups: the owner can still reach the console on a passkey, and the break-glass token still works. That independence is the whole reason an external provider is safe to bolt on.

The flip side is just as direct. Disabling or deleting a connection takes effect at once. The engine bumps that connection’s session epoch, which kills the connection’s live sessions, and the connection drops out of the pre-authentication provider list so its sign-in button disappears immediately. There is no lag where a removed provider keeps minting sessions.

However a person signs in, roles work the same way

Cloudflare Access, a native provider here, and a passkey all resolve to the same role and group-to-role machinery. A connection only decides who can authenticate. What they can then do is decided on the Roles and capabilities tab and through group-to-role mapping, identically across every sign-in method.

The provider menu

The preset registry ships 8 named providers plus a Generic OIDC hatch and a Generic OAuth2 hatch. The named presets pre-fill the endpoints, scopes, claim names and the provider-specific gotchas, so you type only a handful of values (a tenant id, an Okta domain, a Keycloak realm) plus your client id and, for a confidential client, a secret.

Provider Protocol Notes
Microsoft Entra ID OIDC Single-tenant v2.0 endpoints; App Roles map cleanly to roles
Okta OIDC Uses a custom authorisation server so a groups claim can be emitted
Google Workspace OIDC The hosted-domain (hd) gate is mandatory; signs people in, no groups in the token
Keycloak OIDC Roles and groups must be toggled into the ID token, not just the access token
JumpCloud OIDC One shared issuer for all tenants; trust rests on the client id and the connection id folded into each subject
Auth0 OIDC Issuer carries a mandatory trailing slash; roles are a namespaced custom claim
GitLab OIDC GitLab.com and self-managed; reads the groups_direct claim
GitHub OAuth2 No ID token; identity from the GitHub API, keyed on the immutable numeric id
Generic OIDC OIDC Any spec-compliant .well-known/openid-configuration (PingOne, OneLogin, Authentik, Zitadel, and similar)
Generic OAuth2 OAuth2 The no-ID-token hatch for any OAuth2 provider (Discord, Bitbucket, and similar)

The split between the two protocols matters for how trust is established, covered next.

OIDC versus OAuth2 trust

An OIDC connection (every named provider except GitHub) gets a signed id_token from the provider, and the engine cryptographically verifies it. An OAuth2 connection (GitHub, and the Generic OAuth2 hatch) has no id_token at all. For an OAuth2 connection, trust derives from the TLS-protected token exchange, and identity comes from a userinfo call made with the resulting access token. The access token is opaque and is used only as a bearer credential against the provider’s own API.

That is an important honesty point. GitHub and Generic OAuth2 do not verify any token signature, because there is no signed token to verify. They are still safe, because the code exchange happens over a screened HTTPS fetch and the identity is read straight from the provider’s authenticated API, keyed on the immutable user id rather than a renameable username. But do not picture a signed assertion where there is not one.

No custody by construction

A stored connection record never holds a client secret value. The record carries a secret descriptor only: a mode, and an optional non-secret reference. That descriptor is what the console and the audit trail see, so neither ever holds your secret.

There are four named secret-custody modes in the type, but only two are wired in this build. A connection record describes itself with one of these:

Mode What the record holds Wired today
pkce-public Nothing. A public client using PKCE only, no secret at all Yes
do-plaintext A reference only. The secret value lives write-only in a separate engine key, never read back to the console Yes
secrets-store A Cloudflare Secrets Store binding name No. Refused at save time
private-key-jwt A private-key reference No. Refused at save time

The two unwired modes are the trap to know about, but they cannot trap you silently: a connection configured for secrets-store or private_key_jwt is refused at save time, not at first login. Validation runs before the record is ever written to storage, so a proposal in either mode is rejected with a 400 and nothing is persisted. The console deliberately offers only the two working choices, a public client (PKCE) or a write-only do-plaintext secret, so you cannot even reach that rejected save from the UI.

A SAML connection has no client secret of any kind. Its trust root is the set of public X.509 signing certificates you pin, which are not secret and live in the record openly.

The key fetch cannot be steered off your provider

Because a connection’s issuer is a value you supply, it is also the value an attacker would want to change. The engine screens every call it makes to a provider, discovery, the JWKS key fetch and the token exchange, before the request leaves: the URL must be https, never an IP literal, and never localhost or a .localhost name, with a second classifier for the IPv6 unique-local, link-local and obfuscated forms an IP check alone misses. Redirects are never followed, so a 3xx towards a metadata address or a private range is refused rather than chased. The same pin applies to the Cloudflare Access path, where the key fetch is held to an https host under *.cloudflareaccess.com, so a tampered team-domain value cannot redirect it either. See connect an OIDC provider for the mechanism in full.

Only two secret modes actually sign you in

Use a public client (PKCE) or a write-only do-plaintext secret. Cloudflare Secrets Store and private_key_jwt are not options: the engine refuses to save a connection in either mode, so you would see the rejection immediately, not at first login. The console only surfaces the two that work.

v1 scope limits

A few capabilities are deliberately not in v1. Knowing them up front saves a misconfiguration.

The SAML service provider is sign-only and SP-initiated. It rejects an EncryptedAssertion outright (there is no decryption key, and none is advertised in the metadata), and it does not expose an identity-provider-initiated flow. The connection type carries an allowIdpInitiated flag, but the console hard-codes it off, and the access endpoint enforces SP-initiated-only by consuming a single-use RelayState record that only an SP-initiated request can have created. The full SAML behaviour and the email-trust policy are covered on Connect SAML.

If you have a SAML-only provider and would rather drive it through the OIDC path, a SAML-to-OIDC bridge is an optional self-hosted recipe you stand up yourself. It is not part of the engine.

Who can manage connections

Connecting an identity provider decides who can sign in, which makes it an authentication trust root. A group mapped through a connection can even confer access-admin, so connection management is reserved to the owner-exclusive keys.ceremony capability, not the broader access.policy. A generic administrator cannot add, remove, or enable a provider.

Once a second owner exists on the account, creating, deleting or enabling a connection additionally takes a second owner’s approval (a break-glass admin-token sign-in is the exception). Each of those actions runs through the dual-control owner gate, the same second-owner approval pattern used for the other account-takeover-sensitive operations, so no single owner can quietly change who is allowed in. For the wider second-owner model see dual control for recovery and the change-control page.

Where this fits

Last updated .