BeziSync

Single sign-on

Admins set up sign-in in Settings → Sign-in, with as many providers as they need, of three kinds:

  • OpenID Connect: Google, Microsoft Entra ID, Okta, Keycloak, GitLab, Dex, Auth0, Authentik, … Enter the issuer URL, client ID and client secret. ID tokens are verified against the issuer's keys and need a verified email.
  • OAuth2 for providers without OpenID Connect. GitHub (github.com or GitHub Enterprise Server: put your host in the three URLs) reads the person's primary verified address from /user/emails and their organizations (acme) and teams (acme/data-platform) as groups; it needs the user:email scope, and read:org for groups. Other OAuth2 providers (Gitea, …) take an authorization, token and user URL and the names of the user answer's fields (id, email, "email verified", name, groups); an address counts as verified only when that field says so.
  • SAML 2.0: Okta, Entra ID, ADFS, Google Workspace, JumpCloud, … Give the identity provider BeziSync's entity ID / metadata URL (<public URL>/api/v1/auth/saml/metadata) and ACS URL (<public URL>/api/v1/auth/saml/acs, HTTP-POST), all shown in the form; then give BeziSync the identity provider's metadata URL (re-read at every Test, so rotated certificates are picked up), paste its metadata, or enter its entity ID, HTTP-Redirect sign-in URL and signing certificate. Assertions must be signed by that certificate, name BeziSync as their audience, answer the request BeziSync sent (each request and assertion is accepted once) and carry a persistent or email NameID; the email, name and groups attributes are found under their common names unless you name them. Sign-in started at the identity provider is off unless you allow it.

Attribute mapping. OpenID Connect providers are read from the ID token's email, email_verified and name claims unless you name others (Entra ID without a mailbox: email claim upn); OAuth2 providers from the user answer's fields you name; SAML from the attributes you name or the common ones (givenname + surname make the name when there is no display name; repeated attributes are merged). A claim may be a dotted path into the token (Keycloak's realm_access.roles); an exact name wins, so Auth0's namespaced claims such as https://acme.com/groups work too.

Verified email. An address counts only when the provider vouches for it: OpenID Connect's "email verified" claim must be true (Entra ID: add the optional claim xms_edov and name it, as the Entra preset does); OAuth2 needs a field you name for it (GitHub: its primary verified address); a signed SAML assertion vouches for its address. Accept unverified email lets such people in, but only creates new accounts: an unverified address never signs in to an existing one.

Groups that don't fit. When a provider leaves groups out (Entra ID sends a link instead of the groups past 200 in a token or 150 in a SAML assertion; GitHub hides organizations whose SAML single sign-on the sign-in isn't authorized for, X-GitHub-SSO: partial-results), BeziSync refuses the sign-in with the reason instead of syncing an incomplete list, which would remove roles. Send only the groups assigned to the app, or authorize the organization on GitHub (github.com/orgs/<org>/sso) and sign in again.

GitHub. Use an OAuth app (Settings → Developer settings → OAuth Apps); a GitHub App's user token needs the "Email addresses" and "Members" read permissions. Organizations that restrict OAuth apps show no teams until an owner approves BeziSync's app. github.com names itself https://github.com/login/oauth in its callback (RFC 9207): the preset checks it, so a response from anywhere else is refused.

Callbacks name their issuer. An OpenID Connect callback's iss must be the provider's issuer, and is required when the provider advertises it; an OAuth2 provider's is checked when you set its issuer identifier.

The redirect URI for OpenID Connect and OAuth2 is <public URL>/api/v1/auth/oidc/callback, the same for every provider. Client secrets are encrypted and never shown again; leave the secret empty when editing to keep it. Test checks what can be checked without a person (discovery and keys, reachable endpoints, certificate validity); the first real sign-in proves the rest. The sign-in page shows one button per provider that is turned on.

Group rules grant access from the provider's groups (name the claim or attribute if it is not groups, and ask for the scope that adds it):

Group Grants
data-platform Admin
payments editor in payments, may approve
analysts viewer in every project

Project roles are viewer, operator and editor. People whom no rule grants any project (including when the provider sends no groups) get the provider's default role in its default projects, or no access when there is no default role. Roles granted by rules and the default role follow the person's groups at every sign-in and show Managed by SSO in Settings → People. Changing such a person's role or approver flag by hand takes that membership off group sync, so the change sticks; roles granted by hand are never changed by SSO.

One sign-in option per person. The first sign-in links a person to that provider's identity (and to an existing account with the same email). Someone already linked to another provider is refused, so one provider can't take over another's accounts. When you move to a new provider, turn on Take over people who sign in with another option on it: people then move to it at their next sign-in (audited).

Email and password sign-in can be turned off once a provider passed its test and an Admin has signed in with it (so someone can still manage BeziSync). While it is off, the sign-in page shows only the providers; API tokens and MCP keep working. BeziSync refuses to turn off or remove the last provider while password sign-in is off. If the provider breaks, run bezisync enable-password-login in an API or controller pod to turn password sign-in back on.

The BEZISYNC_OIDC_* settings of earlier releases are no longer read (the API logs a warning while any is still set): add the provider in Settings instead.

Edit this page on GitHub