OIDC single sign-on
Fadebox accepts interactive sign-ins two ways, both server-side and cookie-based:
- Form login — username/password against the local user database. Always enabled and not disableable: a misconfigured identity provider must never lock everyone out of the installation.
- OIDC single sign-on — any number of standards-compliant providers (Keycloak, Entra ID, Auth0, Okta, …), added and edited at runtime.
Providers are data, not deployment configuration. Each one is a row an admin creates in the UI; adding, editing or disabling one takes effect on the next request, with no environment variables, no restart and no rebuild. With no provider configured, the sign-in page shows only the password form.
:::info Upgrading from an earlier build?
Fadebox used to take a single provider from QUARKUS_OIDC_* and FADEBOX_SECURITY_OIDC_*
environment variables. Those no longer exist — delete them from your compose file and re-create the
provider under Access → Identity Providers. The callback URL also changed: it is now
per-provider, …/oidc/callback/{slug}.
:::
Add a provider
Access → Identity Providers → Add provider, as a global admin. The same catalog is available over
the API at /api/admin/oidc-providers.
| Field | Meaning |
|---|---|
| Slug | Short identifier such as corp. It becomes part of the login and callback URLs and of the session cookie name, so it is immutable once created — renaming would orphan every live session. |
| Display name | Label of the sign-in button, e.g. Acme SSO. |
| Issuer | The provider's issuer URL, e.g. https://idp.example.com/realms/acme. Fadebox fetches the OIDC discovery document from it. https only. |
| Client ID and Client secret | A confidential client registered at the provider. The secret is write-only: the API accepts it and never returns it. |
| Scopes | Requested scopes; defaults to profile,email. |
| Groups claim | Which token claim carries group values. Defaults to groups; providers disagree, so this is per provider. |
| Required claims | Empty by default. Who may sign in through this provider, beyond being known to it. See Required claims below. |
| Link by verified email | Off by default. See Account linking below. |
| Auto-provision | On by default: create a local account on first sign-in. |
| End the provider session on sign-out | On by default: where the provider supports it, a Fadebox sign-out also ends the session at the provider. Off keeps the provider session and clears only Fadebox's own. See below. |
| Enabled | A disabled provider shows no button and its slug returns 404. The row survives, so accounts that signed in through it keep resolving. |
Register the client at your provider
Per provider, using its slug:
| Redirect URI | https://<your-fadebox-host>/oidc/callback/<slug> |
| Post-logout redirect URI | https://<your-fadebox-host>/sign-in |
| Client type | Confidential (a client secret is required) |
| Scopes | At least profile and email — preferred_username and email drive account resolution |
Fadebox runs the authorization-code flow server-side, one Quarkus tenant per provider: tokens
never reach the browser, and the session lives in an encrypted q_session_<slug> cookie. Sign-out
is RP-initiated where the provider supports it — it clears the Fadebox session and ends the
provider session. Fadebox reads each issuer's discovery document to see whether it advertises an
end_session_endpoint; a provider without one (Google, most prominently) gets a
local sign-out instead: the Fadebox session is cleared, the provider session stays. Detection
is per provider, and the provider's End the provider session on sign-out switch turns the
provider-side half off where you do not want it — an Entra tenant whose people should stay signed
in to Microsoft, say. Off gives that provider the same local sign-out Google gets, with the same
consequence: the sign-in button signs straight back in without a prompt, because the provider
session is still there.
Required claims
A provider tells Fadebox who a person is. Some providers know everybody: "Sign in with Google" authenticates every Google account in the world, a Gmail address as readily as one from your Workspace, and a multi-tenant Entra ID registration accepts every tenant. Required claims say who, of the people the provider knows, may sign in here — only people from your Google Workspace domain, only members of one group, only one Entra tenant.
The provider form lists them as rules, one row each: the claim, a matches select, and the values.
| Part | Meaning |
|---|---|
| Claim | The name of a claim in the ID token, spelled the way the provider spells it: hd, email, tid, groups, a namespaced URI. |
| matches | exactly (the default) — the claim equals the value. ends with — the claim ends with the value. |
| Values | One or more values the claim may have, entered as one comma-separated line (example.com, example.org). |
Over the API (/api/admin/oidc-providers) the list is the provider's requiredClaims field,
on create, update and read alike:
"requiredClaims": [
{ "claim": "hd", "values": ["example.com"], "match": "EXACT" },
{ "claim": "groups", "values": ["platform-team"] }
]
match is EXACT or SUFFIX and defaults to EXACT. An update that leaves requiredClaims
out keeps the stored rules; an empty list removes them. A rule with a blank claim name, no values
or a blank value is refused as 400 claim_rule_invalid, naming the rule's index and the
reason.
How the rules are read:
- Every rule must pass. Two rules — a domain and a group — admit only people who have both.
- Any listed value satisfies a rule. "Either of our two domains" is one rule with two values.
A claim that carries a list, such as
groups, passes when any of its entries matches any of the values. - A missing claim fails its rule. This is the point of the Google case: a Gmail account does
not carry an
hdwith the wrong value, it carries none — and is refused. It also means a typo in the claim name refuses everybody at their next sign-in, so you find out at once. - A rule on
emailalso requires a verified address. It passes only when the token carriesemail_verifiedset totrue(the JSON boolean or the string"true", not a list holding it) as well as a matching address;email_verifiedabsent orfalsefails the rule — see below. Required claims are new in 1.4 and have always checked this, so no rule written before changes meaning. - Matching is case-sensitive, as claim values are.
- No rules, no filter. A provider with an empty list admits whoever the provider authenticates, as before.
Three rules that cover the common cases:
| To admit | Claim | matches | Values |
|---|---|---|---|
| Only accounts of your Google Workspace | hd | exactly | example.com |
| Only people whose verified email is on your domain | email | ends with | @example.com |
| Only members of one group | groups | exactly | platform-team |
Keep the @ in an ends with email value, as the row's hint says: alice@evilexample.com ends with example.com,
but not with @example.com.
The email rule admits an address only when the provider says it has verified it. Many
providers let people type or edit their own address without verifying it — Keycloak and Auth0 by
default, and the email claim of a multi-tenant Entra ID app — so without that check an outsider
could set mallory@example.com and pass an "ends with @example.com" rule. A provider that
never sends email_verified therefore cannot be gated on email at all: every sign-in through it
fails the rule. Gate it on a claim it does assert instead. On Google, prefer hd over the email
rule anyway — hd is set only on Workspace accounts, by Google. On a multi-tenant Entra ID
registration, the tenant is the tid claim: require it to be your directory (tenant) ID. A group
works on any provider that sends one. The group rule needs the provider to put groups into the ID
token under that claim name — see the recipes below; Google never does.
The rules are checked once, when someone signs in, before Fadebox creates or links an account.
A person they refuse gets no account, is not linked to an existing one, and lands back on the
sign-in page with a red alert: Your account is not allowed to sign in here. Ask an
administrator. The page does not say which
rule failed; the audit log does, as user.login_refused naming the claim. When
an email rule's address matched but was not verified, the claim it names is email_verified
rather than email, so an unverified address and a wrong one read differently.
:::caution Tightening the rules locks existing users out
A rule applies to accounts that already exist, too: someone who no longer passes is refused at their next sign-in. A session already open runs on until it ends. Local password sign-in and API keys do not go through a provider and are unaffected — an admin who locks themselves out of SSO with a wrong rule still has the password form.
:::
Required claims do not replace the provider's own restriction. Where the provider can limit who may use the application — an Internal Google consent screen or Workspace app access, a single-tenant Entra registration or assignment required, a Keycloak client policy — that stays the first line: it stops the person before they reach Fadebox at all. Required claims are Fadebox's own check on what the provider asserts, for where no such switch exists or where it belongs to another team.
Provider recipes
Any standards-compliant provider works with the generic registration above. The two below have enough sharp edges to spell out.
Google
Google is a plain OIDC provider from Fadebox's point of view, with three quirks: no groups in its
tokens, no preferred_username, and no RP-Initiated Logout.
In the Google Cloud console:
- Create or pick a project, then configure the OAuth consent screen. The audience choice is load-bearing: Internal restricts sign-in to your Google Workspace organization; External lets any Google account in the world complete the flow.
- APIs & Services → Credentials → Create credentials → OAuth client ID, application type Web application.
- Add the authorized redirect URI:
https://<your-fadebox-host>/oidc/callback/<slug>.
In Fadebox:
| Field | Value |
|---|---|
| Issuer | https://accounts.google.com |
| Client ID / secret | from the OAuth client you created |
| Scopes | leave empty — the profile,email default is right |
| Groups claim | leave the default; it stays inert (see below) |
What to expect:
- Usernames are email addresses. Google's tokens carry no
preferred_username, so account resolution falls back to the email claim. - Group mapping stays inert. Google does not put group memberships into ID tokens, so grant permissions through Fadebox groups and project roles directly.
- Sign-out is local. Google advertises no
end_session_endpoint; Fadebox clears its own session and the Google session survives — clicking the button again signs you straight back in without a password prompt. That is Google's design, not a misconfiguration. - Link by verified email is comparatively safe to enable here: Google sets
email_verifiedreliably. The general caution still applies.
:::caution External audience + auto-provisioning = open registration
Auto-provisioning is on by default. Combined with an External consent screen, every Google
account that completes the flow gets a Fadebox account with the global role user. Use an
Internal (Workspace) audience, add a required claim of hd set to your
Workspace domain, or turn auto-provisioning off and create accounts yourself.
:::
Microsoft Entra ID
In the Entra admin center (Identity → Applications → App registrations → New registration):
- Pick the single-tenant account type unless you know you need otherwise.
- Add a Web platform redirect URI:
https://<your-fadebox-host>/oidc/callback/<slug>, and registerhttps://<your-fadebox-host>/sign-inas the post-logout redirect URI so Entra accepts the RP-initiated sign-out. - Certificates & secrets → New client secret. Note the expiry — Entra secrets are time-boxed, and Fadebox has no way to warn you. When you rotate it, paste the new value into the provider's Client secret field (it is write-only; leaving it blank keeps the old one).
In Fadebox:
| Field | Value |
|---|---|
| Issuer | https://login.microsoftonline.com/<directory-tenant-id>/v2.0 |
| Client ID / secret | application (client) ID and the secret's value |
| Scopes | leave empty — the profile,email default is right |
| Groups claim | groups (the default), if you configure the claim below |
The issuer needs care:
- Use the Directory (tenant) ID — the GUID from the app registration's overview — not a
domain name and not
commonororganizations. Fadebox resolves a token back to its provider by the verifiedissclaim, and Entra always issuesissin the GUID form; any other spelling of the issuer means the token matches no provider. - Keep the
/v2.0suffix: it selects the endpoint version whose tokens carrypreferred_username(the UPN — that is what usernames will look like) andemail.
Group mapping works, with two Entra-specific catches. Add the groups claim under the app registration's Token configuration → Add groups claim; the values it emits are group object IDs (GUIDs), not display names, so Fadebox claim mappings must map those GUIDs. And for users in more than 200 groups Entra omits the claim entirely (a Graph link takes its place, which Fadebox does not follow) — in large directories, emit only groups assigned to the application instead of all of them.
Entra advertises an end_session_endpoint, so sign-out is fully RP-initiated: it ends the Entra
session too. If that is not what you want — people expect to stay signed in to Microsoft — turn
End the provider session on sign-out off on the provider. Fadebox then clears only its own
session, and the sign-in button signs straight back in without a prompt.
Account linking
Identity and permissions are anchored in the local user table. The provider proves who you are; what you may do always comes from your Fadebox account and its groups — never from the token.
A sign-in resolves to an account in this order:
- By provider + subject, from a previous sign-in through the same provider. Never by the bare
subclaim:subis unique only within its own issuer, so with several providers configured, matching on it alone would let one provider sign its users in as another's. - By verified email, only if Link by verified email is on for that provider and the token
carries
email_verifiedset totrue(the JSON boolean or the string"true"). It links only when exactly one active account holds that address — never a service account — and that account gains a permanent identity link. An address two active accounts share links neither: the sign-in goes on to step 3. An address that only deactivated accounts hold refuses the sign-in, as signing in to a deactivated account would. - Auto-provision, if it is on for that provider — a new password-less account with the global
role
user. The username comes frompreferred_username, falling back to the email address and then tooidc-<subject>. A name that is already some account's username, or some account's email address, gets a-2,-3, … suffix, so a new account never takes over the address another one signs in with.
If none of the three applies — an unknown user at a provider with auto-provisioning off — the sign-in is rejected, which is what a closed deployment wants.
:::caution Link by verified email is off by default
The provider decides its own email_verified value. If you federate an identity provider you do
not operate — a contractor or partner directory — its administrator could claim any Fadebox account
by setting an address and marking it verified. Turn this on only for a provider you control.
:::
Inactive accounts are rejected. Password-less (federated-only) accounts cannot use form login.
Mapping groups to permissions
A provider's group claim can place users into Fadebox groups, which is how an SSO login arrives
with project permissions already granted. Claim mappings name which provider asserted the
value, so engineering from a partner directory cannot enter the group that engineering from
your corporate directory enters. See Access control.
Running several Fadebox instances
Nothing notifies one running instance that another wrote a provider row, so each re-reads the
catalog periodically. FADEBOX_SECURITY_OIDC_REGISTRY_REFRESH_INTERVAL (default 60s) bounds how
stale an edit made elsewhere may be; a write on the instance you are using applies immediately.
Troubleshooting
The button does not appear. The provider is disabled, or the sign-in page was loaded before it
was created — the list comes from /api/public/auth-config, which returns only the slug and
display name of enabled providers.
Sign-in fails with an issuer error. Fadebox fetches the discovery document from the issuer URL
at first use. Check that <issuer>/.well-known/openid-configuration is reachable from the
Fadebox container and served over https.
Users arrive with the username oidc-<subject>. The token carried neither
preferred_username nor email — add the profile and email scopes to the client.
"Your account is not allowed to sign in here. Ask an administrator." The provider's required
claims refused the token. The audit log's user.login_refused event names the
provider and the claim of the first rule that failed; check its spelling against a real token,
and remember a claim the token does not carry fails its rule.
Sign-in is rejected for an unknown user. Auto-provisioning is off for that provider and the account does not exist yet. Create it first, or turn auto-provisioning on.
Everyone lands as role user. That is the design: auto-provisioned accounts get the lowest
global role. Grant more through groups and project roles.
Signing out and straight back in skips the password prompt. The provider does not support
RP-Initiated Logout — its discovery document advertises no end_session_endpoint, with
Google the common case — so sign-out clears only the Fadebox session, and the surviving
provider session does what single sign-on says. End the session at the provider itself if you
need it gone.