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=offlineto request refresh tokens- Integration-specific scopes (
GSC_OAUTH_SCOPESorGA4_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:
- Validates the returned state signature and expiry
- Exchanges the authorization code for tokens via
exchangeCode()(POST tohttps://oauth2.googleapis.com/token) - Persists tokens via
upsertGrant() - 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 whenencryptOAuthTokensis 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_SECONDSfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →