Repository navigation
Conversation
A Cloudflare Worker (Hono, jose, D1) in access-site/ that acts as the OIDC provider for Google Workspace's custom OIDC SSO profile, keyed on the numeric GitHub user ID, and lets eligible maintainers create or link their own @modelcontextprotocol.io account. Eligibility comes from the same provisionUser rule Pulumi uses. A nightly job refreshes the account cache and suspends accounts of people who left eligible teams (report-only by default, capped per run, fail-safe on lookup errors). Also: - scripts/backfill-github-ids.ts: stamp existing accounts with GitHub IDs - scripts/sync-team-groups.ts + workflow: mirror GitHub teams as Google groups - src/google.ts: retainOnDelete on Workspace users (suspend, never delete) - src/config/utils.ts: githubTeamsFor, shared by the Worker and the sync - .github/workflows/access-site.yml: test on PR, migrate and deploy on main Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
Pulumi PreviewClick to expand preview output |
Account creations, links, suspensions and the nightly report are now JSON log lines (Workers Logs enabled in wrangler.toml) instead of a D1 audit_log table and an optional Discord webhook. Also declare @types/node in access-site so the tests typecheck in a clean CI checkout without the root node_modules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
…of truth Per Infra WG discussion, account creation and mappings stay infrastructure as code. The Worker now only signs people in: it maps the GitHub login to the googleEmailPrefix in users.ts (same provisionUser rule as Pulumi) and issues the OIDC ID token Google needs. It holds no Google credentials. Removed: self-service account page, Directory API client, D1 account cache, nightly offboarding, backfill script, team-group sync, and the src/google.ts / src/config/utils.ts changes. D1 keeps only short-lived pending requests and single-use codes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
On merge to main, scripts/deploy.sh finds or creates the D1 database,
applies migrations and deploys the Worker, uploading secrets from the
production environment. Non-secret settings (account, GitHub App client
ID, Google redirect URI) are committed in wrangler.toml. The deploy job
skips with a notice until the secrets exist, so merging early is safe.
Replaces the manual generate-keys script; signing key rotation now goes
through the ACCESS_SITE_OIDC_PREVIOUS_JWK secret, and the Worker ignores a
retired "{}" value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
Node: CI on Node 24 (current LTS), engines >=24, @types/node 24. Security review fixes: - Pin GitHub logins to GitHub user IDs on first sign-in; refuse a login that comes back from a different account (freed and re-registered name) - Per-IP rate limit on /authorize and /login (Workers rate limiting binding) so anonymous traffic can't exhaust D1 writes; clean up expired rows on 5% of sign-ins instead of every one - Reject a code_verifier when no PKCE challenge was sent (RFC 9700) - Form-decode Basic client credentials (RFC 6749 2.3.1) - Stamp auth_time at GitHub sign-in, not at token exchange - Revoke the GitHub user token right after reading the user; clear the OAuth state cookie - CI: pin actions to commit SHAs, don't persist Git credentials, expose deploy secrets only to the deploy step (never to npm ci) - Override dev-only undici to the patched 7.30.1 (npm audit: 0) - Test that users.ts has no case-insensitive duplicate logins or prefixes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
Google's custom OIDC profile needs a change password URL; point it at GitHub's password and authentication settings. Replace the 'verify on first deploy' notes with what Google documents: the redirect URI is per-profile (set by PR after setup), and PKCE is handled either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
PILOT_GITHUB_LOGINS in wrangler.toml limits sign-in to the listed GitHub logins; it starts as just "sambhav" (sambhav@modelcontextprotocol.io via users.ts). A test checks every pilot login maps to an account. Remove the line by PR to open sign-in to everyone in users.ts. Setup is reordered so each secret is generated by the person who uses it: the OIDC client secret is created by whoever sets up the Google SSO profile, and goes straight into GitHub secrets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
|
@dsp-ant this is ready for review. Three things need you, all in the Setup → @dsp-ant section of the description:
Sign-in starts as a pilot for me only ( The PR can merge before any of this; the deploy job skips until the secrets exist. Generated by Claude Code |
Automated review (Claude-generated)
Severity key: 🔴 High · 🟠 Medium · 🟡 Low · ⚪ Nit 🔴 High1. 2. Identity is trust-on-first-use of a mutable GitHub login — 🟠 Medium3. GitHub error callback causes a redirect loop — 4. OAuth 5. Key-rotation procedure can break verification for up to the JWKS cache TTL — 6. Deploy gate doesn't require the GitHub client secret — 7. Test fixtures are derived from live 🟡 Low8. 9. Eligibility predicate is re-implemented instead of shared — 10. 11. 12. ⚪ Nit13. Hand-rolled base64url — 14. Generated by Claude Code |
Summary
Maintainers sign in to MCP Google Workspace with GitHub instead of a Google password.
Google stays the IdP for everything. Workspace's built-in "SSO with a third-party IdP" feature hands off only the password step:
App → Google → access.modelcontextprotocol.io → GitHub
access-site/is a small Cloudflare Worker (Hono, jose, D1) that acts as the OIDC provider for Google's custom OIDC SSO profile. It signs the user in with GitHub and returns an ID token whose subject is their numeric GitHub user ID and whose email is their Workspace address.Accounts stay infrastructure as code. The address comes from
users.ts(googleEmailPrefix), using the sameprovisionUserrule Pulumi uses to create the account. The Worker bundlessrc/configand redeploys on every config change, so removing someone fromusers.tsstops their sign-in on the next deploy. It holds no Google credentials and creates nothing in Google. GitHub users without an account get a page explaining how to request one.Provisioning is in GitHub Actions too: on merge to
main,access-site/scripts/deploy.shfinds or creates the D1 database, applies migrations and deploys the Worker with secrets from theproductionenvironment. Non-secret settings are committed inaccess-site/wrangler.toml. Until the secrets exist, the deploy job is skipped with a notice, so this can merge before setup.Security
DELETE ... RETURNING), stored hashed, 2 minute TTL; PKCE enforced when sent and downgrades rejected (RFC 9700); exact redirect URI match; client credentials form-decoded (RFC 6749) and compared in constant time/authorizeand/loginnpm auditreports 0googleEmailPrefixchange can map a GitHub account to a Workspace address, so CODEOWNERS (core maintainers) review is the control thereTesting
node_modulesdeploy.shdry-run against a stubbed Wrangler: creates D1 on first run, reuses it after, uploads only the secrets that are setnpm run validateandnpm testpass; Prettier clean; dry-runwrangler deploybuilds with the rate-limit binding (47 KB gzipped)Rollout
Sign-in starts as a pilot for one person:
PILOT_GITHUB_LOGINS = "sambhav"inwrangler.tomlmeans only @sambhav (sambhav@modelcontextprotocol.io) can get a token, even if the Google SSO profile is assigned more widely. A test checks every pilot login maps to an account. Widening the pilot, or removing the line to open sign-in to everyone inusers.ts, is a PR.Setup
Secrets go straight from the command line into GitHub secrets (needs
opensslandgh), so none pass between people. This PR can merge first: the deploy job skips until the secrets exist.@sambhav
modelcontextprotocol.io. Store asCLOUDFLARE_ACCESS_SITE_TOKENin theproductionenvironment.wrangler.toml, merge, and check https://access.modelcontextprotocol.io/.well-known/openid-configuration.wrangler.toml, then test sign-in.@dsp-ant (org owner and Workspace super admin)
https://access.modelcontextprotocol.iohttps://access.modelcontextprotocol.io/login; leave "Expire user authorization tokens" on; device flow offgh secret set ACCESS_SITE_GITHUB_CLIENT_SECRET --env production -R modelcontextprotocol/accesshttps://access.modelcontextprotocol.iogoogle-workspace; client secret fromoidc-client-secret.txt(delete the file after)https://github.com/settings/securityOnce the pilot works, assign the profile to Model Context Protocol and remove
PILOT_GITHUB_LOGINS.Runbook, including key rotation:
access-site/README.md.https://claude.ai/code/session_01EwaAHtQd625Lwv121Y6PTn
Generated by Claude Code