Identity providers — provider-side setup
Included from Basic tieridentity:builtinidentity:sso
The identity providers guide (opschain learn 7.5)
explains the connection record, provisioning controls and group→role
mapping. This chapter is the other half: what to click in the
provider's own console before a connection can be registered — one
section each for GitHub, Google, Microsoft Entra ID and an LDAP
directory, each in the same shape (what you need, the provider side,
the OpsChain side, verification, gotchas), and a troubleshooting table
at the end.
Before you start — the one value every console asks for
Every browser-redirect provider (GitHub, Google, Entra ID) needs exactly one redirect URI registered on its side:
https://<your server>/auth/callback/<code>
<your server>is the host and port users reach OpsChain on — the address in the browser, scheme included. The server derives the redirect URI from the request it actually received, and providers compare it character for character against what you registered:https://ops.example.comandhttps://ops.example.com:8443are different values, and so are a hostname and its IP address. If users reach the server two ways (a load balancer and a direct address, say), register both — consoles accept several entries.<code>is the connection code you will choose at registration time (github,google,entra, …): 1–64 lowercase letters, digits,-or_, immutable afterwards. Decide it before you open the console.
Two things hold for every provider below:
- The client secret never goes on a command line. Put it in an
environment variable and pass
--credential-env, or let--credential-promptask without echo. It is encrypted at rest and write-only: no read ever returns it, andshowonly reports whether one is stored. - Registration is an administrator verb: run the commands below with
an admin bearer (
opschain admin login, orOPSCHAIN_ADMIN_TOKEN). Cookie sessions must be enabled on the server (token.enable_cookies, the default) — the browser sign-in lands its session in a cookie.
Which providers your licence covers: GitHub on github.com, Google
(https://accounts.google.com) and Microsoft Entra ID on your own
tenant's issuer (registered with --tenant-id) are the built-in
providers (identity:builtin, in every licence). Any other OIDC issuer —
your own Google Workspace tenant, Okta, Keycloak — and every group→role
mapping write need identity:sso; a GitHub Enterprise Server host needs
the Enterprise tier. opschain admin licence features shows what this
server holds.
1. GitHub
Sign-in through GitHub uses a GitHub App (not an OAuth App). Only the App's client id and client secret are involved — no private key, no installation is needed for sign-in itself. The App is registered once per GitHub instance: github.com, or a GitHub Enterprise Server host.
What you need
- An account that can create GitHub Apps where you want the App to live — your personal account for a trial, an organisation for anything shared (organisation owners create Apps under the organisation's settings).
- The redirect URI:
https://<your server>/auth/callback/github.
On the GitHub side
- For an organisation-owned App: your organisation → Settings → Developer settings → GitHub Apps → New GitHub App. For a personal App: your profile → Settings → Developer settings → GitHub Apps → New GitHub App.
- Fill in the form:
- GitHub App name: unique across GitHub (
OpsChain sign-in — Example Corp, say). This is the name users see on the consent screen. - Homepage URL: your server's address,
https://<your server>. - Callback URL:
https://<your server>/auth/callback/github. Add a second entry if users reach the server by another address. - Expire user authorization tokens: leave enabled.
- Request user authorization (OAuth) during installation: leave unticked — sign-in does not go through installation.
- Webhook → Active: untick. (An active webhook with no URL blocks creation, and sign-in needs none.)
- GitHub App name: unique across GitHub (
- Permissions — the minimum, add nothing else:
- Account permissions → Email addresses: Read-only (resolves the user's verified primary email).
- Organization permissions → Members: Read-only (organisation and team membership, for group→role mapping).
- Repository permissions: none.
- Where can this GitHub App be installed? — Only on this account for a trial, Any account if more than one organisation will drive roles through it.
- Create GitHub App. On the App's page, record the Client ID, then Generate a new client secret and copy it immediately — it is shown once. The App ID and the private key are not needed for sign-in.
- If organisation or team membership should drive roles, install
the App on each such organisation (Install App in the left
column of the App's page → the organisation → Only select
repositories with none selected is fine). GitHub only reveals an
organisation's membership to an App the organisation has installed
or authorised; an organisation that has not reads as no membership
at all — the user signs in, but with no
<org>or<org>/<team>groups.
GitHub Enterprise Server: repeat the same steps on the GHES host's
own Developer settings, and pass the host at registration
(--host github.example.com). Registering any host other than
github.com requires the Enterprise licence tier; organisations on
GitHub Enterprise Cloud live on github.com and need no special tier.
On the OpsChain side
export OPSCHAIN_IDP_SECRET='<the client secret you copied>'
opschain admin identity providers create github --kind github \
--display-name GitHub --client-id <client id> \
--credential-env OPSCHAIN_IDP_SECRET
unset OPSCHAIN_IDP_SECRET
opschain admin identity providers test github
The test verb reads the instance's public metadata — it proves the host
is reachable, not the credentials; the first sign-in proves those. A
GitHub connection's step-up mode is always platform (GitHub offers no
"re-authenticate now" signal), so users who meet a step-up floor verify
a platform-registered factor — see opschain learn 7.4.
Map membership to authority. Provider groups arrive lowercased as the
organisation login and <org>/<team>:
opschain admin identity mappings create --provider github \
--provider-group example-corp/platform-admins --role admin
opschain admin identity mappings create --provider github \
--provider-group example-corp --group-name staff
Everyone else who signs in lands in the connection's default group, if
you set one (--default-group), or in no group at all.
Verify
Open https://<your server>/auth/login/github in a browser — or click
the GitHub button on the sign-in page. GitHub shows the App's consent
screen, then returns you to the GUI signed in. In the audit feed
(opschain events --since 10m) expect api:auth:login with
authn_method github:github, and api:auth:mapped_grants_changed
when a mapping granted something. opschain admin users list shows the
new user with provider github:github; its username is the GitHub
login, its identity the GitHub numeric user id (logins can be renamed;
the id cannot).
Gotchas
- "The redirect_uri is not associated with this application" on the GitHub side: the callback URL you registered differs from the one the browser presented — scheme, host or port. Add the exact value.
- Users sign in but have no groups: the organisation has not installed the App (step 6), or the Members permission is missing. Re-installing or granting a permission takes effect at the user's next sign-in — group snapshots refresh at sign-in only.
- Unverified email: an account whose GitHub email is unverified
signs in, but the company-email policy (
--deny-public-email-domains) refuses it — it fails closed where it cannot establish a verified address.
2. Google
Google is a full OIDC provider: issuer https://accounts.google.com,
with discovery, so the connection needs only the issuer, a client id and
a client secret.
What you need
- A Google account with access to the Google Cloud console; a Google Workspace organisation makes the app Internal (no verification review, no tester list).
- A Google Cloud project to hold the OAuth client (any project; the credentials are the only thing OpsChain uses from it).
- The redirect URI:
https://<your server>/auth/callback/google.
On the Google side
- Open
https://console.cloud.google.com, pick or create the project. - Open Google Auth Platform (left navigation, or search for it).
On first use it says the project is not configured yet → Get
started, a short wizard:
- App Information: the app name users see on the consent screen
(
OpsChain), and a support email. - Audience: Internal if your account belongs to a Google Workspace organisation — only that organisation's users can sign in, which is what you want. External otherwise: the app starts in Testing mode, and only addresses listed under Audience → Test users can sign in until you publish it.
- Contact Information: your address → agree → Create.
- App Information: the app name users see on the consent screen
(
- Data Access (scopes): nothing to add. OpsChain requests
openid,emailandprofile, which are non-sensitive and need no entry. - Clients → Create client:
- Application type: Web application. Name it (
OpsChain). - Authorised redirect URIs → Add URI:
https://<your server>/auth/callback/google. (No Authorised JavaScript origins are needed.)
- Application type: Web application. Name it (
- Create. Record the Client ID and copy the Client secret now — the full value is shown only at creation. If you lose it, open the client and add a new secret rather than searching for the old one.
On the OpsChain side
export OPSCHAIN_IDP_SECRET='<the client secret you copied>'
opschain admin identity providers create google --kind oidc \
--issuer https://accounts.google.com --client-id <client id> \
--display-name Google --credential-env OPSCHAIN_IDP_SECRET \
--require-hosted-domain --default-group viewers \
--email-domain-rule example.com=staff
unset OPSCHAIN_IDP_SECRET
opschain admin identity providers test google
The test verb fetches the issuer's discovery document and signing keys.
Google emits no groups claim, so there is nothing to map: authority
comes from the two provisioning controls above — --require-hosted-domain
admits Workspace accounts only (the verified ID token must carry a
hosted-domain claim; consumer Gmail is refused with a use-your-work-
identity message), and --email-domain-rule example.com=staff places
your own domain's users in a staff group ahead of the --default-group
everyone else lands in. The step-up mode may be idp (users re-prove
themselves at Google when a sensitive verb demands it) or platform (a
platform-registered factor).
Verify
https://<your server>/auth/login/google → Google's account chooser and
consent → back in the GUI, signed in. Expect api:auth:login with
authn_method oidc:google; opschain admin users list shows the user
with provider oidc:google and the Google account email as username.
Gotchas
- "Error 400: redirect_uri_mismatch": the registered URI differs from the one presented. Google shows the presented value on the error page — add it verbatim under Authorised redirect URIs. Edits can take a few minutes to apply.
- "Access blocked: … has not completed the Google verification process": the app is External and in Testing, and the account is not a listed test user. Add it, or make the app Internal.
- Every user lands in the default group: expected — Google sends no groups. Use domain rules, or map Workspace groups through a different provider.
3. Microsoft Entra ID
Entra ID (formerly Azure AD) is an OIDC provider with discovery; it is
registered as an app registration in your tenant. The built-in
provider takes your directory (tenant) id — --tenant-id derives
the issuer https://login.microsoftonline.com/<tenant id>/v2.0, so only
your tenant's accounts can sign in, and that tenant form is what makes
the connection a built-in one (identity:builtin). The multi-tenant
common endpoint is not supported: its discovery document names the
literal {tenantid} template as its issuer, so no sign-in through it can
pass the issuer check (and it would admit every Microsoft tenant).
What you need
- An account that can create app registrations in the tenant (Application Developer, Cloud Application Administrator or Global Administrator); granting admin consent (step 6) needs one of the latter two.
- Your Directory (tenant) ID — on the registration's Overview page.
- The redirect URI:
https://<your server>/auth/callback/entra.
On the Entra side
- Open the Microsoft Entra admin center (
https://entra.microsoft.com) → Identity → Applications → App registrations → New registration. (The same blade exists in the Azure portal under Microsoft Entra ID.) - Register an application:
- Name: what users see on the consent prompt (
OpsChain). - Supported account types: Accounts in this organizational directory only (Single tenant) for your own tenant. Choose a multi-tenant option only if accounts from other tenants must sign in.
- Redirect URI: platform Web, value
https://<your server>/auth/callback/entra. - Register.
- Name: what users see on the consent prompt (
- On Overview, record the Application (client) ID and the Directory (tenant) ID.
- Authentication (left menu): under Web → Redirect URIs add any further address users reach the server by. Under Implicit grant and hybrid flows leave both boxes clear — OpsChain uses the authorization-code flow, which does not need them. Allow public client flows stays No.
- Certificates & secrets → Client secrets → New client secret: a description and an expiry (Entra's maximum is 24 months; pick what your rotation policy allows and put the date in your calendar — sign-in stops the day it expires). Add, then copy the secret's Value column immediately: it is shown only while you stay on the page. The Secret ID column is not the secret.
- API permissions: Microsoft Graph → Delegated permissions →
tick openid, email and profile (
User.Readis usually added by default and can stay). If your tenant requires it — the default User consent settings in many tenants do — press Grant admin consent forso users are not stopped by a consent prompt they cannot approve. - Token configuration → Add groups claim, only if directory groups
should drive roles:
- Choose which groups to emit. Groups assigned to the application keeps the claim small and lets you choose which groups matter (assign them under Enterprise applications → your app → Users and groups); Security groups emits every security group the user is in.
- Under Customize token properties by type → ID, the default is
Group ID: the claim carries each group's object ID (a
GUID), and that GUID is what a mapping must reference. With Groups
assigned to the application you may instead pick Cloud-only
group display names, and the mapping then references the
display name. Either way, the claim is named
groups. - Entra omits the claim entirely when a user belongs to more than 200 groups (the ID-token limit) and emits a reference instead; the assigned option is the way to stay under it.
On the OpsChain side
export OPSCHAIN_IDP_SECRET='<the client secret Value you copied>'
opschain admin identity providers create entra --kind oidc \
--tenant-id <directory (tenant) id> \
--client-id <application (client) id> --display-name "Microsoft" \
--username-claim preferred_username --groups-claim groups \
--credential-env OPSCHAIN_IDP_SECRET
unset OPSCHAIN_IDP_SECRET
opschain admin identity providers test entra
--tenant-id is the Directory (tenant) ID from step 3 — the GUID, not
the domain name — and stands in for --issuer.
preferred_username is the user's sign-in name (alice@example.com);
groups is the claim step 7 configured. Both are the defaults, written
out here so the intent is on record. Map groups by whichever value you
chose in step 7:
# Group ID (the default): the group's object ID from Entra
opschain admin identity mappings create --provider entra \
--provider-group 3f2a9c1e-7d4b-4e0a-9b1c-0c5e6f7a8b9d --role admin
# or, with "Cloud-only group display names" selected:
opschain admin identity mappings create --provider entra \
--provider-group platform-admins --group-name deployers
When the secret approaches expiry, create the next one in Certificates
& secrets and rotate with
opschain admin identity providers update entra --credential-env … —
the old secret can then be deleted on the Entra side.
Verify
https://<your server>/auth/login/entra → Microsoft sign-in (and, the
first time, the permissions prompt unless admin consent was granted) →
back in the GUI. Expect api:auth:login with authn_method
oidc:entra; opschain admin identity providers show entra reports
the secret as set; opschain admin users list shows the user with the
preferred_username value as username. If you mapped groups, the
first sign-in of a group member records api:auth:mapped_grants_changed
with the granted role or group.
Gotchas
- AADSTS50011 — "The redirect URI … does not match the redirect URIs configured": add the exact presented value under Authentication → Web. Platform must be Web, not Single-page application.
- AADSTS7000215 — "Invalid client secret provided": you pasted the Secret ID instead of the Value, or the secret has expired. Create a new one and rotate it with the update verb.
- AADSTS65001 — "The user or administrator has not consented": grant admin consent (step 6).
testfails with an issuer mismatch: the issuer must be the exacthttps://login.microsoftonline.com/<tenant id>/v2.0form — the/v2.0suffix matters, and the tenant id is the Directory (tenant) ID, not the domain name. Register with--tenant-idand the issuer is derived for you. A connection registered on the multi-tenant common endpoint always fails this way (its discovery document names the{tenantid}template as its issuer): re-register it withupdate entra --tenant-id <directory (tenant) id>.- No groups arrive: the groups claim was not added (step 7), the user is not in an assigned group, or the user exceeds the 200-group limit. Mappings keyed on a display name while the token carries GUIDs (or the reverse) never match — check which option step 7 selected.
4. LDAP directories
A directory is the one provider kind with no redirect leg: users
enter their directory username and password on OpsChain's own sign-in
page (or opschain login --provider corp --username alice), and the
server binds as them. Nothing is registered at the directory beyond a
read-only service account.
What you need
- A service account that can bind and search — OpsChain never writes to the directory. Record its bind DN and password.
- The directory URL.
ldaps://(or STARTTLS behind a TLS-terminating front) is the expectation: a plaintextldap://URL is refused unless you pass--require-tls=false, a lab-only opt-out. - The user and group search bases, and — for Active Directory — the
attribute logins match against (
sAMAccountName).
On the OpsChain side
export OPSCHAIN_BIND_PW='<service account password>'
opschain admin identity providers create corp --kind ldap \
--display-name "Corporate directory" \
--directory-url ldaps://ldap.example.com:636 \
--bind-dn 'cn=opschain,ou=services,dc=example,dc=com' \
--user-base 'ou=users,dc=example,dc=com' \
--group-base 'ou=groups,dc=example,dc=com' \
--credential-env OPSCHAIN_BIND_PW
unset OPSCHAIN_BIND_PW
opschain admin identity providers test corp
The test verb binds as the service account, runs a one-entry user search
and reports the detected entry-id attribute (entryUUID, or
objectGUID on Active Directory) — the value identities key on, never
the username. Active Directory typically also wants
--user-attribute sAMAccountName and member-of-shaped filters; every
field and default is in opschain docs cli/admin-identity-providers-create.
Directory groups arrive by name and map exactly like the other kinds
(--provider corp). Run opschain admin identity providers sweep corp
to import directory users ahead of their first sign-in and disable
leavers; the full model — reconciliation, the sweep, health under
identity.ldap_providers.<code> — is in opschain docs identity.
Troubleshooting
Provider-side errors appear in the browser before OpsChain is
involved, so nothing is logged for them; everything from the callback
onwards is audited. opschain events --since 1h --filter 'type=*auth*'
lists the relevant events.
| Symptom | Cause | What the audit shows | Fix |
|---|---|---|---|
| The provider's own error page names the redirect URI | The registered redirect URI differs from the one the browser presented (scheme, host, port or path) | Nothing — the browser never reached the callback | Register the exact value: https://<your server>/auth/callback/<code> |
test reports a failed probe for an OIDC connection | Issuer URL wrong or unreachable: the discovery document's issuer must equal the configured value character for character | api:identity_providers:test with the failure reason; warn:auth:provider_unavailable at sign-in | Correct the issuer (update --issuer), check egress from the server to the provider |
| Sign-in bounces back to the login page | The code exchange or ID-token validation failed — expired or mis-pasted client secret, wrong client id, clock skew | warn:auth:login_failed with a reason, provider named; a token-endpoint failure lands on warn:auth:provider_unavailable | Rotate the secret with update --credential-env …; verify the client id; check the server clock |
| User signs in with no groups, mappings do nothing | Provider emitted no groups: Google never does; GitHub needs the App installed on the organisation; Entra needs the groups claim configured; LDAP needs --group-base | api:auth:login with an empty group snapshot; no api:auth:mapped_grants_changed | Fix the provider side, then have the user sign in again — snapshots refresh at sign-in only |
| Mappings exist but never match | The mapping's provider group is spelled differently from the snapshot: GitHub groups are lowercase org / org/team; Entra carries object IDs unless display names were selected | api:auth:login carries the snapshot; compare it with opschain admin identity mappings list --provider <code> | Re-create the mapping with the value as the snapshot carries it |
| A sensitive verb is refused after sign-in | Step-up floor: the session is not elevated. GitHub and LDAP sessions can only elevate with a platform factor; an idp-mode OIDC session elevates by re-signing-in at the provider | warn:auth:step_up_refused with the verb and not_elevated / elevation_expired; api:auth:step_up on success | Enrol a platform factor, or switch the connection's --step-up mode; see opschain learn 7.4 |
| Sign-in worked for months, then stops for everyone | The client secret expired (Entra secrets always carry an expiry; GitHub and Google secrets do not, but can be revoked in the console) | warn:auth:login_failed / warn:auth:provider_unavailable for every attempt; test may still pass, since discovery does not use the secret | Create a new secret in the console, rotate with update --credential-env … |
| Sign-in refused with "use your work identity" | The connection's company-email policy: no hosted-domain claim, or a public email domain at first provisioning | warn:auth:login_failed with the policy named as the reason (never the address) | Expected for consumer accounts; relax --require-hosted-domain / --deny-public-email-domains only if that is the intent |
| Local login or break-glass must keep working during a provider outage | They do — a down provider is skipped, never a credential failure | warn:auth:provider_unavailable, throttled to one per ten minutes | None needed; fix the provider at leisure |
Where next
opschain learn 7.5— the connection record, provisioning controls and the directory sweep.opschain docs identity— the full federated sign-in model: claims, group snapshots, step-up, events.opschain docs cli/admin-identity-providers-createandopschain docs cli/admin-identity-mappings— every flag.opschain learn 7.1— users, groups and path-based access control, where the mapped groups take effect.