Skip to content

Enterprise · Single sign-on and SCIM

Sign in with OpenID Connect

An OIDC provider needs one redirect URI at the IdP and an issuer, client ID and client secret in FluxGate, and a connection test proves the issuer before anyone signs in.

Video coming soon

Sign in with OpenID Connect

Before you start

  • You read Single sign-on and SCIM and know which role sync mode you want.
  • You can register a web application at your OpenID Connect identity provider (IdP), such as Okta, Microsoft Entra ID or Keycloak.
  • The backend has FLUXGATE_ENCRYPTION_KEY set to the standard base64 of exactly 32 bytes. Or you provide the client secret through FLUXGATE_SSO_<SLUG>_CLIENT_SECRET (see Client secrets).
  • In production, public_base_url in config.toml is the public https address of the backend. For a local test you can leave it unset.
  • The roles and teams you want to map exist. Juniper Market maps to the roles Requester and Approver and the team Checkout.
  • Sign in as admin System admin.

How an OIDC sign-in works

FluxGate uses the Authorization Code flow with PKCE (S256). It is a confidential client: it authenticates to the token endpoint with the client secret, through client_secret_basic, or client_secret_post when the IdP does not advertise basic.

  1. The person clicks Sign in with Okta on /login. The backend creates a login state with a state value, a nonce and a PKCE verifier. The state is valid for 10 minutes and works once. The backend sets the cookie fluxgate_sso_state and redirects the browser to the authorization endpoint of the IdP.
  2. The IdP signs the person in and redirects the browser to the FluxGate callback with a code.
  3. The backend checks that the state cookie belongs to this login, consumes the state, and exchanges the code for tokens on the back channel.
  4. The backend validates the ID token, finds or creates the user, and syncs roles.
  5. The backend redirects the browser to the UI with a one-time code (valid 60 seconds, single use). The UI exchanges it for a normal FluxGate session.

The IdP tokens never reach the browser. FluxGate matches people by the provider and the sub claim only, never by the email of a known identity.

What FluxGate checks in the ID token

CheckRule
SignatureVerified with the keys from the IdP's JWKS. RS256, ES256 and PS256 only. none and HS256 are refused.
issEquals the issuer of the discovery document.
audContains the client ID. When aud has several values, azp must equal the client ID.
exp, iatValid, with 60 seconds of leeway.
nonceEquals the nonce of the login state.

FluxGate caches the discovery document and the JWKS for 10 minutes. A token signed with an unknown key ID triggers at most one JWKS refetch per issuer per minute, so a key rollover at the IdP works without a restart.

The redirect URI to register

Register this exact address at the IdP. The match is exact:

Redirect URI
<public_base_url>/api/v1/auth/sso/<slug>/callback

For Juniper Market's provider okta, that is https://fluxgate.example.com/api/v1/auth/sso/okta/callback.

The Redirect URI field in the provider form shows an address that the UI builds from the backend address your browser uses. Treat it as a guide: it can show a port that the backend does not send, for example :443. Register the address built from public_base_url.

If public_base_url is unset, FluxGate builds the redirect URI from each request and honors the Forwarded and X-Forwarded-* headers. A proxy that changes the host between the start of the sign-in and the callback makes the IdP reject the flow.

Add the provider

  1. Open Settings, Single Sign-On (/settings/sso) and click Add provider. The dialog Add SSO provider opens. Keep Protocol at OpenID Connect.
  2. Fill in Display name, Slug, Issuer URL, Client ID and Client secret. Leave Scopes at openid, email and profile. Set Groups claim if roles come from sign-in. See the table below.
  3. Click Copy next to Redirect URI and register the address at the IdP (see above).
  4. Set Allowed email domains, Create accounts on first sign-in, Link to existing accounts by email and Role sync mode. See Who may sign in and Role sync modes.
  5. Leave Enabled off. Click Create provider. The provider appears in the list.
  6. Click the pencil Edit provider on the row, then Test connection. Success shows Connection OK with the issuer and the authorization endpoint. A failure shows Connection failed and an Error: line.
  7. Close the dialog. Click the Group mappings icon on the row, add one row per target, and click Save mappings. The toast "Group mappings saved" appears.
  8. Open Edit provider again, switch Enabled on and click Save changes. The row shows an Enabled badge.
  9. Open /login in a private window. A Sign in with Okta button appears above an or divider. Sign in with a test account at the IdP and check the result under Settings, Users.
Add SSO provider dialog for OpenID Connect with display name, slug, issuer URL, the redirect URI to register, client ID, client secret, scopes, groups claim and role sync mode (opens full size in a new tab)
The Add SSO provider dialog for OpenID Connect. Copy the Redirect URI into your identity provider.

Test connection appears only after you save, and it tests the saved configuration, not unsaved edits. It fetches the discovery document and the JWKS from the backend host with a 5-second timeout. It checks the issuer, not the redirect URI or the client secret: the first real sign-in proves those.

Provider fields

FieldOkta (Juniper Market)KeycloakNotes
Display nameOktaKeycloakShown on the login button: Sign in with plus the display name.
SlugoktakeycloakLowercase letters, digits and dashes, up to 49 characters, unique. Part of the redirect URI.
Issuer URLhttps://junipermarket.okta.comhttps://keycloak.example.com/realms/acmeFluxGate fetches <issuer>/.well-known/openid-configuration.
Client IDFrom the Okta appfluxgate
Client secretFrom the Okta appFrom the Keycloak Credentials tabWrite-only. See Client secrets.
Scopesopenid email profileopenid email profileMust include openid.
Groups claimNot needed: roles come from SCIMgroupsThe claim that carries groups or roles.
Role sync modeSCIM (after a SCIM token exists)AuthoritativeDefault Authoritative.

Juniper Market's Okta provider uses the Okta org issuer, because its roles come from SCIM and the ID token needs no groups claim. To read groups at sign-in from Okta, use a custom authorization server instead (see the Okta recipe).

Where groups come from

FluxGate reads groups from the claim named in Groups claim:

  • It first tries the whole name as a literal key, which suits URL-style claim names. Then it tries a dot path, such as realm_access.roles.
  • A string is one group. An array contributes its string items. Other types give no groups.
  • If the ID token has no such claim and the role sync mode is Authoritative or Additive, FluxGate calls the userinfo endpoint and reads the claim there. The userinfo sub must equal the ID token sub.
  • FluxGate never reads the access token.

Client secrets

A badge next to Client secret shows Set, Not set, or Provided by environment variable (read-only). When you edit a provider, leave the field empty to keep the secret, or click Clear secret to remove it.

FluxGate encrypts a stored secret with FLUXGATE_ENCRYPTION_KEY. The provider ID is bound to the ciphertext, so a secret copied to another provider row does not decrypt. To keep the secret out of the database, set it per provider in the environment instead. Take the slug, uppercase it, and replace every character that is not a letter or digit with _:

Client secret for the provider okta
FLUXGATE_SSO_OKTA_CLIENT_SECRET=<Okta client secret>

The slug my-idp gives FLUXGATE_SSO_MY_IDP_CLIENT_SECRET. A non-empty variable wins over the stored secret and needs no encryption key, which suits secret managers and Kubernetes secrets.

Change the issuer later

If you change the Issuer URL, FluxGate deletes the linked identities of the provider in the same transaction, because a sub is only unique per issuer. Users keep their accounts. At their next sign-in they are linked again (by SCIM or by email) or created, under the usual rules. The audit entry sso_provider_updated records issuer_changed and identities_cleared.

Identity provider recipes

In each recipe, replace <slug> with your slug and register <public_base_url>/api/v1/auth/sso/<slug>/callback. Click Test connection, then switch Enabled on.

The Keycloak recipe was tested against a Keycloak server: sign-in, linking by verified email, and group sync. The Okta recipe was tested against an Okta tenant with the org authorization server and SCIM role sync. The groups claim steps (2 and 3) were not. The Entra ID recipe follows the vendor documentation and was not run against a live tenant.

Keycloak

  1. Create or pick a realm.
  2. Create a client: client type OpenID Connect, client authentication on (confidential), standard flow on. Set the valid redirect URI to the callback address.
  3. Copy the client secret from the Credentials tab.
  4. Choose how groups or roles reach the ID token or userinfo. Pick one:
    • Groups. Add a client scope mapper of type Group Membership with the token claim name groups, Full group path off, added to the ID token and userinfo. Group names then arrive as FluxGate-Checkout, not /FluxGate-Checkout. Set Groups claim to groups.
    • Realm roles. Set Groups claim to realm_access.roles, and turn on Add to ID token or Add to userinfo in the realm roles mapper.
    • Client roles. Under Clients, your client, Client scopes, the dedicated scope, add a mapper of type User Client Role. Set the token claim name to groups, Multivalued on, Add to ID token on and Add to userinfo on. Set Groups claim to groups. Or, in the built-in roles client scope, turn on Add to ID token in the client roles mapper and set Groups claim to resource_access.<client>.roles.
  5. In FluxGate, set Issuer URL to https://<host>/realms/<realm>.
  6. Make sure users have a verified email if you use Allowed email domains or Link to existing accounts by email.

Keycloak puts client roles only in the access token by default. A client role mapping does nothing until a mapper adds the roles to the ID token or userinfo.

Okta

  1. Open Applications, Create App Integration, Classic experience, then OIDC - OpenID Connect of type Web Application, with the Authorization Code grant. Okta pre-fills the name "My Web App" and localhost redirect URIs: set the name, replace the sign-in redirect URI with the callback address, and remove the sign-out redirect URI. Copy the client ID and the client secret from General, Client Credentials.
  2. Roles from sign-in groups only: use a custom authorization server (Security, API). The org authorization server does not support custom claims. The issuer is https://<org>.okta.com/oauth2/<server-id>. When roles come from SCIM, skip this step and step 3, and use the org URL https://<org>.okta.com as the issuer.
  3. On that server, add a groups claim named groups, included in the ID token (Always), with the value type Groups and a narrow filter, for example Starts with FluxGate-. A narrow filter keeps the token small.
  4. Assign the app to the people or groups who may sign in.
  5. In FluxGate, set Issuer URL to the issuer from step 2, and Groups claim to groups (not needed with the role sync mode SCIM).

Okta sends email_verified, so domain lists and linking by email work. For the full Okta walk-through with SCIM, see FluxGate with Okta.

Microsoft Entra ID

  1. Under App registrations, create a registration with the platform Web and the callback address as redirect URI. Create a client secret under Certificates and secrets.
  2. Use the v2.0 issuer https://login.microsoftonline.com/<tenant-id>/v2.0. If the issuer in tokens is the v1 form, set requestedAccessTokenVersion to 2 in the app manifest.
  3. Recommended: define app roles in the app registration, assign users or groups to them under Enterprise applications, and set Groups claim to roles. The role values are the group values in your mappings.
  4. Alternative: add a groups claim under Token configuration, and use the group object ID as the group value. A user in many groups (more than 200) triggers overage: Entra sends a pointer instead of the groups. FluxGate treats this as no groups and logs a warning. In Authoritative mode this removes the SSO-managed roles of that user. Prefer app roles, or select "Groups assigned to the application".
  5. Entra does not send email_verified. With a non-empty Allowed email domains, or with Link to existing accounts by email, FluxGate rejects these sign-ins. Leave the domain list empty and restrict access through user assignment on the enterprise application. For SCIM, link on oid and externalId (see SCIM provisioning).

Good to know

  • Only enabled providers show a button on /login and accept sign-ins. The button is hidden when no provider is enabled.
  • Discovery, JWKS, token and userinfo responses are limited to 1 MiB, with a 10-second timeout and no redirects.
  • If the same new person finishes two first sign-ins at the same time (for example in two tabs), one creates the account and the other fails with sso_provider_error. Signing in again works.
  • Test connection runs from the backend host, so it can reach internal hosts. Only system admins can run it.
  • After a sign-in, the person lands on the page they asked for. FluxGate accepts only a path inside FluxGate as the return address.

Troubleshooting

Saving fails with "Server has no FLUXGATE_ENCRYPTION_KEY"

The key is missing or is not 32 bytes of standard base64. Set a valid key and restart the backend, or set FLUXGATE_SSO_<SLUG>_CLIENT_SECRET.

The IdP reports a redirect_uri mismatch

The registered address differs from the one FluxGate sends. Set public_base_url and register the exact address <public_base_url>/api/v1/auth/sso/<slug>/callback.

There is no login button

The provider is disabled. Open Edit provider and switch Enabled on.

"Sign-in session expired. Try again."

The code is sso_state_invalid. A proxy strips the fluxgate_sso_state cookie, a second tab replaced it, or the sign-in took longer than 10 minutes. Forward cookies on /api/v1/auth/sso/, use one tab, and start again.

"Your email domain is not allowed for this sign-in method."

The domain is not in Allowed email domains, or the IdP does not send email_verified: true. Add the domain, mark emails verified at the IdP, or empty the list. Entra ID never sends email_verified.

Roles are not synced

Check the Groups claim name, that the Role sync mode is not Off, and that the group value matches in case. With Keycloak, Full group path on sends /FluxGate-Checkout: turn it off or map the value with the slash. Roles that appear only in the access token are never read: add a mapper for the ID token or userinfo.

A role change fails with 409 sso_managed

Sync owns the assignment. Change the group at the IdP or change the mapping.

Next

Connect a SAML identity provider in SAML 2.0, or let the IdP manage accounts with SCIM provisioning.