> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corsair.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth 2.0 Authentication

> Connect user accounts with OAuth 2.0 flows in Corsair plugins.

OAuth 2.0 lets users authorize your application to act on their behalf. Corsair handles the entire flow — minting connect links, processing callbacks, storing tokens encrypted, and refreshing them automatically when they expire.

## How it works

1. You register an OAuth app with the service and get a `client_id` and `client_secret`
2. Your app calls `client.connect.createLink()` and redirects the user to the returned URL
3. After the user approves, the service redirects back with an authorization code
4. Corsair exchanges the code for access and refresh tokens and stores them encrypted
5. On every API call, Corsair checks token expiry and refreshes automatically

Corsair supports two connect modes — **`hub`** (Corsair hosts the UI) or **`manual`** (you host connect pages). Both use the same `createLink` API. See [Connect / OAuth](/management/connect) for the full reference.

```ts corsair.ts theme={null}
import { createCorsair } from "corsair";
import { gmail } from "@corsair-dev/gmail";

export const corsair = createCorsair({
    plugins: [gmail({ authType: "oauth_2" })],
    kek: process.env.CORSAIR_KEK!,
    database: db,
});
```

***

## Solo setup

Solo mode connects a single account to your application. Use this for scripts, internal tools, or apps that only ever connect one account.

```ts corsair.ts theme={null}
export const corsair = createCorsair({
    plugins: [gmail({ authType: "oauth_2" })],
    kek: process.env.CORSAIR_KEK!,
    database: db,
});
```

Store your OAuth app credentials, then start the flow:

```bash theme={null}
pnpm corsair setup --plugin=gmail client_id=your-client-id client_secret=your-client-secret
pnpm corsair auth --plugin=gmail
```

The CLI prints an authorization URL. Open it in a browser, approve, and tokens are stored automatically.

After that, all API calls use your connected account:

```ts usage.ts theme={null}
const messages = await corsair.gmail.api.messages.list({ maxResults: 10 });
```

Tokens are refreshed automatically when they expire — no intervention needed.

***

## Multi-tenant setup

In multi-tenant mode, each user connects their own account. Configure **manual** connect mode and mount the [management handler](/management/handler).

```ts corsair.ts theme={null}
export const corsair = createCorsair({
    multiTenancy: true,
    plugins: [gmail({ authType: "oauth_2" })],
    kek: process.env.CORSAIR_KEK!,
    database: db,
    manual: {
        baseUrl: `${process.env.APP_URL}/connect`,
        redirectUri: `${process.env.APP_URL}/api/oauth/callback`,
    },
});
```

```ts app/api/corsair/[[...path]]/route.ts theme={null}
import { toNextJsHandler } from "corsair";
import { corsair } from "@/server/corsair";

export const { GET, POST, OPTIONS } = toNextJsHandler(corsair, {
    basePath: "/api/corsair",
});
```

### 1. Store your OAuth app credentials

Store your client credentials once — these are shared across all tenants:

```bash theme={null}
pnpm corsair setup --plugin=gmail client_id=your-client-id client_secret=your-client-secret
```

### 2. Create a connect link

When a user wants to connect, mint a link from your authenticated backend and redirect them:

```ts app/actions/connect.ts theme={null}
"use server";

import { corsair } from "@/server/corsair";
import { getSessionTenantId } from "@/server/auth";

export async function startOAuthConnect(plugin: string) {
    const tenantId = await getSessionTenantId();
    if (!tenantId) throw new Error("Unauthorized");

    const { connectUrl } = await corsair.manage.connect.createLink({
        plugin,
        tenantId,
    });

    return connectUrl;
}
```

Or from the client via the [management client](/adapters/client):

```tsx connect-button.tsx theme={null}
"use client";

import { createCorsairReactClient } from "corsair/client/react";

const { useCreateConnectLink } = createCorsairReactClient({
    baseURL: "/api/corsair",
});

function ConnectGmail({ tenantId }: { tenantId: string }) {
    const { mutate, loading } = useCreateConnectLink();

    return (
        <button
            disabled={loading}
            onClick={async () => {
                const link = await mutate({ plugin: "gmail", tenantId });
                if (link) window.location.href = link.connectUrl;
            }}
        >
            Connect Gmail
        </button>
    );
}
```

The signed `state` is embedded in `connectUrl` — you do not store it separately.

### 3. Resolve on your connect page

The user lands on `/connect?state=…`. Resolve the state and redirect to the provider:

```ts app/connect/page.tsx theme={null}
import { redirect } from "next/navigation";
import { corsair } from "@/server/corsair";

export default async function ConnectPage({
    searchParams,
}: {
    searchParams: Promise<{ state?: string }>;
}) {
    const { state } = await searchParams;
    if (!state) return <p>Missing state.</p>;

    const { oauthUrl } = await corsair.manage.connect.resolve(state);
    redirect(oauthUrl);
}
```

### 4. Handle the callback

After the user approves, the provider redirects to your callback URL:

```ts app/api/oauth/callback/route.ts theme={null}
import { corsair } from "@/server/corsair";
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

export async function GET(request: NextRequest) {
    const { searchParams } = new URL(request.url);
    const code = searchParams.get("code");
    const state = searchParams.get("state");

    if (!code || !state) {
        return new NextResponse("Missing code or state.", { status: 400 });
    }

    const result = await corsair.manage.connect.oauthCallback({ code, state });
    return NextResponse.redirect(
        `/dashboard?connected=${encodeURIComponent(result.plugin)}`,
    );
}
```

Corsair extracts the `tenantId` from the HMAC-signed state, exchanges the code for tokens, and stores them encrypted for that tenant.

<Info>
  See [Production: OAuth Process](/concepts/oauth-process) for a full implementation with security best practices — authenticated link creation, HTML escaping, and production checklists.
</Info>

### 5. Make API calls per tenant

```ts usage.ts theme={null}
const tenant = corsair.withTenant("user_abc123");

// Uses user_abc123's connected account
const messages = await tenant.gmail.api.messages.list({ maxResults: 10 });
```

### Hub mode alternative

If you don't want to build connect pages, use `hub: { ... }` instead of `manual`. Call the same `createLink` API — the URL points to Corsair Hub's hosted UI. Hub hosts the connect surfaces and stores none of your credentials. See [Hub overview](/hub/overview) for the model and [Connect / OAuth](/management/connect#hub-mode-hosted-connect-ui) for the API.

***

## Automatic token refresh

OAuth access tokens expire (typically after 1 hour). Corsair checks token expiry before every API call and refreshes automatically using the stored refresh token. Your code never needs to handle token expiry.

See [Authentication](/concepts/auth#automatic-token-refresh) for more details.
