A Jenkins security realm that lets users sign in with Google and turns their Google Group memberships into Jenkins authorities, so role-based access control can be managed by adding/removing people in Google Groups instead of maintaining local Jenkins users. A local username/password fallback is kept as a break-glass path.
Onboard a QA engineer -> add them to jenkins-qa@your-domain.com. Done.
Offboard them -> remove them from the group. Applies at their next login;
their API tokens lose it within groupAuthorityMaxAgeDays.
- Features
- How it works
- Requirements
- Installation
- Configuration
- Configuration reference
- The login page
- Troubleshooting
- Limitations
- Building from source
- License
- Sign in with Google: OAuth 2.0 code flow with PKCE; ID token fully verified (signature, issuer, audience, expiry,
email_verified, hosted-domain claim). - Google Groups as authorities: the user's direct group memberships (as lowercase group emails) become Jenkins authorities, usable in any authorization strategy (e.g. Role Strategy).
- No admin grant needed: groups are resolved with the logged-in user's own OAuth token via the Cloud Identity API. No service account, no domain-wide delegation, no Workspace super-admin involvement.
- Break-glass password login: the standard username/password form keeps working for users created under the previous local-database realm, so a Google outage or misconfiguration never locks you out.
- API tokens unaffected: existing automation users keep authenticating with their Jenkins API tokens.
- Offboarding-safe API tokens: group permissions used by API tokens expire a configurable number of days after the user's last Google login (default 7).
- Account binding: each Jenkins user is bound to the Google account (
sub) that first signed in with that email, so a reassigned email address cannot take over the previous owner's API tokens, credentials or permissions. - Configuration as Code: first-class JCasC support, including env-var interpolation for every field.
- Login logging: every Google login logs the full list of resolved authorities, and Jenkins' built-in
/whoAmIpage shows them to the user.
sequenceDiagram
autonumber
actor U as User (browser)
participant J as Jenkins<br/>(this plugin)
participant G as Google OAuth
participant CI as Cloud Identity API
U->>J: GET /login
J-->>U: Login page: username/password form + "Sign in with Google"
U->>J: Click "Sign in with Google"<br/>(/securityRealm/commenceLogin)
J-->>U: Redirect to Google (state, PKCE S256, hd=your-domain.com)
U->>G: Authenticate / pick account
G-->>U: Redirect back with authorization code
U->>J: GET /securityRealm/finishLogin?code=...&state=...
J->>G: Exchange code (+ PKCE verifier) for tokens
G-->>J: ID token + access token
Note over J: Verify ID token: signature, issuer,<br/>audience, expiry, email_verified,<br/>hd == hostedDomain
J->>CI: searchDirectGroups(member_key_id == user email)<br/>with the user's own access token
CI-->>J: Direct group memberships (paginated)
Note over J: authorities = [authenticated,<br/>jenkins-qa@your-domain.com, ...]<br/>(filtered by groupIncludePattern)
J-->>U: Session created, redirect to original page
Note over U,J: Role Strategy matches "group:" entries<br/>against the resolved authorities
- The Jenkins user id is the full email, lowercase, bound at first login to the Google account's stable id (
sub). A later login with the same email but a different Google account (for example after the address was reassigned) is refused. - Authorities are re-resolved at every Google login and last for the session.
- API-token requests reuse the group authorities recorded at the user's most recent Google login for
groupAuthorityMaxAgeDaysdays; after that they carry onlyauthenticateduntil the user signs in with Google again.
| Requirement | Version / notes |
|---|---|
| Jenkins | 2.555.3 or newer (plugin baseline) |
| Java | 21+ (Jenkins controller) |
| Google Workspace / Cloud Identity | any edition (premium not required) |
| GCP project | with the Cloud Identity API enabled |
| Jenkins URL | must be configured in Manage Jenkins → System (used to build the OAuth redirect URI; Google login is refused without it) |
| Outbound HTTPS | from the controller to oauth2.googleapis.com, www.googleapis.com and cloudidentity.googleapis.com, directly or through the HTTP proxy configured in Manage Jenkins → System |
In Jenkins, open Manage Jenkins → Plugins → Available plugins, search for Google Groups OAuth, install it and restart Jenkins.
FROM jenkins/jenkins:2.555.3-jdk21
RUN jenkins-plugin-cli --plugins google-groups-oauth
Important
Do not switch the security realm before completing the Google Cloud setup; keep a break-glass admin (see The login page) until the Google login path is proven.
- Open the Google Cloud console and select (or create) a project.
- Enable the Cloud Identity API: APIs & Services → Library → search "Cloud Identity API" → Enable.
- Configure the OAuth consent screen (APIs & Services → OAuth consent screen):
- User type: Internal. This restricts sign-in to accounts in your Google Workspace organization and skips Google's sensitive-scope verification review.
- Create the OAuth client (APIs & Services → Credentials → Create credentials → OAuth client ID):
-
Application type: Web application
-
Authorized redirect URI:
https://<your-jenkins-url>/securityRealm/finishLogin
-
- Note the Client ID and Client secret; you'll need them in Step 3.
The plugin requests these scopes (no consent-screen scope pre-registration is needed for Internal apps):
openid email profile
https://www.googleapis.com/auth/cloud-identity.groups.readonly
Create one Google Group per Jenkins role, e.g.:
| Group | Purpose |
|---|---|
jenkins-admins@your-domain.com |
Jenkins administrators |
jenkins-qa@your-domain.com |
QA engineers |
jenkins-developers@your-domain.com |
Developers |
jenkins-report-viewers@your-domain.com |
Read-only report access |
Warning
Keep these groups flat. Only direct memberships resolve; nested groups never appear (the transitive-membership API requires Workspace Enterprise Standard/Plus or Cloud Identity Premium). Add every user directly to each group.
Also keep the groups' default member-visibility settings: Google silently omits groups whose membership the caller is not allowed to view.
Via the UI: Manage Jenkins → Security → Security Realm → "Sign in with Google (Google Groups authorities)", then fill in the fields from the reference table below.
Via JCasC (recommended):
jenkins:
securityRealm:
googleGroupsOAuth:
clientId: "1234567890-abc.apps.googleusercontent.com"
clientSecret: "${google-oauth-client-secret}" # JCasC secret source
hostedDomain: "your-domain.com" # required; hd claim check
groupIncludePattern: "^jenkins-.*@your-domain\\.com$" # optional authority filter
onGroupLookupFailure: "DEGRADE" # DEGRADE | FAIL
groupAuthorityMaxAgeDays: 7 # API-token group expiry; 0 = never
Every field supports JCasC interpolation, from environment variables or any other secret source (e.g. files mounted under /run/secrets/):
jenkins:
securityRealm:
googleGroupsOAuth:
clientId: "${GOOGLE_OAUTH_CLIENT_ID}"
clientSecret: "${GOOGLE_OAUTH_CLIENT_SECRET}"
hostedDomain: "${GOOGLE_HOSTED_DOMAIN:-your-domain.com}" # ${VAR:-default} works
groupIncludePattern: "${GOOGLE_GROUP_PATTERN:-^jenkins-.*@your-domain\\.com$}"
onGroupLookupFailure: "${GROUP_LOOKUP_FAILURE_POLICY:-DEGRADE}"
Tip
Prefer a mounted-secret source over an env var for clientSecret: on Kubernetes, a secret mounted under /run/secrets/ is referenced by file name, stays out of the pod spec, and rotations apply on the next JCasC reload, while env-var changes require a pod restart.
With the Role Strategy plugin, bind roles to group emails (lowercase). Keep user: entries for automation accounts and break-glass admins:
jenkins:
authorizationStrategy:
roleBased:
roles:
global:
- name: "admin"
permissions: ["Overall/Administer"]
entries:
- group: "jenkins-admins@your-domain.com"
- user: "admin" # break-glass local admin, keep it
- name: "qa"
permissions: ["Overall/Read", "Job/Build", "Job/Read"]
entries:
- group: "jenkins-qa@your-domain.com"
- user: "qaautomation" # API-token automation user, keep it
That's it. Membership changes in Google Groups apply at each user's next login, and to their API tokens within groupAuthorityMaxAgeDays (see Offboarding).
| Field | Required | Default | Description |
|---|---|---|---|
clientId |
Yes | (none) | OAuth client ID from Step 1 |
clientSecret |
Yes | (none) | OAuth client secret (stored encrypted as hudson.util.Secret) |
hostedDomain |
Yes | (none) | Workspace domain, e.g. your-domain.com. Logins are rejected unless the verified ID token's hd claim equals this (defense in depth on top of the Internal consent screen) |
groupIncludePattern |
No | (all groups) | Java regex; only matching group emails become authorities. Keeps authority lists small, e.g. ^jenkins-.*@your-domain\.com$ |
groupAuthorityMaxAgeDays |
No | 7 |
Days that the group authorities recorded at a user's last Google login stay valid for API-token requests. Jenkins never re-checks Google between logins, so this bounds how long a removed member keeps group permissions through their tokens. After expiry, token requests carry only authenticated until the user signs in with Google again. 0 = never expire (not recommended). Local users are not affected |
onGroupLookupFailure |
No | DEGRADE |
What to do when the group lookup fails at login: DEGRADE = log in with only the authenticated authority and log a loud WARNING; FAIL = refuse the login. Failures are never cached as "no groups" |
Behavior notes:
- Sign-up is always disabled; users cannot self-register.
- Group cache: successful lookups are cached for 60 seconds per user to absorb login storms; failures are never cached.
- Authorities refresh at login, not in the background: a removed group membership persists until that user's session ends and they log in again, and in their API tokens for up to
groupAuthorityMaxAgeDays.
Jenkins API tokens work for every user, including people who only ever sign in with Google. Jenkins does not ask Google about group memberships between logins, so a token request carries the Google Group authorities from its owner's most recent Google login, and only for a limited time:
- For
groupAuthorityMaxAgeDaysdays after that login (default 7), token requests have the owner's Google Group authorities. - After that, token requests carry only
authenticated. The token still authenticates, but anything granted through a Google Group is refused (typically HTTP 403) until the owner signs in with Google again, which renews the groups for anothergroupAuthorityMaxAgeDaysdays. - Permissions granted to the user directly (
user:entries) are not affected.
This is what makes offboarding work for tokens: someone removed from a group, or from the Workspace, loses that group's permissions within groupAuthorityMaxAgeDays even if they never sign in again.
For long-running automation (CI scripts, bots, integrations), do not rely on Google Group authorities. Grant the automation account its permissions with user: entries, as in Step 4. Setting groupAuthorityMaxAgeDays to 0 disables the expiry, but then people removed from a group keep its permissions through their tokens indefinitely.
Removing someone from a Google Group (or from the Workspace) does not reach Jenkins immediately, because Jenkins only talks to Google when that person signs in:
| Access path | When the removal takes effect |
|---|---|
| New Google login | Immediately (the group is gone, or the login fails) |
| Existing browser session | When the session ends (Jenkins session timeout) |
| API tokens | After groupAuthorityMaxAgeDays without a Google login (default 7 days; see API tokens) |
For immediate removal (for example, a departing administrator), open the person's Jenkins user page at https://<your-jenkins-url>/user/<email>/:
- Under Security, terminate their active sessions.
- Then Delete the user. That revokes their API tokens and ends their access at once.
Each Jenkins user record is bound to the Google account (sub claim) that first signed in with its email. If the address is later given to a different Google account, that account's login is refused with a log message naming both account ids, rather than inheriting the old record's API tokens, user-scoped credentials and user: grants. To let the new owner in, delete the old Jenkins user record first. Records created before this binding existed are bound at their next Google login.
The standard Jenkins login page shows both options:
- Username/password form: validated against the password hashes stored on user records created under the previous local-database realm. This is the break-glass path: if Google OAuth is down or misconfigured, pre-existing local admins can still sign in. Users who only ever logged in via Google have no stored password and cannot use the form.
- "Sign in with Google" button: starts the OAuth flow. Users with an active Google session typically see at most an account-chooser click.
Note
Since sign-up is disabled, the set of password-capable users is frozen at migration time. Verify at least one local admin's password before switching the realm.
A user's permissions are missing. Ask them to open https://<your-jenkins-url>/whoAmI/: this built-in Jenkins page lists the authorities resolved at their login and is reachable without Overall/Read (a user whose groups were all filtered may have no permissions at all).
A group is missing from /whoAmI. In order of likelihood:
- The user is a nested member (via a sub-group); only direct memberships resolve. Add them directly.
- The group's member-visibility settings hide memberships from members; Google then silently omits the group. Restore default visibility.
- The group email doesn't match your
groupIncludePattern. - The Cloud Identity API is not enabled on the OAuth client's GCP project.
Every login is also logged at INFO with the full resolved authority list:
Google login: alice@your-domain.com authorities=[authenticated, jenkins-qa@your-domain.com]
Common errors:
| Symptom | Cause / fix |
|---|---|
Google login is not configured correctly: ... (HTTP 500) |
The message names what is missing: Client ID, hosted domain, or the Jenkins URL (Manage Jenkins → System → Jenkins URL). Password login keeps working meanwhile |
this email address belongs to a different Google account |
The email was reassigned to a new Google account; see Email address reuse |
redirect_uri_mismatch from Google |
The authorized redirect URI in GCP must be exactly https://<jenkins-url>/securityRealm/finishLogin |
hd claim ... does not match in logs |
User signed in with an account outside hostedDomain; rejection is expected |
State did not match or login session expired |
Stale/expired login attempt (or cookies blocked); just retry |
Login refused with onGroupLookupFailure=FAIL |
Group lookup failed (network/API error); check controller logs, and switch to DEGRADE if lockout risk outweighs missing groups |
- Direct (flat) group memberships only: a consequence of using the free-tier Cloud Identity API with the user's own token. Revisit only with premium Workspace licensing (
searchTransitiveGroups). - Silent group filtering by Google (visibility settings) cannot be detected server-side in user-token mode; mitigated by login logging and
/whoAmI. - No background authority refresh: changes apply at next login (and to API tokens within
groupAuthorityMaxAgeDays); see Offboarding. - The realm is intentionally designed so a service-account resolver mode (Cloud Identity Groups Reader or Admin SDK with domain-wide delegation) can be added later without redesigning; not implemented in v1.
./mvnw verify # build + tests -> target/google-groups-oauth.hpi
./mvnw hpi:run # throwaway Jenkins with the plugin at http://localhost:8080/jenkins
Warning
Use the bundled Maven Wrapper (./mvnw). The Debian/Ubuntu-packaged Maven (apt install maven) cannot build Jenkins plugins: its patched sisu fails to discover the hpi packaging type ("Unknown packaging: hpi").
Layout:
src/main/java/io/jenkins/plugins/googlegroupsoauth/
GoogleGroupsSecurityRealm.java # realm: login endpoints, password fallback, descriptor
GoogleOAuthService.java # auth URL, code exchange, ID token verification (hd!)
GroupResolver.java # resolver interface (pluggable for future SA mode)
CloudIdentityDirectGroupResolver.java # searchDirectGroups + pagination + filter
GoogleFederatedLoginService.java # "Sign in with Google" button on the login page
GoogleUserInfo.java # profile claims -> Jenkins user record