OAuth Overhaul: Per-Service Connections and Security Fixes
Just wrapped up a marathon session fixing our Google OAuth integration, and honestly, it feels good to have this beast finally tamed.
What Got Done
The big win today was implementing per-service OAuth connections. Instead of just having one "Connect Google" button that tries to grab everything at once, operators can now connect Google Business Profile, GA4, and Google Search Console individually. Each service gets its own clickable badge with proper hover states – much cleaner UX.
I also added a website URL field to client creation (now required) with a "No website" checkbox option. When you check that box, you still need to specify hosting – either on our honeybun servers or external. This prevents those annoying incomplete client records we kept running into.
But the real fun was fixing the OAuth bugs that have been haunting us:
- Refresh token rotation was broken
- Auth key priority was all wrong
- Partial connection reporting was lying to us
Code Review Drama
Ran the code through our parallel review agents and they found some gnarly stuff – 2 CRITICAL issues, 2 HIGH, and 5 MEDIUM. The big ones were missing CSRF protection and blindly trusting OAuth state without re-validation. Classic security holes that could've bitten us hard.
Fixed everything though:
- Added CSRF nonce with 10-minute TTL in KV storage
- Key re-validation in OAuth callbacks
- Proper scope merging instead of replacement
- URL validation that actually works
- Poll timer cleanup (no more interval stacking)
All 15 endpoint tests are passing, and I deployed both the worker and dashboard to production twice to make sure everything stuck.
The Technical Bits
The per-service OAuth uses a SCOPE_MAP that maps service names to Google OAuth scope strings. The worker validates against allowed keys (gbp, ga4, gsc, all) and generates the right scope combinations.
One interesting decision: scope strings now get merged, not replaced. So if you connect GBP first, then add GSC later, you keep both grants instead of losing the first one. Seems obvious in hindsight, but the old code was definitely overwriting everything.
What's Next
Time for the real test – end-to-end OAuth flow with actual Google accounts. The 1-click "Connect Google" has never had a successful full test (embarrassing but true), and the individual service buttons are completely new.
I'm particularly curious to see how the scope merging works in practice. Theory is one thing, but Google's OAuth can be... quirky.
The provision modal also needs a real-world test with the new website/hosting fields. Should be straightforward, but UI surprises are always lurking.
Random Debug Note
Spent way too long confused by curl following 302 redirects by default over HTTP/2. Made it look like OAuth was returning 200 JSON instead of proper redirects. Fixed with --max-redirs 0 but that was a fun 20 minutes of head-scratching.
Also discovered renderCurrentView() didn't actually exist – it was supposed to be render(). Would've caused ReferenceErrors on every successful service connection. Good thing for code reviews, I guess.