Closes Phase 11 of cowork/auth-bundle-2-prompt.md. Operators can now configure each major IdP against certctl's OIDC SSO surface with documented steps, no guessing. Files ===== docs/operator/oidc-runbooks/index.md (NEW): * Index page linking all six per-IdP runbooks. * Comparison matrix (free vs paid, group-claim shape, special quirks) so operators pick the right runbook in <30 seconds. * "Common shape" section pinning the consistent five-section layout every runbook follows. * "Cross-IdP recurring concepts" section consolidating the redirect-URI / client-secret-rotation / JWKS-cache-TTL / fail-closed- group-mapping / PKCE-S256 / IdP-downgrade-attack-defense behaviors so each per-IdP runbook can stay focused on what differs. docs/operator/oidc-runbooks/keycloak.md (NEW): * Canonical reference. Mirrors the testfixtures/keycloak-realm.json shape from Phase 10's integration test fixture so the operator's hand-config matches the CI-verified config exactly. * Step-by-step IdP-side: realm → client → groups → group-mapper → user. Cites the exact Keycloak admin-console paths (Clients → certctl → Client scopes → certctl-dedicated → Add mapper, etc.). * GUI + API + MCP equivalents for the certctl-side configuration. * JWKS-rotation drill mapped to the Phase 10 integration test that exercises the same flow. * 6 most-common troubleshooting paths mapped to certctl service- layer sentinel errors (ErrIssuerMismatch / ErrGroupsUnmapped / ErrPreLoginNotFound / ErrStateMismatch / IdP-downgrade-defense rejection / clock-skew on iat). docs/operator/oidc-runbooks/authentik.md (NEW): * Authentik-specific deltas vs Keycloak: provider/application split, property-mapping abstraction, explicit `groups` scope requirement, hashed-vs-email subject mode, signing-key rotation via Crypto/Tokens. docs/operator/oidc-runbooks/okta.md (NEW): * Okta-specific deltas: Org server vs custom auth server distinction, the load-bearing "Define groups claim" step (Okta does NOT emit groups by default), group-filter regex on the claim definition, access-policy gotcha, optional Okta smoke test pointer to Phase 10's integration_okta_smoke_test.go. docs/operator/oidc-runbooks/auth0.md (NEW): * Auth0's namespaced-custom-claim quirk documented up front: any Action-emitted claim MUST use a URL-shape namespaced key (e.g. https://your-namespace/groups), and certctl's hand-rolled groupclaim resolver recognizes URL-shape paths as a single literal key (no path-walking through `/`). Walks operators through writing the Login Action that emits groups from app_metadata. Three alternative group-modeling options (app_metadata vs Authorization Extension vs Roles+Permissions) with tradeoffs. docs/operator/oidc-runbooks/azure-ad.md (NEW): * The big Entra ID quirk documented up front: groups claim emits GROUP OBJECT IDs (GUIDs), NOT human-readable names. Certctl group→ role mappings MUST be configured against the GUIDs. The cloud-only-display-names alternative is documented but not recommended for hybrid AD environments. Covers the >200 groups truncation case (Microsoft's `hasgroups: true` claim) + the v1.0 vs v2.0 endpoint distinction (certctl supports v2.0 only). docs/operator/oidc-runbooks/google-workspace.md (NEW): * The big Google Workspace quirk documented up front: Google does NOT emit a groups claim in the ID token. Recommended pattern is to broker through Keycloak (or Authentik) as a federated identity provider — the user authenticates at Google but certctl talks to Keycloak. Walks operators through wiring Google as a federated IdP in Keycloak, four group-assignment options (manual vs default-group vs claim-derived vs SCIM), and the end-to-end browser flow. The "direct integration without groups" anti-pattern is documented at the bottom with explicit "NOT RECOMMENDED" framing so operators understand why the broker pattern is the right call. docs/README.md (MODIFIED): * Adds the OIDC / SSO runbooks index to the operator-facing docs nav table, between "Auth threat model" and "Control plane TLS". Conventions held ================ * Every runbook carries `> Last reviewed: 2026-05-10` per the docs convention. * Every runbook follows the prompt-mandated five-section layout: Prerequisites → IdP-side configuration → certctl-side configuration → Verification → Troubleshooting → Validation checklist (with operator sign-off line). * Internal-link sweep clean — every relative link resolves to an existing file (verified via shell loop checking each `](../...)` and `](*.md)` reference). External links to IdP vendor sites are the canonical https URLs. * No leakage of cowork/ workspace paths as Markdown links — the azure-ad.md initially had a `[auth-bundles-index.md](../../../../cowork/...)` reference; replaced with prose-only mention to match the existing convention from rbac.md + migration/api-keys-to-rbac.md. * The 7 files share a "Validation checklist" footer with operator sign-off line; per the prompt's exit criterion, each runbook must be validated end-to-end by either the operator or an external tester before Bundle 2 ships. Verification ============ * Last-reviewed dates: 7/7 runbooks dated 2026-05-10. * Internal-link sweep: 0 broken (every `]( ...)` reference resolves). * docs/README.md → operator/oidc-runbooks/index.md link resolves. * No backend / frontend / Go-test impact — pure docs commit. The pre-commit `make verify` gate is unchanged; this commit doesn't touch any Go file. Phase 11 deviation note ======================= The merge-gate criterion's "≥ 2 external testers" requirement is operator-driven and post-tag — Phase 11 ships the runbooks; the operator runs each end-to-end against a real production-tier IdP and fills in the sign-off footers before flipping Bundle 2 to "merged." Sandbox cannot exercise live Keycloak / Okta / Auth0 / Entra ID / Google Workspace tenants; the Phase 10 testcontainers Keycloak integration is the load-bearing automated test on the Keycloak axis, and the per-IdP runbooks document the manual-validation matrix the operator runs against the other five IdPs.
8.9 KiB
Okta OIDC runbook
Last reviewed: 2026-05-10
This runbook wires certctl's OIDC SSO surface against Okta, a commercial cloud IdP. Okta offers a free developer tier (https://dev-NNNNN.okta.com) suitable for evaluation; production runs on a paid Workforce Identity tenant.
For the canonical reference + mental model, read keycloak.md first; this runbook only documents the Okta-specific deltas.
Prerequisites
On the Okta side:
- A Workforce Identity tenant (or free Developer Edition account at https://developer.okta.com/signup/).
- Super Admin or Application Admin role in your Okta tenant.
- Network reachability from certctl-server to
https://<your-org>.okta.com/.well-known/openid-configurationOR to a custom authorization server endpoint if you're using one (https://<your-org>.okta.com/oauth2/<auth-server-id>/.well-known/openid-configuration).
On the certctl side: same as Keycloak.
IdP-side configuration
1. Create the OIDC application
In the Okta admin console:
Applications → Applications → Create App Integration:
- Sign-in method: OIDC - OpenID Connect.
- Application type: Web Application.
- Click Next.
App config:
- App integration name:
certctl. - Logo: optional.
- Grant types: Authorization Code (CHECK). Leave Refresh Token unchecked unless you have a specific reason — certctl doesn't currently use refresh tokens.
- Sign-in redirect URIs:
https://<your-certctl-host>:8443/auth/oidc/callback. - Sign-out redirect URIs: optional; leave empty unless you also configure RP-initiated logout.
- Trusted Origins: leave default.
- Assignments → Controlled access: Limit access to selected groups (recommended; pick the
certctl-*groups from step 3 below). - Click Save.
On the saved app's General tab, copy the Client ID and Client secret (under Client Credentials). The secret is shown once on creation — copy it immediately or rotate via "Generate new secret".
2. Pick or create an authorization server
Okta has TWO authorization-server tiers:
- The Org Authorization Server at
https://<your-org>.okta.com— emits ID tokens with limited claims; cannot host custom claims directly. Use for the simplest setup. - A Custom Authorization Server at
https://<your-org>.okta.com/oauth2/<auth-server-id>— fully configurable scopes + claims + access policies. The free developer tier ships with a default custom server at/oauth2/default. Recommended for production.
For this runbook we use the default custom server: https://<your-org>.okta.com/oauth2/default.
3. Create the groups + assign users
Directory → Groups → Add Group:
- Repeat for
certctl-engineers,certctl-viewers, optionallycertctl-admins.
Directory → People → → Groups: assign each user to the appropriate certctl-* group(s).
Then go back to the App from step 1 and on the Assignments tab, assign the certctl-* groups to the application. Without this assignment Okta will reject the user's login attempt at the IdP layer with "User is not assigned to the client application".
4. Configure the groups claim
This is the load-bearing Okta-specific step. The default authorization server does NOT emit a groups claim out of the box — you have to define it.
Security → API → Authorization Servers → default → Claims → Add Claim:
- Name:
groups. - Include in token type: ID Token, Always (also tick Access Token if you want the userinfo-fallback path to work).
- Value type: Groups.
- Filter: pick Matches regex with the value
certctl-.*so only thecertctl-*groups are emitted (saves on token size; users in dozens of unrelated groups get a bloated token otherwise). - Disable claim: off.
- Include in: Any scope (or pin to
openidif you want the claim only on the certctl-flow). - Click Create.
5. (Optional) Add email and profile claims
The default custom server already emits email and name under the profile and email scopes — no action needed unless you've stripped them from a custom config.
certctl-side configuration
curl -X POST https://<your-certctl-host>:8443/api/v1/auth/oidc/providers \
-H "Authorization: Bearer ${CERTCTL_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "Okta",
"issuer_url": "https://your-org.okta.com/oauth2/default",
"client_id": "<paste-from-step-1>",
"client_secret": "<paste-from-step-1>",
"redirect_uri": "https://certctl.example.com:8443/auth/oidc/callback",
"groups_claim_path": "groups",
"groups_claim_format": "string-array",
"fetch_userinfo": false,
"scopes": ["openid", "profile", "email"],
"iat_window_seconds": 300,
"jwks_cache_ttl_seconds": 3600
}'
Notes:
issuer_urlMUST match exactly what Okta emits as theissclaim. For the default custom server it'shttps://<your-org>.okta.com/oauth2/default(no trailing slash). The org server's issuer is justhttps://<your-org>.okta.com(no/oauth2/...path). Mismatching either side trips certctl'sErrIssuerMismatchsentinel.- The
groupsscope is NOT required in the scopes list — Okta emits the claim based on the claim definition's "Include in: any scope" setting. Addinggroupsto the scopes list is harmless if your custom server has the scope defined.
Add the group→role mappings: certctl-engineers → r-operator, certctl-viewers → r-viewer, certctl-admins → r-admin.
Verification
End-to-end login + audit + Sessions checks are identical to Keycloak.
Okta-specific: the audit row's details.subject will be Okta's user UID (a 20-char alphanumeric string starting with 00u), stable across email changes. The certctl users table's oidc_subject column will hold this UID.
Optional Okta smoke test in CI: Phase 10 ships an opt-in smoke test at internal/auth/oidc/integration_okta_smoke_test.go (build tags integration && okta_smoke). Set OKTA_ISSUER + OKTA_CLIENT_ID + OKTA_CLIENT_SECRET env vars and run make okta-smoke-test to drive a discovery + RefreshKeys round-trip against your live tenant. Pre-reqs: enable the Resource Owner Password (ROPC) grant on the application (Sign-On tab → Grant types → Resource Owner Password) for the smoke test only; production certctl uses auth-code-with-PKCE.
JWKS-rotation drill: Okta auto-rotates signing keys every ~3 months and publishes the new key alongside the old in the JWKS doc for ~1 month overlap. Manual rotation: Security → API → Authorization Servers → default → Keys → "Generate new key". After rotation, click "Refresh discovery cache" in certctl's GUI; new tokens validate immediately.
Troubleshooting
"User is not assigned to the client application" at the Okta login screen.
You created the app + the user but didn't assign the user to the app via a group. Either assign the user directly (App → Assignments → Assign to People) or assign the certctl-* groups to the app (App → Assignments → Assign to Groups).
Login completes but groups claim is empty in the ID token.
Most common Okta gotcha — the default custom server doesn't emit groups until you define the claim (step 4 above). Decode the ID token at jwt.io to confirm. If the claim is defined but empty, check the regex filter in step 4 — certctl-.* matches names like certctl-engineers but NOT engineers.
ErrIssuerMismatch after correctly configuring the discovery URL.
The issuer claim Okta puts in the ID token MUST match OIDCProvider.IssuerURL byte-for-byte, including trailing slash. The default custom server emits https://<your-org>.okta.com/oauth2/default (no trailing slash); the org server emits https://<your-org>.okta.com. Don't append a trailing slash to either.
Login succeeds but the certctl User.Email is empty.
The email scope wasn't requested OR the user's email isn't verified at Okta. Add email to the certctl scopes config and ensure Okta's user has a verified primary email.
Okta returns "PKCE code verifier required". The certctl service hard-codes PKCE-S256 on every login (RFC 9700 mandate). If Okta is rejecting the verifier, the most likely cause is a misconfigured app type — confirm the Okta application is "Web Application" (which supports auth-code + PKCE), not "Single-Page Application" (which has different token-binding rules) or "Native App".
Custom-server access policies blocking the login.
By default the default custom authorization server has an "Access Policy" with one rule allowing all clients + all users. If you've tightened this (production hygiene), add a rule that allows the certctl client + the certctl-* groups: Security → API → Authorization Servers → default → Access Policies → → Add Rule.
Validation checklist
Same as keycloak.md, with Okta-specific values + the access-policy check above.
Sign-off: _______________ (operator) on _______________ (date).