# How OpenWork Handles OAuth Authentication with Google Workspace: A Complete Technical Guide

> Discover how OpenWork handles Google Workspace OAuth authentication using a secure Authorization Code flow with PKCE. Explore its three-layer architecture for provider config, flow orchestration, and token management.

- Repository: [Different AI/openwork](https://github.com/different-ai/openwork)
- Tags: how-to-guide
- Published: 2026-08-01

---

**OpenWork implements Google Workspace OAuth authentication using a secure, reusable OAuth 2.0 Authorization Code flow with PKCE, centralized in a three-layer architecture that separates provider configuration, flow orchestration, and token management.**

OpenWork connects to Google Workspace APIs (Gmail, Calendar, Drive) through a robust OAuth 2.0 implementation. This article breaks down exactly how the authentication flow works, from provider registration to token refresh, based on the actual source code from the `different-ai/openwork` repository.

## OAuth 2.0 Authorization Code Flow Overview

OpenWork uses the **classic OAuth 2.0 Authorization Code flow** with PKCE (Proof Key for Code Exchange) for enhanced security. The entire flow is abstracted by the **native-OAuth provider registry**, allowing new providers to be added without changing route code.

The implementation spans three main layers:

- **Provider registry** — defines Google Workspace endpoints, scopes, and features
- **OAuth flow implementation** — handles connection start, callback, and token exchange
- **Token retrieval and refresh** — manages access tokens, automatic refresh, and scope validation

## Step 1: Provider Registration in NATIVE_OAUTH_PROVIDERS

Google Workspace is declared as a native OAuth provider in [[`provider-registry.ts`](https://github.com/different-ai/openwork/blob/main/provider-registry.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/capability-sources/provider-registry.ts#L19-L55). This configuration centralizes all provider-specific settings.

```typescript
// ee/apps/den-api/src/capability-sources/provider-registry.ts
export const NATIVE_OAUTH_PROVIDERS: Record<string, NativeOAuthProviderConfig> = {
  "google-workspace": {
    providerId: "google-workspace",
    displayName: "Google Workspace",
    authorizeUrl: env.googleOAuthAuthorizeUrl ?? "https://accounts.google.com/o/oauth2/v2/auth",
    tokenUrl: env.googleOAuthTokenUrl ?? "https://oauth2.googleapis.com/token",
    websiteUrl: "https://workspace.google.com",
    defaultScopes: [
      "openid",
      "https://www.googleapis.com/auth/userinfo.email",
      "https://www.googleapis.com/auth/userinfo.profile",
    ],
    defaultFeatures: ["calendarRead", "gmailDraft", "driveFile"],
    optionalFeatures: {
      calendarRead: ["https://www.googleapis.com/auth/calendar.readonly"],
      calendarWrite: ["https://www.googleapis.com/auth/calendar.events"],
      // …other optional scopes…
    },
    usesPkce: true,
    extraAuthorizeParams: { access_type: "offline", prompt: "consent" },
  },
  // …other providers…
};

```

Key configuration elements include:

- **`usesPkce: true`** — enables PKCE for secure public client flows
- **`extraAuthorizeParams`** — requests offline access (`access_type: "offline"`) and forces consent prompt to ensure refresh tokens are issued
- **`defaultFeatures`** and **`optionalFeatures`** — defines granular scope groups that admins can enable per OAuth client

## Step 2: OAuth Client Configuration

Before users can connect, an organization admin must configure an OAuth client. This is done via the `/v1/oauth-providers/:providerId/client` endpoint, implemented in `registerOAuthProviderRoutes` in [[`oauth-providers.ts`](https://github.com/different-ai/openwork/blob/main/oauth-providers.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/routes/org/oauth-providers.ts).

The client record (containing client ID and secret) is stored in the `org_oauth_clients` table and associated with the organization's Google Cloud project.

## Step 3: Initiating the Connection Flow

When a member clicks **Connect** in the desktop app, the flow begins at `GET /v1/oauth-providers/google-workspace/connect/start`. The `beginNativeProviderConnect` function handles this:

```typescript
// ee/apps/den-api/src/routes/org/oauth-providers.ts
async function beginNativeProviderConnect(input: OrgIds & {
  provider: NativeOAuthProviderConfig;
  request: Request;
}): Promise<{ authorizeUrl: string } | { error: string; message?: string }> {
  const client = await getOrgOAuthClient(input.organizationId, input.provider.providerId);
  if (!client) return { error: "client_not_configured" };

  const { verifier, challenge } = createPkcePair();
  const state = createOAuthStateToken({
    organizationId: input.organizationId,
    orgMembershipId: input.orgMembershipId,
    providerId: input.provider.providerId,
    secret: env.betterAuthSecret,
  });

  const authorizeUrl = buildAuthorizeUrl({
    provider: input.provider,
    client,
    state,
    redirectUri: callbackRedirectUri(input.request, input.provider.providerId),
    codeChallenge: challenge,
  });

  await upsertConnectedAccount({
    organizationId: input.organizationId,
    orgMembershipId: input.orgMembershipId,
    providerId: input.provider.providerId,
    pendingCodeVerifier: verifier,
  });
  return { authorizeUrl };
}

```

This function:

1. Verifies the organization has configured an OAuth client
2. Generates a **PKCE verifier and challenge** using `createPkcePair()`
3. Creates a **signed state token** with `createOAuthStateToken()` to prevent CSRF attacks
4. Builds the Google authorization URL via `buildAuthorizeUrl()`
5. Stores the PKCE verifier in a pending `connected_accounts` row

The desktop app then redirects the user's browser to the returned `authorizeUrl`.

## Step 4: Handling the OAuth Callback

Google redirects the user to `GET /v1/oauth-providers/google-workspace/connect/callback` after authorization. The callback handler:

1. Validates the signed state token with `verifyOAuthStateToken()`
2. Retrieves the saved OAuth client and pending PKCE verifier
3. Exchanges the authorization code for access and refresh tokens

```typescript
// ee/apps/den-api/src/routes/org/oauth-providers.ts (callback handler)
const tokens = await exchangeCodeForTokens({
  provider,
  client,
  code,
  redirectUri: callbackRedirectUri(c.req.raw, providerId),
  codeVerifier: pending.pendingCodeVerifier,
});

await completeConnectedAccountForActiveMember({
  organizationId: statePayload.organizationId,
  orgMembershipId: statePayload.orgMembershipId,
  providerId,
  expectedAccountId: pending.id,
  expectedPendingCodeVerifier: pending.pendingCodeVerifier,
  accessToken: tokens.access_token,
  refreshToken: tokens.refresh_token ?? null,
  tokenType: tokens.token_type ?? null,
  expiresAt: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000) : null,
  scopes: tokens.scope
    ? tokens.scope.split(" ")
    : resolveProviderScopes(provider, clientSelectedFeatures(provider, client.extra)),
  pendingCodeVerifier: null,
});

```

The `exchangeCodeForTokens` function in [[`generic-oauth.ts`](https://github.com/different-ai/openwork/blob/main/generic-oauth.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/capability-sources/generic-oauth.ts) performs the POST to Google's token endpoint. Upon success, `completeConnectedAccountForActiveMember` persists all token data to the database.

## Step 5: Token Usage and Automatic Refresh

Capability routes (Gmail, Calendar, Drive) obtain valid access tokens through the `googleWorkspaceToken` helper in [[`google-workspace.ts`](https://github.com/different-ai/openwork/blob/main/google-workspace.ts)](https://github.com/different-ai/openwork/blob/dev/ee/apps/den-api/src/routes/org/google-workspace.ts):

```typescript
// ee/apps/den-api/src/routes/org/google-workspace.ts
async function googleWorkspaceToken(input: {
  organizationId: DenTypeId<"organization">;
  orgMembershipId: DenTypeId<"member">;
}): Promise<GoogleWorkspaceAccessToken> {
  const provider = getNativeOAuthProvider("google-workspace");
  if (!provider) return { kind: "google_api_error", message: "google-workspace provider is not registered." };

  const token = await getValidAccessToken({
    provider,
    organizationId: input.organizationId,
    orgMembershipId: input.orgMembershipId,
  });

  if ("error" in token) {
    return { kind: "needs_connection", message: CONNECT_GOOGLE_ACCOUNT_MESSAGE };
  }
  return { kind: "ok", accessToken: token.accessToken, account: token.account };
}

```

The `getValidAccessToken` function in [`generic-oauth.ts`](https://github.com/different-ai/openwork/blob/main/generic-oauth.ts) handles:

- Reading the stored access token from the database
- **Automatic token refresh** if expiration is near, using `refreshTokens()`
- Returning either valid credentials or a `needs_connection` error

## Step 6: Scope Enforcement

Each capability validates required scopes before calling Google APIs:

```typescript
if (missingScope(token.account, [GMAIL_READ_SCOPE])) {
  return c.json(
    { error: "needs_connection", message: missingPermissionMessage("Gmail read") },
    409,
  );
}

```

The `missingScope` helper checks the granted scopes stored in the connected account record. If scopes are missing, the route returns a `409 Conflict` with a user-friendly message directing the admin to enable the required permission feature.

## Token Lifecycle: Refresh and Revocation

OpenWork implements several safeguards for credential management:

| Aspect | Implementation |
|--------|---------------|
| **Automatic refresh** | `getValidAccessToken` refreshes tokens on demand when near expiry |
| **Token persistence** | Refresh tokens and expiry times stored in `connected_accounts` table |
| **Revocation on client change** | `revokeAccountsBeforeOAuthClientIdentityChange` invalidates existing connections when OAuth client or tenant ID changes |
| **CAS updates** | Compare-and-set style updates in [`oauth-credentials.ts`](https://github.com/different-ai/openwork/blob/main/oauth-credentials.ts) prevent race conditions during concurrent token refreshes |

## Architecture Benefits: Adding New Providers

This layered design enables rapid provider expansion. To add a new native OAuth provider (Microsoft 365, Slack, etc.), you only need to:

1. Add an entry to `NATIVE_OAUTH_PROVIDERS` in [`provider-registry.ts`](https://github.com/different-ai/openwork/blob/main/provider-registry.ts)
2. Create capability-specific route files that call `getValidAccessToken`

The route handlers in [`oauth-providers.ts`](https://github.com/different-ai/openwork/blob/main/oauth-providers.ts) automatically work with any registered provider—no code changes required.

## Summary

- OpenWork's Google Workspace OAuth uses **Authorization Code flow with PKCE** for maximum security
- Provider configuration is centralized in **`NATIVE_OAUTH_PROVIDERS`** in [`provider-registry.ts`](https://github.com/different-ai/openwork/blob/main/provider-registry.ts)
- The **[`oauth-providers.ts`](https://github.com/different-ai/openwork/blob/main/oauth-providers.ts)** route file handles all provider-agnostic flow logic: client storage, PKCE generation, state tokens, callback processing, and token persistence
- Core OAuth utilities in **[`generic-oauth.ts`](https://github.com/different-ai/openwork/blob/main/generic-oauth.ts)** manage PKCE, state tokens, URL building, token exchange, and automatic refresh
- Capability routes in **[`google-workspace.ts`](https://github.com/different-ai/openwork/blob/main/google-workspace.ts)** use `googleWorkspaceToken()` for validated, automatically-refreshed access tokens with mandatory scope checks
- The architecture separates concerns cleanly, allowing new providers to be added by configuration alone

## Frequently Asked Questions

### What OAuth flow does OpenWork use for Google Workspace?

OpenWork uses the **OAuth 2.0 Authorization Code flow with PKCE**. This is implemented through the `beginNativeProviderConnect` function, which generates a PKCE verifier/challenge pair and includes the code challenge in the authorization URL. PKCE provides additional security for public clients like desktop applications.

### How does OpenWork handle token refresh automatically?

Token refresh is handled transparently by `getValidAccessToken` in [`generic-oauth.ts`](https://github.com/different-ai/openwork/blob/main/generic-oauth.ts). When a capability route requests a token, this function checks the expiration timestamp. If the token is near expiry, it calls `refreshTokens` to exchange the stored refresh token for new access and refresh tokens, then persists the updated credentials before returning them to the caller.

### Where are OAuth client credentials stored in OpenWork?

OAuth client credentials (client ID and secret) are stored in the **`org_oauth_clients`** database table. They are configured by organization admins through the `/v1/oauth-providers/:providerId/client` endpoint. Access tokens and refresh tokens for individual users are stored in the **`connected_accounts`** table.

### What happens when required scopes are missing for a Google Workspace capability?

Each capability route calls `missingScope()` to validate granted scopes before API calls. If required scopes are missing, the route returns a **`409 Conflict`** response with `error: "needs_connection"` and a message prompting the admin to enable the specific permission feature in the OAuth client configuration. This enables graceful degradation and clear user guidance.