How OpenSEO Manages Google OAuth Tokens: A Complete Technical Breakdown

OpenSEO uses a self-hosted OAuth flow with encrypted token storage, signed state parameters for CSRF protection, and automatic refresh via a generic MCP OAuth provider.

The OpenSEO codebase implements a production-ready Google OAuth integration that handles authorization, secure token persistence, and seamless refresh for Google Search Console and Google Analytics 4 APIs. This article examines the complete token management lifecycle based on the every-app/open-seo source code.

Environment-Driven OAuth Configuration

All OAuth credentials are loaded from environment variables to keep secrets out of version control. In src/server/features/google/oauth-config.ts, the getGoogleOAuthClientConfig() function validates that GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and a sufficiently long BETTER_AUTH_SECRET are present via hasSelfHostedGoogleOAuthConfig().

// src/server/features/google/oauth-config.ts
export async function getGoogleOAuthClientConfig() {
  // Reads GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET from env
  // Validates BETTER_AUTH_SECRET length for encryption
}

The BETTER_AUTH_SECRET serves dual purposes: it signs the OAuth state parameter and encrypts tokens at rest when the encryptOAuthTokens flag is enabled.

Generating the Authorization URL with Signed State

The createSelfHostedGoogleAuthorizationUrl() function in src/server/features/google/selfHostedOAuth.ts (lines 86-115) constructs Google's consent URL with several security features:

  • HMAC-SHA-256 signed state encoding userId, callbackPath, and expiry timestamp
  • access_type=offline to request refresh tokens
  • Integration-specific scopes (GSC_OAUTH_SCOPES or GA4_OAUTH_SCOPES)
import { GSC_INTEGRATION, createSelfHostedGoogleAuthorizationUrl } 
  from "@/server/features/google/selfHostedOAuth";

const authUrl = await createSelfHostedGoogleAuthorizationUrl({
  integration: GSC_INTEGRATION,
  user: { userId: "12345", userEmail: "user@example.com" },
  callbackURL: "https://app.example.com/callback",
  publicOrigin: "https://app.example.com",
});

The signed state prevents CSRF attacks and ensures the callback can be matched to the original request.

Handling the OAuth Callback and Token Exchange

When Google redirects back to the application, handleSelfHostedGoogleOAuthCallback() (lines 118-158 in selfHostedOAuth.ts) performs four critical operations:

  1. Validates the returned state signature and expiry
  2. Exchanges the authorization code for tokens via exchangeCode() (POST to https://oauth2.googleapis.com/token)
  3. Persists tokens via upsertGrant()
  4. Handles refresh token logic (preserves existing refresh token if Google omits one in refresh responses)
// src/server/features/google/selfHostedOAuth.ts
export async function handleSelfHostedGoogleOAuthCallbackRequest(
  request: Request, 
  integration: GoogleIntegration
) {
  // State validation, code exchange, token storage
}

Encrypted Token Storage in the Database

The upsertGrant() function manages database persistence with optional encryption. Tokens are stored in the account table with the following security measures:

  • Conditional encryption: symmetricEncrypt() applied when encryptOAuthTokens is enabled
  • Expiry tracking: Access token expiration timestamp recorded
  • Refresh token preservation: Existing refresh token retained if new response lacks one
// Simplified flow from selfHostedOAuth.ts lines 122-154
async function upsertGrant(params: GrantParams) {
  const existingAccount = await findAccount(userId, providerId, googleAccountId);
  
  const accessToken = encryptOAuthTokens 
    ? symmetricEncrypt(newAccessToken, BETTER_AUTH_SECRET)
    : newAccessToken;
    
  // Store with refresh token (re-used if not provided in response)
}

Automatic Token Refresh via MCP OAuth Provider

Token refresh is abstracted through the generic MCP OAuth provider in src/server/mcp/oauth-provider.ts (lines 409-415). This provider:

  • Defines MCP_REFRESH_TOKEN_TTL_SECONDS for refresh token lifecycle management
  • Automatically exchanges refresh tokens for new access tokens before API calls
  • Updates the database with refreshed credentials transparently

Server functions in src/serverFunctions/gsc.ts and src/serverFunctions/ga4.ts retrieve stored tokens via getAuth() helpers. If expired, the OAuth provider refreshes them automatically before attaching to the Authorization: Bearer header.

// Typical usage pattern in server functions
import { getStoredAccessToken } from "@/server/mcp/oauth-provider";

async function callGoogleApi(accountId: string) {
  const accessToken = await getStoredAccessToken(accountId); // Auto-refreshes if needed
  
  return fetch("https://searchconsole.googleapis.com/v1/...", {
    headers: { Authorization: `Bearer ${accessToken}` }
  });
}

Security Architecture Summary

Component Implementation Source File
Configuration Environment variables with validation oauth-config.ts
State security HMAC-SHA-256 signed, expiry-bound selfHostedOAuth.ts
Token encryption symmetricEncrypt() with BETTER_AUTH_SECRET selfHostedOAuth.ts
Refresh automation MCP provider with TTL management oauth-provider.ts
Scope isolation Separate constants for GSC vs GA4 selfHostedOAuth.ts

Summary

  • OpenSEO's Google OAuth token management uses environment-driven configuration with mandatory validation
  • Signed state parameters prevent CSRF and bind flows to specific users
  • Tokens are optionally encrypted at rest using the Better Auth secret
  • The MCP OAuth provider abstracts refresh logic across all Google integrations
  • Server functions receive automatically refreshed tokens without manual intervention

Frequently Asked Questions

How does OpenSEO prevent CSRF attacks during OAuth?

OpenSEO generates an HMAC-SHA-256 signed state parameter in createSelfHostedGoogleAuthorizationUrl() that encodes the user ID, callback path, and expiry timestamp. The handleSelfHostedGoogleOAuthCallback() function validates this signature before processing any tokens, ensuring the callback originated from a legitimate authorization request initiated by the same user.

Where are Google OAuth tokens stored in OpenSEO?

Tokens persist in the account database table via upsertGrant() in src/server/features/google/selfHostedOAuth.ts. The system lookups existing rows by userId, providerId, and Google account ID. When encryptOAuthTokens is enabled, symmetricEncrypt() secures both access and refresh tokens using BETTER_AUTH_SECRET before storage.

How does OpenSEO handle expired access tokens for Google APIs?

The generic MCP OAuth provider in src/server/mcp/oauth-provider.ts manages expiration transparently. It defines MCP_REFRESH_TOKEN_TTL_SECONDS and automatically exchanges the stored refresh token for a new access token when needed. Server functions calling getStoredAccessToken() receive valid credentials without implementing refresh logic themselves.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →