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

> Learn how the open-seo GA4 connection authenticates users using OAuth 2.0 and discover the essential Google scopes required for seamless integration and data access.

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

---

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

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

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

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

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