ADR-014: Google OAuth as the agent's own identity, not service account + delegation
Context
Each Aucert agent that touches Google Workspace (Atlas, and any future agent that reads or comments on Google Docs/Drive) needs a Google identity. The original v0.1 design used a service account with domain-wide delegation (DWD) so the SA could createDelegated(atlas-agent@aucert.dev) and act as that real Workspace user. That works, but it has three structural problems:
- Blast radius. A service account with DWD enabled can impersonate any user in the Workspace domain — not just the agent. If the SA JSON key leaks, every employee's Drive is compromised, not just the agent's. The damage is bounded by the OAuth scopes granted in Workspace Admin, but those scopes are wide (
drive,documents). - Static keys. Using DWD requires a downloaded SA JSON key. GCP's
iam.managed.disableServiceAccountApiKeyCreationorg policy blocks this by default; we had to disable enforcement to make a key (captured in drift-2026-04-26-google-workload-identity-deferred.md). Static keys never expire and are a persistent leak risk. - Operational confusion. "The agent impersonates atlas-agent" sounds like the agent borrows a real user's permissions, but it's actually the SA borrowing them. ACL audits, Drive ownership, and "who shared this doc with whom" become harder to reason about because two identities are in play.
The originally-planned mitigation was Workload Identity Federation (WIF) trusting the AKS OIDC issuer — eliminates the static key but keeps DWD, so the blast-radius problem remains.
Decision
Each agent authenticates to Google as its own Workspace identity (e.g. atlas-agent@aucert.dev) using OAuth 2.0 with a long-lived refresh token. No service account, no DWD, no impersonation. The agent only ever sees what its own email has been granted access to.
Concretely:
- A long-lived refresh token, plus the OAuth client id and client secret, is stored encrypted in
agent_tokensper agent, alongside the agent's email as a diagnostic field. GoogleOAuthProviderreads those four values viaAstraClient.fetchTokenand builds aUserCredentialsfor the Google API client library.- A one-time browser consent flow (tools/scripts/google-oauth-setup.py) mints the refresh token; the operator pastes the values into Astra → Agent → Tokens.
GoogleDocsClient.create(oauth)andGoogleDriveClient.create(oauth)are the production entry points.
This applies to every agent with a Workspace identity, not just Atlas.
Alternatives considered
| Option | Pros | Cons |
|---|---|---|
| Chosen: OAuth as the real agent user | Smallest blast radius — agent is its own identity. No SA, no DWD, no key. ACL semantics are obvious: "what is shared with the agent's email." Simplest to explain. | One-time human consent flow per agent. Refresh token still needs safe storage (Astra encrypts it at rest). |
| SA + DWD impersonation (current v0.1) | Already implemented; no consent flow per agent. | Catastrophic blast radius — leaked SA key compromises every Workspace user. Requires disabling an org policy. Static key never expires. |
| SA + direct Drive sharing (no DWD) | No impersonation surface. | Service accounts cannot be sharees of a Drive ACL the way real users can — the SA email has no user session, so sharing a folder with atlas-agent-sa@…iam.gserviceaccount.com doesn't grant access. Already tried in V010 and reverted in V011 for exactly this reason. |
| Workload Identity Federation (planned v0.2) | Eliminates the static SA key. | Still requires DWD to access user-shared files, so the blast-radius problem is unchanged. More moving parts (OIDC pool, K8s SA projection) for a partial improvement. |
Consequences
What becomes easier
- Drive ACLs are now the only access-control surface — "share with the agent's email" is the entire mental model. No DWD scope list to keep in sync.
- The
iam.managed.disableServiceAccountApiKeyCreationorg policy can be re-enabled, restoring GCP's "secure by default" posture. - Reasoning about "what can this agent do?" only requires looking at the agent's email, not at SA bindings + DWD scope grants in Workspace Admin.
- The WIF migration plan and the GCP-A/B/C parallel prompts are no longer needed — the WIF drift doc is superseded by this ADR.
What becomes harder
- Onboarding a new agent now includes a one-time browser-based OAuth consent flow run by an operator while signed in as the agent. Cannot be fully automated through Astra's web UI (which has no localhost redirect handler), so we keep
tools/scripts/google-oauth-setup.pyas a small CLI exception to the "prefer Astra UI for ops" rule. - The refresh token is operator-handled cleartext for the brief window between the script printing it and the operator pasting it into Astra. Astra encrypts it at rest using the existing
AgentTokenEncryptionflow.
Risks
- Refresh token revocation. Google may revoke a refresh token unilaterally (account password change on the agent, prolonged inactivity, manual revoke at myaccount.google.com/permissions). The agent will start failing with 401s; recovery is rerunning
google-oauth-setup.py. We accept this — it is a normal operational event for OAuth-based integrations. - Token exfiltration. A leaked refresh token gives full access to the agent's Drive scope. Mitigations: encrypted at rest in Astra, never logged, rotation by re-running the setup script (which obsoletes the previous token if
prompt=consentis used, which the script enforces). - Per-agent OAuth client provisioning. Multiple agents can share a single OAuth client, or each agent can have its own. Either is acceptable — they are app identifiers, not user identifiers. We default to one shared OAuth client per project for simplicity; agents are distinguished by their refresh tokens.
Cleanup checklist (operator action after this ADR is accepted)
- Run
tools/scripts/google-oauth-setup.pyfor each affected agent (Atlas first), signed in as the agent email. - Paste
google-oauth-client-id,google-oauth-client-secret,google-oauth-refresh-tokeninto Astra → Agent → Tokens. - Revoke the obsolete
google-sa-keytoken in Astra. (Keepgoogle-agent-email.) - Delete the
atlas-agentservice account from GCP Console → IAM → Service Accounts. - Delete any downloaded SA JSON key files from operator laptops.
- Re-enable the
iam.managed.disableServiceAccountApiKeyCreationorg policy per the parameters captured in drift-2026-04-26 (now superseded). - Mark the WIF drift doc as
superseded(done in the same PR as this ADR).
References
- drift-2026-04-26-google-workload-identity-deferred.md — superseded by this ADR
- V011__restore_google_integration_dwd.sql — last DWD-based registry state
- V012__google_integration_oauth.sql — applies this ADR to the registry
- GoogleOAuthProvider.kt