Identity providers
for operators
psd’s single sign-on is generic OpenID Connect (Authorization Code + PKCE; see the configuration reference for every field). “Support for provider X” is therefore not a code path — it is knowing the shape of X’s documents, the failures X makes likely, and the two or three settings that differ. This page is that knowledge, Okta first because it is the one most orgs bring.
What every provider has in common:
- psd is registered as a web application with the redirect URI
{issuer}/login/oidc/callback(psd prints the exact value at startup). - There is no audience setting. psd validates the ID token’s
audagainstclient_id; that is the whole contract. (If you also run apd, note that its admin SSO has anaudiencethat means an API audience, not a client id — the two are not interchangeable.) required_claimsis mandatory and is the whole authorization gate. A claim that is absent from the ID token and a claim that is present but does not match are reported differently, because they send you to different screens:ID token has no 'groups' claim; the identity provider is not sending itversus'groups' does not include a permitted value.- Persons are keyed on the provider’s
(iss, sub), never email.subis usually opaque (00u1a2b3…); psd storesemailfor display and puts it in the sign-in audit line. - Offboarding at the provider stops sign-ins, not agents:
psd person deactivate.
If you also run apd. apd’s admin SSO
validates an access token an operator’s tool presents to its API; psd
validates an ID token it fetched itself from the provider’s token
endpoint, with client authentication and PKCE, in the request that spent
the code. Three checks differ because of that one fact and not because
either side is looser: psd requires azp to name its client when aud is
plural (for an ID token aud is the client, so the check means something;
on an access token aud is the API and azp the client, so apd relies on
aud alone); psd records a cnf claim rather than refusing it (nothing
presents the ID token as a bearer, so there is no sender constraint to
downgrade; apd refuses cnf because its bearer token could be a DPoP token
being downgraded); and group claims come from the ID token here and from
the access token there. The operator-visible surface — field names, the
two gate messages, this page’s structure — is the same on both.
Okta
Issuer. Okta has two kinds of authorization server and both work for
psd, because psd consumes an ID token whose aud is the client id — it
does not need a custom audience:
- the org authorization server,
https://acme.okta.com(or your custom domain); - a custom authorization server,
https://acme.okta.com/oauth2/default(needs API Access Management;defaultexists on developer orgs).
Either value goes in oidc.issuer exactly as Okta shows it under Security
→ API → Authorization Servers — no trailing slash. Discovery is the issuer
plus /.well-known/openid-configuration in both cases, and every endpoint
comes from that document.
Application. Applications → Create App Integration → OIDC → Web
Application, sign-in redirect URI https://ps.example.com/login/oidc/callback,
grant type Authorization Code. Copy the client id and secret; put the
secret in client_secret_file. Client authentication is
client_secret_basic, Okta’s default.
Groups — the most predictable failure. Okta does not put groups in an
ID token until you say so. With required_claims: {"groups": "psd-users"}
and no groups claim configured, every sign-in is refused with
ID token has no 'groups' claim; the identity provider is not sending it.
The fix is on the application, not on the person: Applications → your app
→ Sign On → OpenID Connect ID Token → Groups claim type: Filter → Groups
claim filter: groups · Matches regex · ^psd- (or Equals psd-users).
Keep the filter narrow: Okta caps the groups claim in an ID token, and a
person in more groups than the cap (about a hundred) gets no groups claim
at all — which denies exactly the most heavily-permissioned accounts,
usually the admin doing the testing. A dedicated psd-users group with a
filter that emits only it keeps the array small by construction.
Everyone. Every Okta user is in the Everyone group.
{"groups": "Everyone"} passes psd’s non-empty check and authorizes the
whole directory — an empty gate wearing a costume. Do not write it.
Custom domains. login.acme.com and acme.okta.com are different
issuers; the token’s iss is whichever the application uses. If psd says
the ID token’s issuer is not the configured provider, this is the usual
cause — a copy-paste, not an attack.
Tenant. Okta has no organisation claim by default; if you want
tenant in tokens, add a custom claim (e.g. org from a profile
attribute) and set tenant_claim to it.
Key rotation is automatic and needs nothing from you: an unknown kid
makes psd refetch the JWKS (at most once a minute).
Microsoft Entra ID
oidc.issuer:https://login.microsoftonline.com/<tenant-id>/v2.0(the v2.0 endpoint; the tenant id, notcommon, soissis exact).- App registration: Web platform, the redirect URI, a client secret. ID tokens are RS256.
- Groups: enable Token configuration → Add groups claim (security
groups, emitted as object ids — so
required_claims: {"groups": "<group-object-id>"}), or prefer app roles assigned to a group and gate onroles. Entra omitsgroupsentirely for users in more than ~150 groups (it emits_claim_names/_claim_sourcesinstead), so a broad group gate fails for the most-permissioned users; app roles do not have this problem. - Tenant:
tenant_claim: "tid".
Google Workspace
oidc.issuer:https://accounts.google.com.- Google publishes its keys on a different host: add
"jwks_cross_origin_hosts": ["www.googleapis.com"]or discovery will refuse the JWKS as cross-origin. - Gate on the hosted domain:
required_claims: {"hd": "acme.com"}— the explicit way to say “everyone in our domain”. Google has no groups claim in ID tokens. - Tenant:
tenant_claim: "hd".
Keycloak
oidc.issuer:https://kc.acme.example/realms/<realm>.- Groups and roles: map a client scope; realm roles arrive as
realm_access.roles, sorequired_claims: {"realm_access.roles": "psd-user"}(dotted paths resolve). - Client authentication: Client authentication: On, credentials tab for the secret.
Auth0
oidc.issuer:https://<tenant>.auth0.com— Auth0 shows its issuer with a trailing slash; psd refuses that form (discovery is the issuer plus a suffix, andissis compared exactly). Drop the slash.- Custom claims are namespaced URLs; a claim path such as
https://acme.example/groupsresolves as a literal key.
First-tenant checklist
Before pointing a real tenant at a production psd:
psd servestarts and printssign-in: passkeys + OpenID Connect (…)with the redirect URI you registered. A wrong issuer, secret path or unreachable provider fails here.- Sign in as yourself. The consent screen still asks you when an agent asks — signing in is not consent.
- Sign in as a colleague who should not have access. A gate nobody has
tested from outside is not known to be a gate. Their refusal page should
read
'groups' does not include a permitted value(or your claim), and the audit lineoidc_login_deniedshould name it. - Read one
signed_inaudit line and checkmethod,idp_iss,idp_subandemailare what you expect. - Deactivate a test person at the provider and run
psd person deactivate— and write both steps into the offboarding runbook. - Keep one passkey for an operator that does not go through the provider.