# Configure Custom OAuth/OIDC Providers

Set up any OAuth 2.0 or OIDC-compatible identity provider for self-hosted Supabase.

This guide explains how to add a custom OAuth 2.0 or OpenID Connect (OIDC) identity provider to a self-hosted Supabase instance and how to manage it through the Auth admin API. To learn how custom providers work and which advanced options they support, see [Custom OAuth/OIDC Providers](https://supabase.com/docs/guides/auth/custom-oauth-providers).

There are two provider types:

- **OAuth 2.0**: for generic OAuth 2.0 providers where you supply the authorization, token, and userinfo endpoints manually.
- **OIDC**: for providers that support [OpenID Connect](https://openid.net/connect/) discovery. You supply only the issuer URL and endpoints are resolved automatically.

## Provider identifiers

Every custom provider identifier must start with the `custom:` prefix. Identifiers are 2-50 characters, lowercase alphanumeric with hyphens and colons allowed. Examples:

- `custom:my-provider`
- `custom:github-enterprise`

## Before you begin

You need:

- A working self-hosted Supabase instance on release `0.8.1` or later, which ships Supabase Auth `v2.196.0`. See [Self-Hosting with Docker](https://supabase.com/docs/guides/self-hosting/docker) and [Update Your Self-Hosted Deployment](https://supabase.com/docs/guides/self-hosting/updating).
- Your project's secret key, `SUPABASE_SECRET_KEY`, from your `.env` file.
- `API_EXTERNAL_URL` set to the publicly reachable URL of your Auth service, ending in `/auth/v1`, for example `https://<your-domain>/auth/v1`. The Auth service derives the OAuth callback URL for custom providers from this value.

Danger: Most OAuth providers reject `http://` callback URLs other than `localhost`, so your instance needs HTTPS. See [Configure Reverse Proxy and HTTPS](https://supabase.com/docs/guides/self-hosting/self-hosted-proxy-https) for setup instructions.

When registering your application with an external identity provider, add the following URL as the redirect URI in the provider's settings:

```
https://<your-domain>/auth/v1/callback
```

## Optional Auth configuration

Custom providers are enabled by default in the Auth service, so no changes to `docker-compose.yml` are required. The following environment variables adjust the defaults:

| Variable                            | Description                                                                                                |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `GOTRUE_CUSTOM_OAUTH_ENABLED`       | Set to `"false"` to disable custom OAuth/OIDC providers. Defaults to `true`.                               |
| `GOTRUE_CUSTOM_OAUTH_MAX_PROVIDERS` | Maximum number of custom OAuth/OIDC providers allowed. Defaults to `0`, which means unlimited.             |
| `GOTRUE_CUSTOM_OAUTH_EXTERNAL_URL`  | Base URL used to build the callback URL for custom providers. Defaults to the value of `API_EXTERNAL_URL`. |

To change any of these, add the variable to the `auth` service in your `docker-compose.yml`:

```yaml name=docker-compose.yml
auth:
  environment:
    # ... existing variables ...
    GOTRUE_CUSTOM_OAUTH_MAX_PROVIDERS: 5
```

Then recreate the Auth service for the change to take effect:

```sh
sh run.sh recreate auth
```

## Create a provider

Use the Auth admin API to create providers. You need your project's secret key for authentication.

Danger: The JavaScript examples use a [supabase-js](https://supabase.com/docs/reference/javascript/start) client created with the secret key (`SUPABASE_SECRET_KEY`), which must only run in trusted server-side environments.

```js
import { createClient } from '@supabase/supabase-js'

const supabase = createClient('http://<your-domain>', 'your-supabase-secret-key')
```

The Auth service fetches and validates the provider's endpoints when you create the provider, so the request fails if the issuer or endpoint URLs can't be reached.

### OAuth 2.0 provider

Use an OAuth 2.0 provider when your identity provider does not support OpenID Connect discovery. You must supply the authorization, token, and userinfo endpoint URLs explicitly.

**JavaScript**

```js
const { data, error } = await supabase.auth.admin.customProviders.createProvider({
  provider_type: 'oauth2',
  identifier: 'custom:my-oauth-provider',
  name: 'My OAuth Provider',
  client_id: 'your-client-id',
  client_secret: 'your-client-secret',
  authorization_url: 'https://provider.example.com/oauth/authorize',
  token_url: 'https://provider.example.com/oauth/token',
  userinfo_url: 'https://provider.example.com/oauth/userinfo',
  scopes: ['profile', 'email'],
})
```

**cURL**

```sh
curl -X POST "http://<your-domain>/auth/v1/admin/custom-providers" \
  -H "apikey: your-supabase-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "provider_type": "oauth2",
    "identifier": "custom:my-oauth-provider",
    "name": "My OAuth Provider",
    "client_id": "your-client-id",
    "client_secret": "your-client-secret",
    "authorization_url": "https://provider.example.com/oauth/authorize",
    "token_url": "https://provider.example.com/oauth/token",
    "userinfo_url": "https://provider.example.com/oauth/userinfo",
    "scopes": ["profile", "email"]
  }'
```

### OIDC provider

Use an OIDC provider when your identity provider supports OpenID Connect. Supply the `issuer` URL and the discovery document, JWKS, and endpoints are resolved automatically.

**JavaScript**

```js
const { data, error } = await supabase.auth.admin.customProviders.createProvider({
  provider_type: 'oidc',
  identifier: 'custom:my-oidc-provider',
  name: 'My OIDC Provider',
  client_id: 'your-client-id',
  client_secret: 'your-client-secret',
  issuer: 'https://auth.example.com',
  scopes: ['openid', 'profile', 'email'],
})
```

**cURL**

```sh
curl -X POST "http://<your-domain>/auth/v1/admin/custom-providers" \
  -H "apikey: your-supabase-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "provider_type": "oidc",
    "identifier": "custom:my-oidc-provider",
    "name": "My OIDC Provider",
    "client_id": "your-client-id",
    "client_secret": "your-client-secret",
    "issuer": "https://auth.example.com",
    "scopes": ["openid", "profile", "email"]
  }'
```

OIDC providers have the following automatic behavior:

- The discovery document is fetched from `{issuer}/.well-known/openid-configuration`, or from `discovery_url` when it is set.
- The `openid` scope is always included. It is automatically added if missing from the `scopes` array.
- ID tokens are verified against the provider's JWKS, which is fetched from the discovery document's `jwks_uri`.

### Verify the provider

List the configured providers to confirm the new provider was saved:

```sh
curl "http://<your-domain>/auth/v1/admin/custom-providers" \
  -H "apikey: your-supabase-secret-key"
```

The response includes the provider you created. Users can now sign in with it from your app:

```js
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'custom:my-oidc-provider',
})
```

## Example: Telegram

The following steps walk through setting up Telegram sign-in with Supabase Auth.

Note: Telegram sign-in fails on Supabase Auth versions before `v2.196.0` because of an [unsupported signing algorithm issue](https://github.com/supabase/auth/issues/2534).

### Step 1: Create a Telegram bot

1. Open [@BotFather](https://t.me/BotFather) on Telegram and send the command `/newbot`.
2. Follow the prompts to choose a name and username for your bot.

### Step 2: Register the redirect URL

1. Open the [@BotFather mini app](https://t.me/botfather?startapp) on Telegram and select the bot you created in the previous step.
2. Select **Login Widget** to open the page where you configure redirect URIs.
3. If you don't see OIDC settings, click on "Switch to OpenID Connect Login."
4. Click **Add a Redirect URI** and enter your self-hosted Supabase callback URL:

   ```
   https://<your-domain>/auth/v1/callback
   ```

This screen also shows your Client ID and Client Secret. You use them to create the provider in the next step.

### Step 3: Create the Telegram provider

Copy the Client ID and Client Secret from the **Login Widget** screen, then use them to create a custom Telegram OAuth provider:

**JavaScript**

```js
const { data, error } = await supabase.auth.admin.customProviders.createProvider({
  provider_type: 'oidc',
  identifier: 'custom:telegram',
  name: 'Telegram',
  client_id: 'your-client-id',
  client_secret: 'your-client-secret',
  issuer: 'https://oauth.telegram.org',
  scopes: ['openid', 'profile'],
  email_optional: true,
})
```

**cURL**

```sh
curl -X POST "http://<your-domain>/auth/v1/admin/custom-providers" \
  -H "apikey: your-supabase-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "provider_type": "oidc",
    "identifier": "custom:telegram",
    "name": "Telegram",
    "client_id": "your-client-id",
    "client_secret": "your-client-secret",
    "issuer": "https://oauth.telegram.org",
    "scopes": ["openid", "profile"],
    "email_optional": true
  }'
```

A few notes on these fields:

- `identifier`: can be any value that starts with `custom:` and follows the [identifier rules](#provider-identifiers).
- `name`: can be any value you want.
- `email_optional`: must be `true` because Telegram doesn't return an email address.

The `profile` scope returns the user's name and picture. To also receive the phone number, add the `phone` scope. For the full list of available scopes, see the [Telegram Login docs](https://core.telegram.org/bots/telegram-login#available-scopes).

### Step 4: Sign in with Telegram

With the provider configured, you can now sign in with Telegram from your app:

```js
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'custom:telegram',
})
```

### Step 5: Test the sign-in flow

Only the Supabase callback URL needs to be public and served over HTTPS, because that is where the identity provider redirects. The test page itself is a static HTML file that can run anywhere your browser can reach, including your local machine.

1. Save the code below to `index.html`.
2. Set `SITE_URL` in your self-hosted Supabase `.env` file to the URL where the page runs, for example `http://localhost:3000`, and recreate the Auth service with `sh run.sh recreate auth`. To allow more than one URL, add the others to `ADDITIONAL_REDIRECT_URLS`.
3. Start a simple HTTP server via `python -m http.server 3000` to serve `index.html`.
4. Open your browser and go to `http://localhost:3000`.

```html
<!doctype html>
<html>
  <body>
    <h1>Supabase custom provider test</h1>
    <button id="loginBtn">Sign in with Telegram</button>
    <pre id="result"></pre>

    <script src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"></script>
    <script>
      document.addEventListener('DOMContentLoaded', function () {
        const SUPABASE_URL = 'https://<your-domain>'
        const SUPABASE_PUBLISHABLE_KEY = 'your-supabase-publishable-key'

        const supabase = window.supabase.createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY)

        document.getElementById('loginBtn').addEventListener('click', async () => {
          const { error } = await supabase.auth.signInWithOAuth({
            provider: 'custom:telegram',
          })

          if (error) {
            document.getElementById('result').textContent = JSON.stringify(error, null, 2)
          }
        })

        supabase.auth.onAuthStateChange((_event, session) => {
          if (session) {
            document.getElementById('result').textContent = JSON.stringify(session.user, null, 2)
          }
        })
      })
    </script>
  </body>
</html>
```

Clicking the button sends the browser to your Auth service, which redirects to Telegram. After you confirm the sign-in in the Telegram app, Telegram sends the browser back to `https://<your-domain>/auth/v1/callback`. The Auth service completes the exchange and redirects to `SITE_URL`, where `supabase-js` picks up the session and the page prints the signed-in user.

Telegram doesn't return an email address, so `email` is empty in the user object. The profile claims that Telegram returns, `name`, `given_name`, `family_name`, and `picture`, are in `user_metadata` together with the ID token claims, and `app_metadata.provider` is `custom:telegram`. The `sub` claim is the stable identifier that Auth uses to match the user on later sign-ins.

To test a different custom provider, change the `provider` value to its identifier.

## Manage providers

Use the admin API to list, update, and delete custom providers. Self-hosted Studio doesn't include a UI for them. The examples use `custom:my-provider` as the identifier. Replace it with the identifier of your provider.

### List providers

**JavaScript**

```js
// List all custom providers
const { data, error } = await supabase.auth.admin.customProviders.listProviders()

// Filter by provider type
const { data, error } = await supabase.auth.admin.customProviders.listProviders({
  type: 'oidc',
})
```

**cURL**

```sh
# List all custom providers
curl "http://<your-domain>/auth/v1/admin/custom-providers" \
  -H "apikey: your-supabase-secret-key"

# Filter by provider type
curl "http://<your-domain>/auth/v1/admin/custom-providers?type=oidc" \
  -H "apikey: your-supabase-secret-key"
```

### Update a provider

Send only the fields you want to change. Fields you leave out keep their current values, so you can rotate a client secret by sending `client_secret` alone. The `provider_type` and `identifier` fields are fixed when the provider is created and can't be updated.

**JavaScript**

```js
const { data, error } = await supabase.auth.admin.customProviders.updateProvider(
  'custom:my-provider',
  {
    name: 'Updated Provider Name',
    scopes: ['openid', 'profile', 'email'],
  }
)
```

**cURL**

```sh
curl -X PUT "http://<your-domain>/auth/v1/admin/custom-providers/custom:my-provider" \
  -H "apikey: your-supabase-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Updated Provider Name",
    "scopes": ["openid", "profile", "email"]
  }'
```

### Delete a provider

**JavaScript**

```js
const { data, error } =
  await supabase.auth.admin.customProviders.deleteProvider('custom:my-provider')
```

**cURL**

```sh
curl -X DELETE "http://<your-domain>/auth/v1/admin/custom-providers/custom:my-provider" \
  -H "apikey: your-supabase-secret-key"
```

For PKCE, authorization parameters, and OIDC-specific options, see [Advanced configuration](https://supabase.com/docs/guides/auth/custom-oauth-providers#advanced-configuration).

## Additional resources

- [Custom OAuth/OIDC Providers](https://supabase.com/docs/guides/auth/custom-oauth-providers)
- [Configure Social Login (OAuth) Providers](https://supabase.com/docs/guides/self-hosting/self-hosted-oauth)
