feat(seo): swap GSC auth from OAuth to a service account (shared google-auth.sh) #328
No reviewers
Labels
No labels
Agent
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No assignees
2 participants
Notifications
Total time spent: 46 seconds
Due date
igor
46 seconds
No due date set.
Dependencies
No dependencies set
Reference
joshtronic/igor!328
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/gsc-service-account-auth"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Rights the ship on the SEO auth: the GSC OAuth refresh-token flow was a gap-fill from when the service account couldn't be added to Search Console / Analytics. It can now, so this moves to server-to-server service-account (JWT-bearer) auth — no user consent, no refresh token to babysit. The same credential powers the GA wire-up (#309).
What changed
lib/google-auth.sh(new) —google_sa_access_token <scope>mints a scoped token fromGOOGLE_SERVICE_ACCOUNT(base64 | file path | inline JSON) by signing a JWT with the SA key viaopenssl. Shared by GSC and (next) GA.lib/gsc.sh—gsc_access_tokendelegates togoogle_sa_access_token(scopewebmasters.readonly); dropped the OAuth token exchange.lib/ceo.sh—ceo_read_gscgates onGOOGLE_SERVICE_ACCOUNT.bin/tick.sh— sourcesgoogle-auth.sh; the SEO opt-in gate + env are nowGOOGLE_SERVICE_ACCOUNT(single var replaces the threeGSC_OAUTH_*).bin/gsc-auth.sh(the browser-based OAuth minting tool) andGSC_OAUTH_*from.env.example.Tests
bin/test-google-auth.sh(new) — SA parsing (base64 / file / inline / unset / garbage) and JWT assembly, including verifying the produced signature against the SA public key (a throwaway RSA keypair; curl stubbed, no network).test-ceo.shupdated to the new gate var.check-sync.shgreen.Verified live
Against the real service account: mints a token, lists all 12 GSC domain properties, and a
searchAnalyticsquery returns rows.Env lockstep
GOOGLE_SERVICE_ACCOUNTmust be set on the host — it already is. The now-deadGSC_OAUTH_*can be removed from.envwhenever; nothing reads them anymore.Next
This is item 3 (the swap). The GA client (
lib/ga.sh, samegoogle-auth.sh) + wiring GA data into the SEO reports is #309 — built to use GSC and/or GA per domain, whatever's available.🤖 Generated with Claude Code
🤖 Review —
COMMENT(automated)CI for
85ca362d: successSwaps GSC auth from the OAuth refresh-token flow to service-account JWT-bearer auth via a new shared
lib/google-auth.sh. Focused, well under 400 lines, checklist matches the diff, CI green. One sourcing concern I can't verify from the diff keeps me from a clean approve.Main reservation — transitive sourcing of
google-auth.sh:gsc_access_tokenwas self-contained inlib/gsc.sh. Now it delegates togoogle_sa_access_token(inlib/google-auth.sh). That means every runtime entry point that sourcesgsc.sh(and, transitively,lib/ceo.shviaceo_read_gsc) must now ALSO sourcegoogle-auth.sh.bin/tick.shis correctly updated (new. "$AGENT_HOME/lib/google-auth.sh"beforegsc.sh). But ifceo.sh/gsc.share sourced from any other binary (e.g. a standalone CEO/report runner), a missing source there would makegoogle_sa_access_tokena "command not found" at runtime →gsc_access_tokenreturns non-zero → silently degrades to the "token mint failed / not configured" path. It wouldn't crash, so it would go unnoticed.test-ceo.shstubsgsc_access_token, so the delegation is never exercised there. Please confirm no other entry point sourcesgsc.sh/ceo.shwithout also picking upgoogle-auth.sh.Things that look right:
lib/google-auth.shsigning path is sound: JWT header/claims are base64url'd, signed withopenssl dgst -sha256 -sign <(...)(key via process substitution, never hits disk or logs), and--data-urlencode assertion=...is safe.google_sa_jsonresolution order (file path → valid-inline-JSON → base64) is reasonable and returns rc=1 on unset/garbage.jq -r '.private_key'correctly unescapes the JSON\ninto real PEM newlines..env.example,CLAUDE.md,docs/architecture.md, and thegsc-auth.shremoval are all consistent with the single-var (GOOGLE_SERVICE_ACCOUNT) model; the old threeGSC_OAUTH_*are fully purged from the touched files.Test coverage — strong:
test-google-auth.shcovers all four SA-parsing shapes plus garbage, and crucially verifies the produced JWT signature against the SA public key (real RS256), header alg, all claims, andexp > iat, with curl stubbed (no network). Skip-safe on missing deps. Nicely done.No security or correctness defects found in the code itself — just want a human to confirm the sourcing is complete across all entry points before merge.
Independent review by the harness on
claude-opus-4-8(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.