← back to George Gmail
OAUTH-SERVICE-ACCOUNT-RUNBOOK.md
144 lines
# George OAuth — permanent fix for the Workspace accounts (service account + domain-wide delegation)
**Goal:** kill the ~7-day token treadmill **forever** for the two Google **Workspace** accounts
`steve@designerwallcoverings.com` and `info@designerwallcoverings.com`. No refresh token, no
consent screen, no CASA verification, $0.
**Does NOT apply to** `steveabramsdesigns@gmail.com` / `theagentabrams@gmail.com` — those are
**consumer** Gmail, which a service account cannot impersonate. Their only options stay:
periodic re-consent, or drop them from George.
**Split of labor:** the Google Cloud Console + Workspace Admin steps are **yours** (identity /
admin — I can't touch them). The George code changes are **mine** (additive, reversible, off
until the key is present). Nothing breaks the current refresh-token path until you flip it.
---
## ⭐ FASTEST FIX (do this first) — flip the OAuth app to "Internal" (YOU · ~1 min, ZERO code)
There is **no "expire in 1 year" setting** in Google OAuth. Refresh-token lifetime is set by the
app's **publishing status**: **Testing = hard 7-day cap**; **Internal = long-lived** (lasts until
revoked / ~6 months unused — effectively a year+). To stop the 7-day treadmill for the two
Workspace accounts, just change the status:
> **Cloud Console → APIs & Services → OAuth consent screen → User Type → change to `Internal` → Save.**
- Works only if that Cloud project lives in the **designerwallcoverings.com Workspace org** (it does —
steve@/info@ authorize against it).
- Internal apps **skip Google verification / CASA entirely** AND their refresh tokens **don't hit the
7-day cap**. Zero code change. Then re-consent steve@ + info@ once (`/auth/steve-office`, `/auth/info`)
to mint the now-long-lived tokens.
- **Result:** steve@ + info@ stop expiring — the whole fleet send path + the info@ catch-all, solved
in ~1 minute.
### ⚠️ The fork this forces (unavoidable)
Flipping to **Internal BREAKS the two consumer Gmail accounts** — `steveabramsdesigns@gmail.com` and
`theagentabrams@gmail.com` **cannot authorize an Internal app**. Google does not allow a long-lived,
unverified token for consumer accounts on restricted scopes (gmail.modify / drive). So for those two,
pick one:
1. **Own separate OAuth client, published to Production** → triggers Google verification + CASA
(weeks, paid) — the trap. Not recommended just for two mailboxes.
2. **Stay on the current Testing client** → 7-day re-consent forever (a periodic click).
3. **Drop them from George** → cleanest if they aren't doing real work. (Remove their entries from the
ACCOUNTS map in `server.js` + their `*_REFRESH_TOKEN` from `.env`; I can do this local edit on your go.)
**Recommended:** Internal flip for steve@ + info@ **now** (fastest, permanent, free). Decide 1/2/3 for the
two consumer accounts separately — and if you don't actively use them through George, **#3 (drop)** ends
this chore entirely.
The service-account approach below (PART 1–5) is the **alternative** durable fix for the Workspace pair —
use it instead of the Internal flip only if you specifically want token-less per-request minting (e.g. you
can't set the consent screen to Internal). For most cases, the Internal flip above is simpler.
---
## Why this works (30 seconds)
A service account (SA) with **domain-wide delegation** is authorized, once, in your Workspace
Admin console to impersonate users in `designerwallcoverings.com` for a fixed scope list. George
then mints a short-lived access token per request by signing a JWT with the SA key + a `subject`
(the user to act as). Google issues the token directly — there is no user refresh token to expire,
no "Testing" 7-day cap, and no OAuth consent screen. Internal to your own Workspace, so **no app
verification / no CASA**.
---
## PART 1 — Google Cloud Console (YOU · ~5 min)
Use the **same Cloud project** that holds the current `GMAIL_CLIENT_ID`
(`493480746821-…apps.googleusercontent.com`).
1. **APIs & Services → Enable APIs** — confirm these are enabled (they already are for OAuth, but verify):
Gmail API, Google Calendar API, Google Drive API, Google Docs API, Google Sheets API,
Google Slides API, Google Tasks API, Google Forms API.
2. **IAM & Admin → Service Accounts → Create service account**
- Name: `george-workspace-impersonation`
- Skip the optional roles (delegation is granted in Admin, not via IAM roles). Create.
3. Open the new SA → **Keys → Add key → Create new key → JSON**. A `.json` downloads. Keep it safe.
4. Open the SA's details and **copy its numeric Client ID** ("Unique ID", ~21 digits) — you need it in Part 2.
(Also confirm **Domain-wide delegation** is enabled on the SA; on newer consoles it's implicit once
you authorize the client in Admin in Part 2.)
## PART 2 — Workspace Admin console (YOU · ~3 min)
`admin.google.com` → **Security → Access and data control → API controls → Domain-wide delegation → Add new**
- **Client ID:** the SA numeric Client ID from Part 1 step 4.
- **OAuth scopes** (comma-separated — these are the exact 11 George uses):
```
https://www.googleapis.com/auth/gmail.modify,
https://www.googleapis.com/auth/gmail.settings.sharing,
https://www.googleapis.com/auth/gmail.settings.basic,
https://www.googleapis.com/auth/tasks,
https://www.googleapis.com/auth/drive,
https://www.googleapis.com/auth/documents,
https://www.googleapis.com/auth/spreadsheets,
https://www.googleapis.com/auth/presentations,
https://www.googleapis.com/auth/calendar,
https://www.googleapis.com/auth/forms.body,
https://www.googleapis.com/auth/forms.responses.readonly
```
- **Authorize.** (Propagation is usually instant, occasionally up to ~30 min.)
## PART 3 — hand me the key (YOU → ME)
Put the SA JSON key somewhere George can read it and tell me the path, e.g.:
```
~/Projects/george-gmail/secrets/george-sa.json (chmod 600; add secrets/ to .gitignore)
```
Route it via the `secrets` skill if you prefer — do **not** paste the key contents in chat.
## PART 4 — George code (ME · additive, reversible)
Once the key exists I add an SA auth path that is used **only** for `steve-office` + `info` when
`GEORGE_SA_KEY` is set, and otherwise falls back to today's refresh-token client (zero behavior
change until you flip it). Sketch:
```js
// lib/sa-auth.js (new file — does not touch the existing OAuth2 clients)
import { google } from 'googleapis';
const SA_KEY = process.env.GEORGE_SA_KEY; // path to the JSON key
const SCOPES = [/* the 11 scopes above */];
export function saClientFor(subject) { // subject = user email to impersonate
const auth = new google.auth.GoogleAuth({ keyFile: SA_KEY, scopes: SCOPES, clientOptions: { subject } });
return auth; // google.gmail({version:'v1', auth}) etc.
}
```
Wiring in `server.js`: for the `steve-office` and `info` account entries, if `GEORGE_SA_KEY` is set,
build their gmail/calendar/drive/etc. clients from `saClientFor('steve@…')` / `saClientFor('info@…')`
instead of the refresh-token `oauth2Client`. Consumer accounts are untouched.
## PART 5 — roll out + verify (ME with your go)
1. `GEORGE_SA_KEY=<path>` in `.env`; restart George (local) — then Kamatera (your deploy).
2. Verify: `GET /api/profile?account=steve-office` and `?account=info` return OK with **no refresh token in play**.
3. Let it sit past 7 days — the `token-age-warn` CRIT for those two never fires again.
4. Retire the now-pointless watchers for the Workspace accounts:
`com.steve.dw-appointments-oauth-canary` (it watched calendar-push death caused by this exact expiry).
---
## After this
- **steve@ + info@** → never expire again. This is the fleet-critical win (send path + info@ catch-all across 209 domains).
- **steve-personal + agentabrams** → still consumer Gmail; still a periodic re-consent (or drop them). No service-account path exists for consumer accounts — that's Google's boundary, not a gap in this plan.
## Provenance
Extends `PLAN-OAUTH-DURABILITY.md` (2026-07-01, council idea #3) into an executable runbook. The
"just publish to Production" path is a trap for restricted Gmail/Drive scopes (annual CASA
assessment) — domain-wide delegation avoids verification entirely because it's internal to your
own Workspace.