Skip to main content
Version: v5.7.0

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.com and https://ops.example.com:8443 are 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-prompt ask without echo. It is encrypted at rest and write-only: no read ever returns it, and show only reports whether one is stored.
  • Registration is an administrator verb: run the commands below with an admin bearer (opschain admin login, or OPSCHAIN_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​

  1. 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.
  2. 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.)
  3. 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.
  4. 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.
  5. 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.
  6. 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​

  1. Open https://console.cloud.google.com, pick or create the project.
  2. 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.
  3. Data Access (scopes): nothing to add. OpsChain requests openid, email and profile, which are non-sensitive and need no entry.
  4. 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.)
  5. 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​

  1. 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.)
  2. 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.
  3. On Overview, record the Application (client) ID and the Directory (tenant) ID.
  4. 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.
  5. 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.
  6. API permissions: Microsoft Graph → Delegated permissions → tick openid, email and profile (User.Read is 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 for so users are not stopped by a consent prompt they cannot approve.
  7. 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).
  • test fails with an issuer mismatch: the issuer must be the exact https://login.microsoftonline.com/<tenant id>/v2.0 form — the /v2.0 suffix matters, and the tenant id is the Directory (tenant) ID, not the domain name. Register with --tenant-id and 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 with update 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 plaintext ldap:// 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.

SymptomCauseWhat the audit showsFix
The provider's own error page names the redirect URIThe registered redirect URI differs from the one the browser presented (scheme, host, port or path)Nothing — the browser never reached the callbackRegister the exact value: https://<your server>/auth/callback/<code>
test reports a failed probe for an OIDC connectionIssuer URL wrong or unreachable: the discovery document's issuer must equal the configured value character for characterapi:identity_providers:test with the failure reason; warn:auth:provider_unavailable at sign-inCorrect the issuer (update --issuer), check egress from the server to the provider
Sign-in bounces back to the login pageThe code exchange or ID-token validation failed — expired or mis-pasted client secret, wrong client id, clock skewwarn:auth:login_failed with a reason, provider named; a token-endpoint failure lands on warn:auth:provider_unavailableRotate the secret with update --credential-env …; verify the client id; check the server clock
User signs in with no groups, mappings do nothingProvider emitted no groups: Google never does; GitHub needs the App installed on the organisation; Entra needs the groups claim configured; LDAP needs --group-baseapi:auth:login with an empty group snapshot; no api:auth:mapped_grants_changedFix the provider side, then have the user sign in again — snapshots refresh at sign-in only
Mappings exist but never matchThe 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 selectedapi: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-inStep-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 providerwarn:auth:step_up_refused with the verb and not_elevated / elevation_expired; api:auth:step_up on successEnrol a platform factor, or switch the connection's --step-up mode; see opschain learn 7.4
Sign-in worked for months, then stops for everyoneThe 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 secretCreate 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 provisioningwarn: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 outageThey do — a down provider is skipped, never a credential failurewarn:auth:provider_unavailable, throttled to one per ten minutesNone 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-create and opschain docs cli/admin-identity-mappings — every flag.
  • opschain learn 7.1 — users, groups and path-based access control, where the mapped groups take effect.