Skip to content

Google OAuth Provider

Implements OAuthAuthorizationServerProvider from the MCP SDK, delegating identity verification to Google while owning the full OAuth 2.1 state machine.

Overview

The provider handles:

  • Dynamic Client Registration — MCP clients register themselves on first connection.
  • Authorization — Redirects clients to Google's authorization endpoint; stores pending auth state in GCS.
  • Google callback — Exchanges the Google code for a userinfo token, verifies the email, and mints an MCP authorization code.
  • Token exchange — Exchanges MCP authorization codes for access/refresh token pairs.
  • Token refresh — Issues new access tokens from valid refresh tokens.
  • Token revocation — Removes both the access token and all associated refresh tokens (or vice versa).

State persistence

All OAuth state is stored in a single JSON blob at oauth-state/state.json in the GCS bucket configured by OAUTH_STATE_BUCKET. The blob is read at startup and written synchronously after every mutation. This ensures sessions survive Cloud Run container restarts and scale-to-zero events.

The blob contains:

{
  "clients": { "<client_id>": { ... } },
  "auth_codes": { "<code>": { ... } },
  "access_tokens": { "<token>": { ... } },
  "refresh_tokens": { "<token>": { ... } },
  "pending_auths": { "<google_state>": { ... } }
}

Expired tokens are filtered out on load and never written back.

Token lifetimes

Token TTL
Access token 3,600 s (1 hour)
Refresh token 2,592,000 s (30 days)
Authorization code 300 s (5 minutes)
Pending auth state 600 s (10 minutes)

Module reference

the_curator.auth.provider

Google OAuth 2.0 delegation provider for the MCP authorization server.

Implements OAuthAuthorizationServerProvider so the MCP SDK handles all protocol-level OAuth 2.1 machinery. Identity verification is delegated to Google; only the configured allowed_email can authenticate.

State is persisted to a GCS blob so tokens survive Cloud Run restarts.

logger = logging.getLogger(__name__) module-attribute

GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth' module-attribute

GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token' module-attribute

GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v3/userinfo' module-attribute

ACCESS_TOKEN_TTL = 3600 module-attribute

REFRESH_TOKEN_TTL = 86400 * 30 module-attribute

AUTH_CODE_TTL = 300 module-attribute

PENDING_AUTH_TTL = 600 module-attribute

STATE_BLOB_NAME = 'oauth-state/state.json' module-attribute

GoogleOAuthProvider

OAuth provider that delegates identity verification to Google.

Single-user: only allowed_email can authenticate. All OAuth state is kept in memory and persisted to GCS after every mutation so container restarts don't invalidate active sessions.

__init__(google_client_id: str, google_client_secret: str, allowed_email: str, server_url: str, gcs_bucket: str) -> None

Configure Google OAuth credentials the single allowed email, and the GCS state bucket.

get_client(client_id: str) -> OAuthClientInformationFull | None async

Return the registered client for client_id, or None if not found.

register_client(client_info: OAuthClientInformationFull) -> None async

Register a new OAuth client and persist state to GCS.

authorize(client: OAuthClientInformationFull, params: AuthorizationParams) -> str async

Begin the Google OAuth flow; return the Google authorization URL.

handle_google_callback(code: str, state: str) -> str async

Exchange Google code for identity, verify email, mint MCP auth code.

Returns the redirect URL to send the browser back to the MCP client. Raises ValueError if state is invalid or the user is not authorized.

load_authorization_code(client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None async

Return the auth code if it exists and belongs to the requesting client.

exchange_authorization_code(client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken async

Consume an auth code and issue a new access + refresh token pair.

load_refresh_token(client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None async

Return the refresh token if valid, unexpired, and owned by the requesting client.

exchange_refresh_token(client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken async

Rotate the refresh token and issue a new access + refresh token pair.

load_access_token(token: str) -> AccessToken | None async

Return the access token if it exists and has not expired.

revoke_token(token: AccessToken | RefreshToken) -> None async

Revoke the given token and all paired tokens for the same client.