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

# OAuth

> Call the API as a user in your workspace after they sign in to your OAuth app.

An OAuth app lets users in your workspace sign in to your app with their Plain account. Your app then calls the API as that user, with the permissions their role already has, and Plain records those actions as that user. To call the API as an integration rather than as a user, create a [machine user](https://www.plain.com/docs/graphql/authentication.md) instead.

## Choose an OAuth app type

The OAuth app type determines how your app authenticates when it exchanges an authorization code for an access token. Pick the type before you create the app, because you can't change it afterwards:

* **Client-side**: your app exchanges the code in a browser or a mobile or desktop app, where a secret wouldn't stay secret. The app has no client secret and must use Proof Key for Code Exchange (PKCE).
* **Server-side**: your app exchanges the code from your backend, where you can keep a secret. Plain creates a client secret for the app and shows it once.

PKCE proves that the token request comes from the app that started the sign-in. Your app generates a random code verifier, sends its SHA-256 hash as the code challenge when the user signs in, then sends the verifier itself with the authorization code.

To switch types, create a new OAuth app and move your integration to it.

## Create an OAuth app

Creating an OAuth app gives you a client ID, which identifies your app to Plain and isn't secret. A server-side app also gets a client secret.

**1. Open the form**

Go to **Settings → OAuth applications** and click **Add OAuth app**.

**2. Fill in the form**

The form has these fields:

* **App name**: The name users see when they sign in.
* **Description**: Optional.
* **OAuth app type**: **Client-side** or **Server-side**. The form selects **Client-side** by default.
* **Redirect URLs**: Where Plain sends a user after they sign in. Put each URL on its own line.
* **Allowed origins**: Shown only for a client-side app. See [Allowed origins for a client-side app](#allowed-origins-for-a-client-side-app).

Each redirect URL and allowed origin must use HTTPS, or HTTP on `localhost`.

**3. Create the app**

Click **Create OAuth app**. For a client-side app, Plain opens the app's settings page, which shows the client ID.

For a server-side app, Plain first shows the client ID and client secret. Click **Done** to open the app's settings page.

> [!WARNING]
>
> Copy the client secret before you click **Done**. You can't see it again. If you lose it, create a new server-side app.

The list in **Settings → OAuth applications** shows each app's type, and so does the **Credentials** card on the app's settings page.

### Allowed origins for a client-side app

An allowed origin is a website that may call Plain's sign-in endpoints, such as the token endpoint, from the browser. A client-side app needs at least one, because it exchanges the code from the browser, and the browser blocks the call from any other origin.

Write each origin on its own line as a scheme and host, with an optional port and no path, such as `https://app.example.com` or `http://localhost:3000`. A server-side app exchanges the code from your backend, so the create form doesn't ask for allowed origins.

## Get an access token

Your app gets an access token with the OAuth 2.0 authorization code flow: it sends the user to sign in, receives a code at a redirect URL, and exchanges that code for tokens. The authorization server is `https://signin.auth.plain.com`, and its [OpenID configuration](https://signin.auth.plain.com/.well-known/openid-configuration) lists the endpoints.

**1. Send the user to sign in**

Redirect the user to the authorize endpoint with these query parameters:

* **`client_id`**: Your app's client ID.
* **`redirect_uri`**: One of the app's redirect URLs.
* **`response_type`**: `code`.
* **`scope`**: `openid`, plus `offline_access` if you want a refresh token.
* **`state`** and **`nonce`**: Random values you generate for this sign-in. Check that the `state` Plain returns matches the one you sent.
* **`code_challenge`** and **`code_challenge_method`**: The PKCE code challenge, with method `S256`. A client-side app must send these. A server-side app can skip PKCE, but we recommend it.

This builds the URL in TypeScript:

```ts theme={null}
const params = new URLSearchParams({
  client_id: 'your_client_id',
  redirect_uri: 'your_redirect_url',
  response_type: 'code',
  scope: 'openid offline_access',
  state: 'your_state',
  nonce: 'your_nonce',
  code_challenge: 'your_code_challenge',
  code_challenge_method: 'S256',
});
const authorizeUrl =
  `https://signin.auth.plain.com/oauth2/authorize?${params}`;
```

A user in more than one workspace chooses which workspace to sign in to. The app only works in the workspace you created it in.

**2. Exchange the code for tokens**

After the user signs in, Plain redirects to `redirect_uri` with a `code`. Exchange it at the token endpoint, `https://signin.auth.plain.com/oauth2/token`.

For a server-side app, send the client secret from your backend. This request also sends the code verifier, because it used PKCE:

```bash theme={null}
curl -X POST https://signin.auth.plain.com/oauth2/token \
  -d client_id=your_client_id \
  -d client_secret=your_client_secret \
  -d grant_type=authorization_code \
  -d code=your_authorization_code \
  -d redirect_uri=your_redirect_url \
  -d code_verifier=your_code_verifier
```

For a client-side app, send the code verifier and no client secret. The page's origin must be one of the app's allowed origins:

```ts theme={null}
const tokenUrl = 'https://signin.auth.plain.com/oauth2/token';
const response = await fetch(tokenUrl, {
  method: 'POST',
  body: new URLSearchParams({
    client_id: 'your_client_id',
    grant_type: 'authorization_code',
    code: 'your_authorization_code',
    redirect_uri: 'your_redirect_url',
    code_verifier: 'your_code_verifier',
  }),
});
```

The response includes these fields:

* **`access_token`**: The token you send to the API.
* **`expires_in`**: How many seconds the access token lasts.
* **`refresh_token`**: Included when the sign-in requested `offline_access`.

**3. Refresh the access token**

Before the access token expires, send the refresh token to the token endpoint with `grant_type=refresh_token` and the client ID. A server-side app also sends its client secret:

```bash theme={null}
curl -X POST https://signin.auth.plain.com/oauth2/token \
  -d client_id=your_client_id \
  -d client_secret=your_client_secret \
  -d grant_type=refresh_token \
  -d refresh_token=your_refresh_token
```

Store the refresh token from each response and use it for the next refresh.

## Call the API

The API URL and request body are the same as for an API key; see [Key details](https://www.plain.com/docs/graphql/introduction.md#key-details). Send the access token in the same header an API key uses:

```plaintext theme={null}
Authorization: Bearer your_access_token
```

Each query and mutation runs as the signed-in user, with their current role. Changing their role changes what the app can do, and removing them from the workspace stops the app working for them. If they lack a permission, the error names it.

GraphQL subscriptions don't accept OAuth app tokens. The API rejects a token from a disabled or deleted app, or from another workspace, with HTTP status `401` and the error code `UNAUTHENTICATED`. The error doesn't say which of these applies.

## Disable or delete an OAuth app

Disabling an OAuth app stops it without losing its settings. On the app's settings page, turn off **Enabled** in the **Status** card and click **Save changes**. Users can't sign in to the app, and the API rejects every request from it, including tokens issued before you disabled it, until you enable it again.

To delete the app, use the **Delete** card on its settings page.

> [!WARNING]
>
> Deleting an OAuth app ends its access immediately. You can't undo a delete.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.