mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-09 14:18:57 +00:00
00c708524d
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.
144 lines
8.9 KiB
Markdown
144 lines
8.9 KiB
Markdown
# Okta OIDC runbook
|
|
|
|
> Last reviewed: 2026-05-10
|
|
|
|
This runbook wires certctl's OIDC SSO surface against [Okta](https://www.okta.com/), 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](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-configuration` OR 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`, optionally `certctl-admins`.
|
|
|
|
**Directory → People → <user> → 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 the `certctl-*` 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 `openid` if 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
|
|
|
|
```bash
|
|
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_url` MUST match exactly what Okta emits as the `iss` claim. For the default custom server it's `https://<your-org>.okta.com/oauth2/default` (no trailing slash). The org server's issuer is just `https://<your-org>.okta.com` (no `/oauth2/...` path). Mismatching either side trips certctl's `ErrIssuerMismatch` sentinel.
|
|
- The `groups` scope is NOT required in the scopes list — Okta emits the claim based on the claim definition's "Include in: any scope" setting. Adding `groups` to 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 → <policy> → Add Rule**.
|
|
|
|
## Validation checklist
|
|
|
|
Same as [keycloak.md](keycloak.md#validation-checklist), with Okta-specific values + the access-policy check above.
|
|
|
|
Sign-off: _______________ (operator) on _______________ (date).
|