Google Workspace: multi-connector setup
This page is not about a single connector — it's about running
identos.google-drive, identos.google-gmail,
identos.google-calendar, identos.google-chat and
identos.google-directory together off a single Google IdP.
One IdP, many connectors
The expected setup looks like this:
identity_provider
└── google (registered once, OAuth client_id/client_secret from Google Cloud)
managed_resource
├── drive-user → google-drive + idp_passthrough + google IdP
├── gmail-user → google-gmail + idp_passthrough + google IdP
├── calendar-user → google-calendar + idp_passthrough + google IdP
├── calendar-rooms → google-calendar + google_sa + service account
├── chat-user → google-chat + idp_passthrough + google IdP
└── directory → google-directory+ idp_passthrough + google IdP
A single Google login grants access to all of them at the same time. A Claude Code session hitting this gateway picks up tools from every one because they're all provisioned against the same authenticated subject.
Why you almost always want google-directory alongside the others
The rest of the Google APIs identify people by opaque numeric ID. Chat
returns members and message senders as users/102770538280811199852 with no
name and no email; Drive's list_file_permissions returns permission holders;
Calendar attendees arrive as bare addresses needing profile lookup. Two Chat
tools — find_direct_message and add_member — require a users/{id} that
no Chat tool can produce.
identos.google-directory is the resolver for all of that. Without it, an
agent can read a Chat space's whole history and still not be able to tell you
who said what.
Do not assume an ID from one Google API works in another. Observed live: Drive
permission IDs are 20 digits and can carry leading zeros
(01611141363991203537), while Chat member IDs are 21 digits without
(102770538280811199852) — different identifier spaces. google-directory
resolves the People API space, which is the one Chat uses.
How scope growth works
The tricky part is OAuth scopes. Each connector declares
upstream_scopes on its idp_passthrough auth option — the actual
Google OAuth scopes its users need. Drive wants
drive.readonly + drive.file; Gmail wants gmail.readonly +
gmail.send + gmail.modify + gmail.labels; Calendar wants
calendar.readonly + calendar.events.
When you provision a managed resource in idp_passthrough mode, the
gateway automatically merges that connector's upstream_scopes
into the target IdP's scopes list (IdpScopeMerger). Subsequent
user logins at /authorize pass the union of all those scopes to
Google, and the user consents once for everything.
before drive-user provision: idp.scopes = [openid, email]
after drive-user provision: idp.scopes = [openid, email,
drive.readonly, drive.file]
after gmail-user provision: idp.scopes = [... + gmail.readonly,
gmail.send, ...]
after calendar-user provision: idp.scopes = [... + calendar.readonly,
calendar.events]
The merge is additive and idempotent — uninstalling a connector does not remove scopes (another managed resource may still need them, and we can't safely attribute scopes to a single installer).
When a user sees "stored IdP token missing required scopes"
If you install / provision a Google connector after users have already logged in at Google through PBAC, their stored IdP tokens do not carry the newly-added scopes. The first gateway call that needs those scopes fails with a 502:
Stored IdP token missing required scopes:
[https://www.googleapis.com/auth/drive.readonly]. User must re-authenticate with step-up.
Two paths out:
- Recommended: have the user sign out and back in. On the next
/authorizepass, PBAC requests the expanded scope list and the user consents on Google's screen. No operator action required. - Batch: clear
upstream_identity_tokenfor the affected users via the admin API; their next interaction will bounce through Google for consent.
Going forward, the rule is: install and provision Google connectors before inviting users to the gateway, and users never see this error.
Pre-configuring the Google OAuth app
On the Google Cloud side, you still need to add these scopes to the OAuth 2.0 client's OAuth consent screen configuration so Google will permit them. The PBAC auto-merge handles the PBAC side; it can't teach Google about scopes its consent screen hasn't been told to offer.
Minimum scopes for all three connectors:
openid
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
https://www.googleapis.com/auth/drive.readonly
https://www.googleapis.com/auth/drive.file
https://www.googleapis.com/auth/gmail.readonly
https://www.googleapis.com/auth/gmail.send
https://www.googleapis.com/auth/gmail.modify
https://www.googleapis.com/auth/gmail.labels
https://www.googleapis.com/auth/calendar.readonly
https://www.googleapis.com/auth/calendar.events
Set the consent screen to External → Production (or Internal for a Workspace-only deployment) once you're past testing and want tokens to survive the 7-day expiry.
Two managed resources, one connector
Running the same connector in both idp_passthrough and google_sa
mode requires two managed resources (e.g. calendar-user and
calendar-rooms). A single managed resource picks one auth mode at
provision time; there's no runtime switch.
The policy layer can tell them apart by input.resource.name if you
need different rules for user-mode vs service-account mode (e.g.
room_only_mode: true only on calendar-rooms).
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| "Stored IdP token missing required scopes" | User logged in before the connector was provisioned | Re-log at Google, or clear the upstream_identity_token row |
| "idp_passthrough requires an authenticated subject" | Client used client_credentials (no user context) | Either switch the connector to google_sa or issue the call with an authorization_code token |
404 on /admin/api/connectors/identos.google-gmail/install | Jar was built before the connector was added | Rebuild the jar (mvn package -DskipTests) and recreate the instance |
| PDP denies every Gmail send with "recipient domain ... is on the operator denylist" | blocked_recipient_domains data is set and a recipient matches | Review the operator-data blob at data.pbac.operator.connectors["identos.google-gmail"] |
See also
- Google Drive connector
- Gmail connector
- Google Calendar connector
- Google Chat connector
- Google Directory connector — resolves the principal IDs the others return
- MCP clients — how Claude Code and other MCP clients attach to the gateway