mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:11:29 +00:00
2893f9b48e
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.
199 lines
9.8 KiB
Markdown
199 lines
9.8 KiB
Markdown
# Auth0 OIDC runbook
|
|
|
|
> Last reviewed: 2026-05-10
|
|
|
|
This runbook wires certctl's OIDC SSO surface against [Auth0](https://auth0.com/), a commercial cloud IdP (now part of Okta but operationally distinct). Auth0 has a free developer tier suitable for evaluation; production runs on a paid B2B / B2C plan.
|
|
|
|
For the canonical reference + mental model, read [keycloak.md](keycloak.md) first; this runbook only documents the Auth0-specific deltas.
|
|
|
|
## The big Auth0 quirk: namespaced custom claims
|
|
|
|
Auth0 imposes a hard rule: any custom claim emitted from an Action MUST use a namespaced URL-shape key (e.g. `https://your-namespace/groups`). Auth0 silently strips claims that look like standard OIDC claims (`groups`, `roles`, `permissions`, etc.) when emitted from an Action — this is a security feature to prevent claim-spoofing.
|
|
|
|
certctl handles this via the `groups_claim_path` config. If your Action emits `https://your-namespace/groups`, set `OIDCProvider.groups_claim_path` to that exact URL. The hand-rolled groupclaim resolver at `internal/auth/oidc/groupclaim/resolver.go` recognizes URL-shape paths (anything starting with `http://` or `https://`) and treats the entire string as a single literal key — it does NOT split on `/`.
|
|
|
|
Set `groups_claim_format` to `string-array`; the underlying claim shape is still a JSON array of group-name strings, just stored under a URL-shape key.
|
|
|
|
## Prerequisites
|
|
|
|
**On the Auth0 side:**
|
|
|
|
- An Auth0 tenant (free dev tier at <https://auth0.com/signup> works). Tenant URL looks like `https://<tenant-name>.<region>.auth0.com`.
|
|
- Owner or Auth0 Administrator role.
|
|
- Network reachability from certctl-server to `https://<tenant>.auth0.com/.well-known/openid-configuration`.
|
|
|
|
**On the certctl side:** same as Keycloak.
|
|
|
|
## IdP-side configuration
|
|
|
|
### 1. Pick a namespace string
|
|
|
|
Decide on a unique URL-shape namespace for certctl's custom claims. It does NOT have to resolve to a real domain; Auth0 just requires it to be URL-shape and unique within your tenant. A reasonable choice:
|
|
|
|
```
|
|
https://certctl.example.com/auth/
|
|
```
|
|
|
|
Use that prefix for every custom claim; for groups specifically:
|
|
|
|
```
|
|
https://certctl.example.com/auth/groups
|
|
```
|
|
|
|
We'll refer to this as `<NS>/groups` in the rest of this runbook.
|
|
|
|
### 2. Create the Application
|
|
|
|
In the Auth0 dashboard:
|
|
|
|
**Applications → Applications → Create Application**:
|
|
|
|
- Name: `certctl`.
|
|
- Application Type: **Regular Web Applications**.
|
|
- Click **Create**.
|
|
|
|
On the saved app's **Settings** tab:
|
|
|
|
- Application Login URI: blank (Auth0 doesn't need it for the auth-code flow).
|
|
- Allowed Callback URLs: `https://<your-certctl-host>:8443/auth/oidc/callback` (one entry, exact match).
|
|
- Allowed Logout URLs: optional.
|
|
- Allowed Web Origins: `https://<your-certctl-host>:8443`.
|
|
- Token Endpoint Authentication Method: **Post** (default; matches the certctl service's expectation of `client_secret_post`).
|
|
- Save Changes.
|
|
|
|
Copy the **Domain** (this is the issuer base — `https://<tenant>.auth0.com`), **Client ID**, and **Client Secret** from the same Settings page.
|
|
|
|
### 3. Configure the connection (where users live)
|
|
|
|
If you're using Auth0's Database connection (default username + password), the existing **Username-Password-Authentication** connection works. For SSO to Google / Microsoft / SAML, configure those connections under **Authentication → Enterprise** or **Authentication → Social** and ensure the connection is enabled on the certctl Application (App → Connections tab).
|
|
|
|
### 4. Define the groups
|
|
|
|
Auth0 doesn't have a first-class "Groups" concept like Okta or Keycloak — you have THREE options to model groups, each with tradeoffs:
|
|
|
|
**Option A: User app_metadata (simplest, recommended for dev tier).**
|
|
|
|
Each user has a `app_metadata` JSON blob you can set via the Management API, the dashboard, or a post-registration script. Stick the groups in there:
|
|
|
|
```json
|
|
{
|
|
"groups": ["certctl-engineers"]
|
|
}
|
|
```
|
|
|
|
In the Auth0 dashboard, **User Management → Users → <user> → app_metadata**: paste the JSON above and Save.
|
|
|
|
**Option B: Auth0 Authorization Extension (paid plans, recommended for production).**
|
|
|
|
Install the Authorization Extension from **Marketplace → Extensions → Authorization**. It adds a first-class "Groups" concept with UI for assignment + nested groups. Read the extension's docs; it emits groups under `<NS>/groups` automatically once enabled.
|
|
|
|
**Option C: Roles + Permissions (Auth0's RBAC primitive).**
|
|
|
|
Use **User Management → Roles** to define roles like `certctl-engineer` + `certctl-viewer`. Assign roles to users. Have your Action emit role names as a `groups` claim. This is what Auth0 documents as the canonical pattern; it's slightly heavier than Option A but more discoverable in the dashboard.
|
|
|
|
This runbook uses **Option A** for clarity; the Action below reads from `app_metadata.groups`.
|
|
|
|
### 5. Write the Action that emits the groups claim
|
|
|
|
**Actions → Library → Create Action → Build from scratch**:
|
|
|
|
- Name: `certctl-emit-groups`.
|
|
- Trigger: **Login / Post Login**.
|
|
- Runtime: Node 18.
|
|
- Click **Create**.
|
|
|
|
Paste this code:
|
|
|
|
```javascript
|
|
exports.onExecutePostLogin = async (event, api) => {
|
|
const namespace = "https://certctl.example.com/auth/";
|
|
const groups = (event.user.app_metadata && event.user.app_metadata.groups) || [];
|
|
if (groups.length > 0) {
|
|
api.idToken.setCustomClaim(namespace + "groups", groups);
|
|
api.accessToken.setCustomClaim(namespace + "groups", groups);
|
|
}
|
|
};
|
|
```
|
|
|
|
Replace `https://certctl.example.com/auth/` with your namespace from step 1. Click **Deploy**.
|
|
|
|
Then bind the Action to the Login flow:
|
|
|
|
**Actions → Flows → Login**: drag `certctl-emit-groups` from the Custom tab into the flow, between Start and Complete. Click **Apply**.
|
|
|
|
### 6. Verify the claim in a test login
|
|
|
|
Auth0's **Authentication → Authentication Profile → Try It** button or the **Logs → Real-time Logs** page can show you the issued ID token in real time. Decode at jwt.io to confirm `<NS>/groups` is present + populated.
|
|
|
|
## 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": "Auth0",
|
|
"issuer_url": "https://<tenant>.auth0.com/",
|
|
"client_id": "<paste-from-step-2>",
|
|
"client_secret": "<paste-from-step-2>",
|
|
"redirect_uri": "https://certctl.example.com:8443/auth/oidc/callback",
|
|
"groups_claim_path": "https://certctl.example.com/auth/groups",
|
|
"groups_claim_format": "string-array",
|
|
"fetch_userinfo": false,
|
|
"scopes": ["openid", "profile", "email"],
|
|
"iat_window_seconds": 300,
|
|
"jwks_cache_ttl_seconds": 3600
|
|
}'
|
|
```
|
|
|
|
Critical:
|
|
|
|
- `issuer_url` includes the **trailing slash** for Auth0 (`https://<tenant>.auth0.com/`). Auth0's `iss` claim emits with the trailing slash; mismatching trips `ErrIssuerMismatch`.
|
|
- `groups_claim_path` is the **full namespaced URL**, not the bare `groups` key. The certctl resolver treats this as a single literal lookup key against the ID token claims map (no path-walking through `/`).
|
|
|
|
Add the group→role mappings: `certctl-engineers` → `r-operator`, etc. The mapping table maps the group VALUES (the strings inside the claim's array), not the claim path.
|
|
|
|
## Verification
|
|
|
|
End-to-end login + audit + Sessions checks are identical to Keycloak. The audit row's `details.subject` will be Auth0's user_id (e.g. `auth0|abc123…` for database users, `google-oauth2|...` for federated), stable across email changes.
|
|
|
|
## Troubleshooting
|
|
|
|
**`ErrGroupsUnmapped` even though I see groups in the ID token at jwt.io.**
|
|
|
|
Check `groups_claim_path` exactly matches the namespaced key in the token. A common mistake: setting `groups_claim_path` to `groups` (the bare key) when the actual claim key is `https://certctl.example.com/auth/groups` (the namespaced version). The resolver's URL-shape detection is what makes the namespaced path work; if the claim path doesn't start with `http://` or `https://`, the resolver tries to walk it as a dot-separated path and fails.
|
|
|
|
**The `<NS>/groups` claim is missing from the ID token.**
|
|
|
|
- Action not bound to the Login flow: revisit step 5's "Apply" step.
|
|
- Action returns early because `event.user.app_metadata.groups` is undefined: confirm the user has the metadata set.
|
|
- Trying to set the claim under a non-namespaced key (e.g. `api.idToken.setCustomClaim("groups", groups)`): Auth0 silently drops it. Always use the namespace prefix.
|
|
|
|
**Auth0 returns "Service not found" or "Invalid audience".**
|
|
|
|
This usually means the certctl client wasn't authorized to access the userinfo endpoint or the application's `audience` setting conflicts with the OIDC discovery doc. The certctl service uses the Application's `client_id` as the `audience` claim — confirm Auth0 is emitting tokens with `aud = <client_id>` (decode at jwt.io).
|
|
|
|
**Login redirects loop between Auth0 and certctl.**
|
|
|
|
Most often a callback-URL mismatch — Auth0's "Allowed Callback URLs" must contain the EXACT certctl callback URL including port + scheme. Wildcards aren't allowed in production.
|
|
|
|
**`email_verified` is `false` and certctl rejects the user.**
|
|
|
|
certctl doesn't currently gate on `email_verified` — the User row stores email regardless. If your operator policy requires verified-only, add an Action that throws on `event.user.email_verified === false`:
|
|
|
|
```javascript
|
|
if (!event.user.email_verified) {
|
|
api.access.deny("email-not-verified");
|
|
}
|
|
```
|
|
|
|
## Validation checklist
|
|
|
|
Same as [keycloak.md](keycloak.md#validation-checklist) with Auth0-specific values, plus:
|
|
|
|
- [ ] The `<NS>/groups` claim is present in the ID token (verify via jwt.io decode).
|
|
- [ ] Removing a user's group from `app_metadata.groups` causes the next login to land on "no roles assigned".
|
|
- [ ] The Auth0 dashboard's **Logs → Real-time Logs** shows the certctl callback completing with HTTP 302 to the dashboard.
|
|
|
|
Sign-off: _______________ (operator) on _______________ (date).
|