# How OpenSEO Manages Google OAuth Tokens: A Complete Technical Breakdown

> Discover how OpenSEO manages Google OAuth tokens with a self-hosted flow, encrypted storage, CSRF protection, and automatic refresh. Learn the technical details.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-09-06

---

**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`](https://github.com/every-app/open-seo/blob/main/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()`.

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/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`)

```typescript
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`](https://github.com/every-app/open-seo/blob/main/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)

```typescript
// 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

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/gsc.ts) and [`src/serverFunctions/ga4.ts`](https://github.com/every-app/open-seo/blob/main/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.

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/oauth-config.ts) |
| State security | HMAC-SHA-256 signed, expiry-bound | [`selfHostedOAuth.ts`](https://github.com/every-app/open-seo/blob/main/selfHostedOAuth.ts) |
| Token encryption | `symmetricEncrypt()` with `BETTER_AUTH_SECRET` | [`selfHostedOAuth.ts`](https://github.com/every-app/open-seo/blob/main/selfHostedOAuth.ts) |
| Refresh automation | MCP provider with TTL management | [`oauth-provider.ts`](https://github.com/every-app/open-seo/blob/main/oauth-provider.ts) |
| Scope isolation | Separate constants for GSC vs GA4 | [`selfHostedOAuth.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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.