user@gitdiot:~/blog/how-i-operate/google-auth-fragmentation$
● online ~/index ~/about
$ cat ./content/operating/google-auth-fragmentation.md --render
~/blog/how-i-operate/google-auth-fragmentation
how-i-operate google-workspace oauth auth Mar 24, 2026 · 7 min read

How Google's Permission Maze Fragmented Our CRM Auth (And How We Fixed It)

Every feature touched a different Google API behind a different permission gate. This is how the auth fragmented — and how we put it back together.

We built an AI-powered CRM that sends emails, manages deals, tracks deliverability, and alerts our team via Google Chat. Every one of those features touches a different Google API. And every one of those APIs lives behind a different permission gate, managed in a different admin console, governed by a different set of rules.

This is the story of how a system that worked perfectly on day one slowly fractured into three separate auth profiles with mismatched scopes, silent failures, and a re-auth process that broke more than it fixed.

It Started Simple

AgentCRM began as a Google Chat bot. One OAuth client, one scope (chat.messages), one profile. Claude responds to CRM queries in a Chat space. Clean.

Then we added email generation — personalized outreach emails with HTML signatures, tracking pixels, Gmail drafts. That needed gmail.modify. Different API, different scope, different consent. We created a second profile (CRM1) for the sales sender.

Then Drive uploads for account handoff documents. That needed drive. We added it to the existing profile.

Then email delivery tracking — polling Google's Admin Reports API to find out if our emails actually landed. That needed admin.reports.audit.readonly. A restricted scope. A different API entirely. One that requires workspace admin privileges and has its own enablement flow.

Then we moved the Chat bot into a Docker container and needed a third profile (docker-work) with file-based credential storage because containers don't have system keyrings.

Each capability was individually reasonable. Each addition was a clean PR. But the aggregate was a mess.

The Permission Archaeology

Here's what three months of incremental feature development produced:

Profile User Scopes What Broke
work michael.paige@ cloud-platform, drive Missing gmail.modify (drafts fail), missing admin.reports (delivery tracking 403s)
CRM1 ryan.slipakoff@ gmail.modify, drive Only profile that actually worked correctly
docker-work michael.paige@ 7 chat sub-scopes, cloud-platform Missing gmail.modify, missing drive

Two of three profiles were degraded. Features that appeared to work were actually routing through the wrong profile or silently skipping steps.

Google's Five Layers of Permission

What makes this particularly insidious is that Google doesn't have one permission system. They have at least five, and you need all of them aligned for a single API call to succeed:

Layer 1: GCP API Enablement

Where: Google Cloud Console → APIs & Services → Library

Before any OAuth scope does anything, the underlying API must be enabled on the GCP project. Gmail API, Drive API, Chat API, Admin SDK — each is a separate toggle. If Admin SDK isn't enabled, requesting the admin.reports.audit.readonly scope during OAuth just silently... doesn't grant it.

No error. No warning. The consent screen shows fewer scopes than you asked for, and unless you're counting, you won't notice.

Layer 2: OAuth Consent Screen Scope Registration

Where: Google Cloud Console → APIs & Services → OAuth consent screen → Edit → Scopes

Even after the API is enabled, the scope must be registered on the consent screen. This is a different step from enabling the API. You can have the Gmail API enabled but gmail.modify not listed on the consent screen. The OAuth flow will just drop that scope from the token.

Restricted scopes (like admin.reports.audit.readonly) have additional review requirements. For internal Workspace apps this is less onerous, but it's still a separate configuration surface.

Layer 3: OAuth Token Scopes

Where: The OAuth flow itself (browser consent screen)

When you run gws auth login -s gmail.modify,drive,chat, the CLI requests those scopes. But here's the trap: GWS CLI replaces scopes on re-auth, it doesn't append. If you re-auth to add one scope, you lose all the others unless you specify every scope in a single command.

We learned this the hard way. Re-authing to add admin.reports.audit.readonly dropped gmail.modify and chat.messages. The delivery tracking we were trying to fix broke the email drafting that was working.

Layer 4: Workspace Admin — Trusted App Status

Where: Google Workspace Admin Console → Security → API controls → App access control

Google's Re-Authentication Policy (RAPT) enforces periodic re-auth for OAuth apps that aren't marked as "Trusted" by the workspace admin. This manifests as invalid_rapt errors after 24 hours — your refresh token is fine, your scopes are fine, but Google forces an interactive re-auth anyway.

This is managed in the Workspace Admin Console, not the GCP Console. Different URL, different UI, different permission model. You need workspace admin access, not just GCP project owner access.

The fix is adding the OAuth client ID as a "Trusted" app. But finding this setting requires navigating: admin.google.com → Security → API controls → App access control → Manage Third-Party App Access → Configure new app → OAuth App Name or Client ID. That's six clicks deep in an admin console that most developers never open.

Layer 5: Per-User Scope Consent

Where: The user's own Google account security settings

Each user who authenticates through the OAuth flow has their own consent record. If a user previously granted 3 scopes and you add 2 more to the consent screen, the user needs to re-authenticate to grant the new scopes. Existing tokens won't magically gain new permissions.

And if the user revokes access from their Google Account settings (myaccount.google.com → Security → Third-party apps), all scopes are gone and re-auth is required — but the GCP and Workspace admin settings remain unchanged.

The Compound Failure Mode

The interaction between these layers creates failure modes that are genuinely difficult to debug:

Scenario: You add Admin SDK API support for delivery tracking.

  1. You enable Admin SDK API in GCP ✓
  2. You add admin.reports.audit.readonly to the consent screen ✓
  3. You run gws auth login -s admin.reports.audit.readonly
  4. The API returns 403 insufficientPermissions

What happened: Step 3 replaced all existing scopes with just the one you specified. Your Gmail drafts stopped working too, but you don't notice because you're focused on the Admin SDK error.

Another scenario: Everything works for 24 hours, then Chat messages stop sending.

  1. OAuth scopes are correct ✓
  2. API is enabled ✓
  3. Token is valid ✓
  4. invalid_rapt error ✗

What happened: The OAuth app isn't Trusted in Workspace Admin. RAPT kicked in after 24 hours. Nothing in the GCP Console or OAuth flow would have told you this. The error message doesn't mention RAPT, Workspace Admin, or Trusted apps — it just says "invalid_rapt" and you're left to Google that string.

How We Fixed It

We stopped treating auth as something you configure once and forget. We built a system around it:

1. Canonical scope manifest — A single TypeScript file (profiles.ts) defines which profile needs which scopes. This is the source of truth, not whatever scopes happen to be on the token.

2. Deterministic re-auth scriptgws-reauth.sh reads the canonical scope set and passes all scopes in one command. No more "add one scope, lose three" accidents.

3. Health check — An MCP tool and HTTP endpoint that compares actual token scopes against the canonical set. Reports missing scopes with exact fix commands.

4. Documentation — A single doc that maps every Google integration to its profile, scopes, APIs, admin settings, and failure modes. Step-by-step GCP Console instructions because the UI changes frequently enough that "go to the scopes page" isn't sufficient.

The health check output looks like this:

work ([email protected]) — WARN
  Token: valid
  Scopes: MISSING: gmail.modify, chat.messages, admin.reports.audit.readonly
  Fix: ./scripts/gws-reauth.sh work

CRM1 ([email protected]) — OK
  Token: valid
  Scopes: all present

docker-work ([email protected]) — WARN
  Token: valid
  Scopes: MISSING: gmail.modify, drive
  Fix: ./scripts/gws-reauth.sh docker-work

The Lesson

Google's APIs are individually well-designed. The OAuth flows work. The admin consoles are functional. But the aggregate experience of building a system that touches Gmail + Drive + Chat + Admin SDK across multiple users is genuinely complex in ways that aren't documented in any single place.

Five separate admin surfaces. Five layers that all need to agree. Silent scope dropping. Scope replacement on re-auth. An admin setting six clicks deep that causes 24-hour auth expiry. Error messages that require you to know the acronym "RAPT" to debug.

The answer isn't to avoid Google APIs — they're powerful and our CRM depends on them. The answer is to treat auth as infrastructure, not configuration. Monitor it. Codify the expected state. Detect drift. Automate recovery.

Because if you're building anything that touches more than two Google APIs, your auth will fragment. The only question is whether you'll notice before your users do.


Built with AgentCRM by United Logic. We build AI-powered systems on Adobe and Google platforms — and we've learned where the edges are.

subscribe.sh

Get the field notes

Weekly dispatches from an aging tech worker's refactoring. No spam, no thought leadership.

no spam · only high-signal logic