Set up federated login (your IdP)
ProbeRun’s dashboard signs your people in through your own OpenID Connect identity provider. The dashboard sends the API a short-lived, signed identity assertion after your IdP has authenticated the person, and the API re-reads that person’s membership and role from ProbeRun’s own database on every request.
How sign-in is verified
The API does not take the dashboard’s word for who signed in. The assertion
carries the ID token your IdP issued at sign-in (and its access token, when
there was one), and the API checks that token’s signature against the public
keys your IdP publishes through OIDC discovery, that it was issued by your
configured issuer for your dashboard’s client, and that it is for the same
person the assertion names. Who you are, your email and whether it is
verified, your groups and your organization are all read from that IdP-signed
token, never from anything the dashboard wrote on its own. A token that fails
any check is refused (invalid_identity, and the dashboard sends you back to
sign in); when the IdP’s keys cannot be fetched the request fails as
temporarily unavailable rather than letting anyone through. The API needs
AUTH_OIDC_ISSUER and AUTH_OIDC_CLIENT_ID and outbound HTTPS to your issuer
for this; it never calls your IdP about a person.
What you configure on the dashboard
Set these on the dashboard process (an operator does this once per deployment):
| Setting | Meaning |
|---|---|
AUTH=oidc |
Turns identity on. Until it is set, the dashboard runs with no sign-in. |
AUTH_OIDC_ISSUER |
Your IdP’s discovery root. Okta: https://<org>.okta.com/oauth2/default. Auth0: https://<tenant>.us.auth0.com. Entra ID: https://login.microsoftonline.com/<tenant-id>/v2.0. WorkOS: the AuthKit domain https://<env>.authkit.app, with a Connect OAuth app’s credentials. |
AUTH_OIDC_CLIENT_ID / AUTH_OIDC_CLIENT_SECRET |
The OIDC client. The secret never reaches the browser or the database. |
AUTH_OIDC_GROUP_CLAIM |
Which claim carries group membership (default groups). It must match the tenant’s group claim under Team → Settings or group mapping silently never matches. |
AUTH_OIDC_ORG_CLAIM |
Which claim carries the person’s organization (default org_id). The dashboard forwards it to the API as org_id. |
AUTH_OIDC_DEDICATED_ISSUER |
Set on the API. true only when the issuer serves your one organization (your own Entra tenant, Okta org or Keycloak realm). Default false: the issuer is treated as shared by many organizations. See below. |
AUTH_SECRET, AUTH_IDENTITY_SECRET |
Cookie-signing secret and the shared assertion key. Required under AUTH=oidc. |
WorkOS AuthKit additionally needs AUTH_OIDC_TOKEN_AUTH_METHOD=client_secret_post
and AUTH_OIDC_AUTHORIZE_PARAMS=provider=authkit.
Verified emails: what your users will hit
Under AUTH=oidc an invitation or a bootstrap admin grant is only claimable
when your IdP positively asserts email_verified: true for the person. A
missing claim counts as unverified. An explicit false is always refused.
If your IdP genuinely cannot assert the claim, an Admin can switch on
Trust unverified email for your tenant (Team → Settings,
trust_unverified_email). This restores the older tolerance for your tenant
only: a missing claim is accepted, an explicit false is still refused. Treat
it as a documented risk, not a default: it means an attacker who can register
an unverified address at your IdP that matches an outstanding invitation could
claim it.
Email addresses are compared after Unicode NFKC normalisation and lowercasing,
so Admin@Example.com and admin@example.com are the same invitee.
Invitations are bound to the IdP they were created under
An invitation created while your tenant had an IdP connection with an issuer URL is claimable only by a sign-in from that issuer. If you change IdPs, re-issue outstanding invitations.
Once your tenant’s IdP connection names an issuer, every sign-in into your
tenant must come from that issuer: a sign-in whose identity carries no issuer,
or a different one, is refused for your tenant and recorded in your audit log
as issuer_mismatch.
Group → role mapping and just-in-time provisioning
Under Team → Settings an Admin can:
- set the default role new members get (
viewer,developer,admin); - switch JIT provisioning on, so people from your organization whom your IdP authenticates become members with the default role on first sign-in, with no invitation;
- list admission groups (
admit_groupson the IdP connection), so a person in one of those groups from your organization is admitted, and map IdP groups to roles, so a member in a mapped group gets that role.
Roles are nested: viewer < developer < admin.
Shared sign-in services: your tenant is bound to your organization
Some sign-in services, such as WorkOS AuthKit and similar hosted services, sign in people from many organizations through one issuer. There, “anyone the IdP authenticates” would mean anyone with an account at that service, so ProbeRun only lets JIT provisioning, admission groups and group-to-role mapping act for people whose sign-in carries the organization your tenant is bound to:
- The binding is set by the ProbeRun operator, not in Team → Settings
(
cd apps/api && python -m proberun_server.cli tenants bind-org <slug> <org_id>on the server; the pip-installedprobeCLI has notenantscommand). A settings request that names an organization is refused. - Until your tenant is bound, turning JIT provisioning on or saving admission
groups is refused with
org_binding_required; ask the operator to bind your organization. Turning them off, or clearing the groups, always works. - A person from another organization is not admitted by JIT or by a group, even when their own directory has a group with the same name as yours, and never learns your tenant’s name from the tenant chooser.
- Once your tenant is bound, everyone who enters it must sign in from
your organization: existing members, people you invited, and the first
Admin’s bootstrap grant alike. A sign-in carrying another organization (or
none) is refused for your tenant and recorded in your audit log as
org_mismatch, and a foreign group never changes anyone’s role. So removing a person from your organization at the sign-in service ends their access here at their next sign-in. - An organization is bound to at most one tenant.
Until your tenant is bound, existing members and invitations keep working whatever organization a sign-in carries (there is nothing to compare them with); only JIT and admission groups wait for the binding.
If your deployment’s issuer serves only your organization (your own Entra ID
tenant, Okta org or Keycloak realm), the operator sets
AUTH_OIDC_DEDICATED_ISSUER=true on the API and no binding is needed.
Which claim carries the organization depends on the sign-in service; ProbeRun
reads AUTH_OIDC_ORG_CLAIM (default org_id) from the ID token, or, when it
is not there, from the access token of the same sign-in — only if that token
was issued by your configured issuer to your dashboard’s client. It is never
read from profile metadata a user can edit. WorkOS AuthKit, for example,
documents the organization as an access-token claim. With another IdP, check
which of its tokens carries your organization claim.
Session lifetime and offboarding
A dashboard session lasts AUTH_SESSION_MAX_AGE_HOURS (default 8). Changes
made in ProbeRun (a role change, a removal) take effect on the person’s next
request. Disabling someone at the IdP alone does not end an already-open
session before the cookie expires, so make removal in ProbeRun part of your
offboarding runbook, or lower the session lifetime.
Signing out ends the session at the API, not only in the browser: every session
of that person that started before the sign-out is refused from then on
(session_revoked), in every tenant they are a member of — also when they had
not chosen a tenant yet — and such a session cannot join a new tenant either
(by invitation, JIT or group). The trade-off is
that signing out on one device signs you out on all of them; signing in again
works straight away. If the dashboard cannot reach the API while you sign out,
you are still signed out locally and the failure is logged on the server, but
the old session is not revoked at the API until it expires.