How the open-seo GA4 Connection Authenticates Users and Required Google Scopes

The open-seo GA4 connection authenticates users via a standard OAuth 2.0 authorization code flow using the better-auth library, requiring four specific Google scopes: openid, email, profile, and https://www.googleapis.com/auth/analytics.readonly.

The open-seo repository implements a secure Google Analytics 4 (GA4) integration that enables users to link their analytics accounts through a self-hosted OAuth flow. Understanding how this GA4 connection authenticates users and which Google scopes are required is essential for developers extending the platform or deploying custom instances. The implementation leverages the better-auth library's genericOAuth provider alongside custom state management and token encryption mechanisms.

OAuth 2.0 Flow Architecture

The GA4 connection relies on a standard OAuth 2.0 authorization code flow with PKCE (Proof Key for Code Exchange) enabled. This architecture ensures secure token exchange between open-seo and Google's identity platform while maintaining compatibility with self-hosted deployments.

Key security features implemented in the flow include:

  • Encrypted state tokens to prevent CSRF attacks
  • Offline access type for refresh token persistence
  • Account-specific token storage with symmetric encryption

Required Google OAuth Scopes

The GA4 integration requires four specific OAuth scopes defined in src/shared/ga4.ts. These scopes grant the application the minimum necessary permissions to identify users and access their analytics data.

Identity and Profile Scopes

  • openid – Requests an ID token for user identity verification
  • email – Grants access to the user's email address for account linking
  • profile – Retrieves basic profile information from the Google account

Analytics Data Scope

  • https://www.googleapis.com/auth/analytics.readonly – Provides read-only access to Google Analytics 4 data, including property listings and reporting metrics
// src/shared/ga4.ts
export const GA4_OAUTH_SCOPES = [
  "openid",
  "email",
  "profile",
  "https://www.googleapis.com/auth/analytics.readonly",
] as const;

export const GA4_OAUTH_PROVIDER_ID = "google-analytics" as const;

Provider Configuration with better-auth

The GA4 provider registration occurs in src/lib/auth-config.ts using the genericOAuth plugin from better-auth. This configuration shares the same OAuth credentials as the Google Search Console integration but maintains a distinct provider ID for GA4-specific flows.

// src/lib/auth-config.ts
{
  providerId: GA4_OAUTH_PROVIDER_ID,  // "google-analytics"
  clientId: env.GOOGLE_CLIENT_ID?.trim() ?? "",
  clientSecret: env.GOOGLE_CLIENT_SECRET?.trim() ?? "",
  discoveryUrl: "https://accounts.google.com/.well-known/openid-configuration",
  scopes: [...GA4_OAUTH_SCOPES],
  accessType: "offline",
  prompt: "select_account consent",
  pkce: true,
}

The accessType: "offline" parameter ensures the authorization returns a refresh token, enabling long-term API access without requiring users to re-authenticate. The prompt parameter forces account selection and consent screens to guarantee users explicitly authorize the requested scopes.

Self-Hosted OAuth Implementation

When operating in self-hosted mode, the GA4 connection follows the implementation pattern defined in src/server/features/google/selfHostedOAuth.ts. This module handles the complete lifecycle from authorization URL generation to persistent token storage.

Authorization URL Generation

The createSelfHostedGoogleAuthorizationUrl function constructs the Google consent URL by embedding the GA4 scopes and a cryptographically secure state token:

import { GA4_INTEGRATION, createSelfHostedGoogleAuthorizationUrl } from "@/server/features/google/selfHostedOAuth";

const authUrl = await createSelfHostedGoogleAuthorizationUrl({
  integration: GA4_INTEGRATION,
  user: { userId: "user-123", userEmail: "user@example.com" },
  callbackURL: "https://myapp.com/dashboard",
  publicOrigin: "https://myapp.com",
});
// Redirect the user to `authUrl`

State Handling and CSRF Protection

The implementation uses createState and verifyState functions to generate signed, encrypted state tokens. These tokens protect against cross-site request forgery (CSRF) and replay attacks by binding the authorization request to the specific user session.

Token Exchange and Persistence

After Google redirects the user back to the application, the handleSelfHostedGoogleOAuthCallbackRequest function processes the callback:

  1. Code Exchange – The exchangeCode function posts the authorization code to https://oauth2.googleapis.com/token and validates the response against googleTokenResponseSchema
  2. Account Identification – The system extracts the Google account ID from the ID token using getGoogleAccountId
  3. Encrypted Storage – The upsertGrant function stores access and refresh tokens using symmetricEncrypt, linking them to the open-seo user record in the account table (defined in src/db/schema.ts)
import { handleSelfHostedGoogleOAuthCallbackRequest } from "@/server/features/google/selfHostedOAuth";

// In a Cloudflare Worker route:
export async function onRequest(context) {
  return await handleSelfHostedGoogleOAuthCallbackRequest(
    context.request,
    GA4_INTEGRATION,
  );
}

Making Authenticated API Calls

Once authenticated, the system retrieves stored credentials to call Google Analytics APIs via the client implemented in src/server/lib/ga4Client.ts. The client uses the refresh token to obtain fresh access tokens automatically, ensuring uninterrupted access to GA4 reporting data without user intervention.

Summary

  • The open-seo GA4 connection uses a standard OAuth 2.0 authorization code flow with better-auth orchestrating the authentication
  • Four Google scopes are required: openid, email, profile, and https://www.googleapis.com/auth/analytics.readonly
  • Provider configuration resides in src/lib/auth-config.ts, using genericOAuth with offline access and PKCE enabled
  • The self-hosted flow implementation in src/server/features/google/selfHostedOAuth.ts handles URL generation, state encryption, and token persistence
  • Tokens are symmetrically encrypted at rest and linked to user records via the account table schema

Frequently Asked Questions

What authentication protocol does the open-seo GA4 connection use?

The open-seo GA4 connection uses OAuth 2.0 with the authorization code grant type. The implementation includes PKCE (Proof Key for Code Exchange) for enhanced security and uses the better-auth library's genericOAuth provider to manage the flow with Google's identity platform.

Why does the GA4 integration need the openid scope?

The openid scope requests an ID token from Google, which contains the user's unique Google account identifier. open-seo uses this ID token to extract the account ID via getGoogleAccountId, ensuring the OAuth grant is correctly linked to the right user identity in the database.

How are refresh tokens secured in the self-hosted implementation?

Refresh tokens are encrypted using symmetric encryption via the symmetricEncrypt function before storage in the database. The upsertGrant function in src/server/features/google/selfHostedOAuth.ts handles this encryption when persisting tokens to the account table, ensuring credentials remain secure even if the database is compromised.

Can I use different OAuth credentials for GA4 and Google Search Console?

While the current implementation in src/lib/auth-config.ts shares the same GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables for both integrations, the architecture supports distinct credentials. Each integration uses a unique providerId ("google-analytics" for GA4), allowing you to configure separate OAuth applications in Google Cloud Console if needed.

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 →