ProbeRun

Docs

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_groups on 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-installed probe CLI has no tenants command). 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.