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.
12 KiB
Google Workspace OIDC runbook (broker via Keycloak)
Last reviewed: 2026-05-10
This runbook wires certctl's OIDC SSO surface against Google Workspace (formerly G Suite). Google's OIDC implementation has a well-known limitation that makes it unsuitable for direct integration with certctl: the ID token does not emit a groups claim, so there is no way for certctl's ErrGroupsUnmapped fail-closed contract to resolve a user's role assignment.
The recommended pattern is to broker Google Workspace through Keycloak (or Authentik) as a federated identity provider. The end-user still signs in with their Google account, but certctl talks to Keycloak — which DOES emit groups — instead of talking to Google directly.
For the canonical reference + mental model, read keycloak.md first; this runbook builds on top of it.
The Google Workspace quirk in detail
What Google emits in an ID token: iss, aud, sub, azp, exp, iat, email, email_verified, name, picture, given_name, family_name, locale, hd (hosted domain). That's it.
What it does NOT emit: groups, roles, permissions, or any indicator of the user's Google Workspace organizational unit / group membership.
There is a Cloud Identity Groups API at https://cloudidentity.googleapis.com/v1/groups/-/memberships:searchTransitiveGroups that lets a privileged service account look up a user's groups, but:
- It requires a service account with domain-wide delegation, which is a major security surface to grant to certctl.
- It's a separate REST call after the OIDC flow, not a claim — certctl's group-claim resolver is path-shape, not API-shape.
- The latency budget of an extra API call per login is non-trivial in steady state.
For these reasons, the broker pattern is strongly preferred. If you absolutely cannot deploy a broker, see "Direct integration without groups" at the bottom of this runbook for a degraded mode where every Google-authenticated user gets a single fixed role.
Architecture: broker pattern
end user → Google Workspace login → Keycloak (federated IdP) → certctl
↑
│
adds groups claim from Keycloak's group store
(NOT from Google)
In this topology:
- The end user's authentication credentials live at Google.
- The user's group / role assignments live at Keycloak (manually or via SCIM provisioning from Google).
- certctl talks ONLY to Keycloak. From certctl's perspective this is identical to the keycloak.md runbook.
Prerequisites
- A running Keycloak instance with a realm dedicated to certctl. Read keycloak.md and complete that runbook FIRST against a local-only test user. Verify end-to-end OIDC works against Keycloak before adding Google as a federated provider.
- A Google Workspace tenant where you have Super Admin access OR can ask your Workspace admin to create OAuth credentials.
- A Google Cloud project (free; same console as Workspace).
IdP-side configuration
Step 1: create a Google OAuth client
In the Google Cloud Console (https://console.cloud.google.com/):
APIs & Services → OAuth consent screen → Configure:
- User Type: Internal (restricts to your Workspace domain) OR External (any Google account; usually NOT what you want for an internal cert-management tool).
- App name:
certctl SSO via Keycloak. - User support email: your team's address.
- Authorized domains: add the domain Keycloak runs on.
- Save.
APIs & Services → Credentials → Create Credentials → OAuth client ID:
- Application type: Web application.
- Name:
certctl-via-keycloak. - Authorized redirect URIs:
https://<keycloak-host>/realms/<realm-name>/broker/google/endpoint— this is Keycloak's default federated-IdP callback URL. Get the exact URL from Keycloak in step 2 below. - Click Create.
Copy the Client ID and Client secret.
Step 2: add Google as a federated identity provider in Keycloak
In the Keycloak admin console (https://<keycloak-host>/admin/):
Realm → Identity providers → Add provider → Google:
- Alias:
google(becomes part of the broker URL). - Display name:
Google Workspace. - Client ID: paste from step 1.
- Client secret: paste from step 1.
- Default scopes:
openid profile email. - Hosted Domain: your Workspace domain (e.g.
example.com); restricts to your tenant. - Sync mode: Force (rewrites the user's first/last name/email from Google on every login; the alternative
Importonly writes on first login). - Trust email: on (Google verifies emails; certctl-Keycloak chain inherits the trust).
- Click Save.
The Redirect URI field at the top of the saved provider's page shows the exact URL you should have entered in Google's console at step 1. Re-verify match.
Step 3: configure group assignment in Keycloak
This is the load-bearing step — we're explicitly NOT trusting Google for groups, so Keycloak has to provide them.
Option A: Manual group assignment in Keycloak.
Federated users from Google appear in Users in Keycloak after their first login. You assign them to certctl-engineers / certctl-viewers / etc. groups in Keycloak's UI manually. Pro: simple. Con: doesn't scale; new hires can't log in until an operator adds them to a group.
Option B: Default groups via "Default Groups" realm config.
Realm settings → User registration → Default Groups → Add: pick the lowest-privilege group (e.g. certctl-viewers). Every new federated user lands here automatically; operators promote individual users to higher groups as needed.
Option C: Mapper that derives groups from Google claims.
If your Google Workspace has organizational units that align with your role split, you can add a Keycloak Identity Provider Mapper that maps hd (hosted domain) or a custom Google directory custom-schema field to a Keycloak group. This is moderately fragile and Workspace-version-dependent; recommend B for most operators.
Option D: SCIM provisioning from Google to Keycloak.
Google Workspace can SCIM-push group memberships to Keycloak via the SCIM-for-Google-Cloud-Identity feature. Heavyweight; recommend only if you already have SCIM infrastructure.
This runbook uses Option B (default group) for clarity.
Step 4: verify the broker flow at Keycloak alone
Before bringing certctl into the picture:
- Log out of Keycloak's admin console.
- Hit
https://<keycloak-host>/realms/<realm-name>/accountin an incognito window. - Click "Sign in" — Keycloak's login page should now show Sign in with Google Workspace as a button below the local login form.
- Click it; authenticate via Google; you should land on Keycloak's account page.
- Back in the admin console, the user appears under Users. Confirm they're in the default group (Option B).
Only proceed to step 5 when Keycloak alone works end to end.
Step 5: configure certctl against Keycloak (NOT against Google)
Follow the keycloak.md runbook. Use the realm + client + groups configuration you set up there. The OIDCProvider.issuer_url is https://<keycloak-host>/realms/<realm-name> — Keycloak's URL, not Google's.
When the user clicks "Sign in with Keycloak" on certctl's login page, the browser flow is:
- certctl → Keycloak authorize endpoint.
- Keycloak's login page shows Sign in with Google Workspace + the local login form. User clicks Google.
- Keycloak → Google authorize endpoint. User authenticates at Google.
- Google → Keycloak callback (
/broker/google/endpoint). Keycloak resolves the user, assigns the default group. - Keycloak → certctl callback. certctl sees a normal Keycloak ID token with the
groupsclaim populated by Keycloak. - certctl mints the session.
End-to-end the user clicks twice (Keycloak's "Sign in with Google" button + Google's consent / login). Subsequent logins skip the consent screen if Google's session is fresh.
Verification
End-to-end login + audit + Sessions checks are identical to Keycloak. The key Google-Workspace-specific check:
- The
users.oidc_subjectcolumn in certctl's database should contain the Keycloak-side stable subject (a UUID), NOT the Google subject. Decode the certctl-side ID token and confirmissis Keycloak's URL,subis the Keycloak UUID. Don't confuse the certctl ID token with Google's ID token (which lives one hop upstream and certctl never sees directly).
Direct integration without groups (NOT RECOMMENDED)
If broker deployment is impossible:
- Configure certctl with
issuer_url = https://accounts.google.com,client_id+client_secretfrom your Google OAuth client (with redirect URI pointed at certctl directly). - Add a SINGLE group→role mapping where
group_nameis the empty string. Wait — certctl rejects empty group names. This is the structural reason this mode doesn't work: the fail-closed contract requires a real group claim to match.
The actual workaround is to manually add EVERY operator's email to a per-email mapping, OR to add a custom claim emitter at a thin proxy in front of Google. Both are hacks; the broker pattern is strictly better. We document the constraint here so future operators don't burn cycles trying to make it work.
Troubleshooting
Federated Google login completes at Keycloak but the user lands on "no roles assigned" at certctl.
The user authenticated through Google → Keycloak successfully but Keycloak didn't assign them a group (Option A wasn't completed for that user, or Option B's default group isn't mapped on the certctl side). Check:
- Keycloak → Users → → Groups: is the user in any
certctl-*group? - certctl → Auth → OIDC Providers → Keycloak → Group → role mappings: is that group mapped?
Google login fails with "redirect_uri_mismatch".
The Google OAuth client's authorized redirect URI doesn't match Keycloak's broker callback URL exactly. Re-fetch the URL from Keycloak (Identity Providers → Google → Redirect URI field) and paste it verbatim into Google's console.
Google auto-closes the consent prompt and returns "access_denied".
Workspace admin policies may block third-party app access. Either the Google OAuth client wasn't approved by the Workspace admin (Google Workspace Admin Console → Security → API controls → Trusted apps), or the OAuth consent screen is configured for "External" but the user is from a different Workspace. Switch to "Internal" if everyone signing in is in the same Workspace.
Keycloak log shows "Federated identity returned no email claim".
You requested OAuth scopes other than openid profile email. Re-add email to the Default Scopes on the Keycloak Identity Provider config.
Sign-out from certctl doesn't sign the user out of Google.
Expected. certctl revokes its own session; Google's session continues independently. If the user needs to fully log out, they sign out at https://accounts.google.com/Logout. The certctl + Keycloak chain is the standard "single sign-on, separate sign-outs" model.
Validation checklist
Same as keycloak.md, with these additions:
- Google → Keycloak federation works without certctl in the loop (step 4 above passes).
- A first-time Google sign-in lands the user in the Keycloak default group (or whatever Option you picked).
- The certctl audit row's
details.subjectis the Keycloak UUID, NOT Google'ssub(which would be a Google account ID). - Removing a user from Google Workspace causes their NEXT certctl session-validate to fail (after their existing session expires) — verify with a deactivated test user.
Sign-off: _______________ (operator) on _______________ (date).