Files
certctl/docs/operator/oidc-runbooks/google-workspace.md
T
shankar0123 2893f9b48e auth-bundle-2 Phase 11: 6 per-IdP OIDC runbooks + index + docs/README wiring
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.
2026-05-10 15:49:56 +00:00

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:

  1. It requires a service account with domain-wide delegation, which is a major security surface to grant to certctl.
  2. It's a separate REST call after the OIDC flow, not a claim — certctl's group-claim resolver is path-shape, not API-shape.
  3. 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 Import only 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:

  1. Log out of Keycloak's admin console.
  2. Hit https://<keycloak-host>/realms/<realm-name>/account in an incognito window.
  3. Click "Sign in" — Keycloak's login page should now show Sign in with Google Workspace as a button below the local login form.
  4. Click it; authenticate via Google; you should land on Keycloak's account page.
  5. 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:

  1. certctl → Keycloak authorize endpoint.
  2. Keycloak's login page shows Sign in with Google Workspace + the local login form. User clicks Google.
  3. Keycloak → Google authorize endpoint. User authenticates at Google.
  4. Google → Keycloak callback (/broker/google/endpoint). Keycloak resolves the user, assigns the default group.
  5. Keycloak → certctl callback. certctl sees a normal Keycloak ID token with the groups claim populated by Keycloak.
  6. 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_subject column in certctl's database should contain the Keycloak-side stable subject (a UUID), NOT the Google subject. Decode the certctl-side ID token and confirm iss is Keycloak's URL, sub is 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).

If broker deployment is impossible:

  1. Configure certctl with issuer_url = https://accounts.google.com, client_id + client_secret from your Google OAuth client (with redirect URI pointed at certctl directly).
  2. Add a SINGLE group→role mapping where group_name is 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.subject is the Keycloak UUID, NOT Google's sub (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).