# Configure SAML SSO

Set up SAML 2.0 Single Sign-On for self-hosted Supabase with Docker.

SAML 2.0 SSO lets your users authenticate through an enterprise Identity Provider (IdP) such as Okta, Azure AD (Entra ID), Google Workspace, or any SAML 2.0-compliant provider. Unlike OAuth providers, SAML IdPs are not configured through environment variables - they are managed dynamically at runtime through the Auth admin API.

This guide covers the full setup: generating a signing key, enabling SAML in your Supabase instance, registering an IdP, and integrating SSO into your application.

Note: Client-side integration uses the same `supabase.auth.signInWithSSO()` method documented in the [SSO with SAML 2.0](/docs/guides/auth/enterprise-sso/auth-sso-saml) guide. This guide focuses on the self-hosted server configuration.

## Before you begin

You need:

- A running self-hosted Supabase instance (see the [setup guide](/docs/guides/self-hosting/docker))
- Open SSL installed (for key generation)
- Your IdP's SAML metadata URL or metadata XML
- Your project's secret key, `SUPABASE_SECRET_KEY`, from your `.env` file (needed for admin API calls)
- `API_EXTERNAL_URL` set to the publicly-accessible URL of your Supabase Auth service (e.g., `https://<your-domain>/auth/v1`). Used as the base for constructing the SAML Service Provider entity ID and ACS endpoint URL

## How SAML SSO works in Supabase

SAML SSO is configured in two layers:

1. **Global SAML enable (environment variables)** - a small set of env vars that enable the SAML engine and provide a signing key. These go in `.env` and `docker-compose.yml`.
2. **Per-IdP configuration (admin API)** - individual Identity Providers are registered, updated, and deleted at runtime via the Auth admin API. No restart is needed when adding or removing providers.

The sign-in flow works as follows:

1. Your app calls `POST /auth/v1/sso` with a domain or provider\_id
2. Auth generates a SAML `AuthnRequest` and returns a redirect URL to the IdP
3. The user authenticates at the IdP
4. The IdP POSTs a SAML Response to `POST /auth/v1/sso/saml/acs`
5. Auth validates the assertion, creates or links the user, and issues a session
6. The user is redirected back to your app with session tokens

## Step 1: Generate an RSA private key

SAML requests must be signed. The value expected by `GOTRUE_SAML_PRIVATE_KEY` is a Base64-encoded PKCS#1 DER RSA private key (with a 2048-bit key as the minimum requirement). Generate it with:

```sh
openssl genpkey -algorithm RSA -out pk_pkcs8.pem -quiet && \
openssl pkey -in pk_pkcs8.pem -out pk_rsa1.der -outform DER -traditional && \
base64 -w 0 -i pk_rsa1.der
```

The commands above:

1. Generate an RSA private key in PKCS#8 PEM format
2. Convert the key to PKCS#1 DER format
3. Base64-encode the key for use as the value of `GOTRUE_SAML_PRIVATE_KEY`

Save the Base64 output - make sure to copy it as single line, ignoring the trailing newline. Remove the temporary files.

Caution: Keep this key secret. Anyone with the private key can forge SAML requests on behalf of your Service Provider. Do not commit it to the version control system.

Note: For a production deployment, consider using a 4096-bit key by adding `-pkeyopt rsa_keygen_bits:4096` to the `openssl genpkey` command above.

## Step 2: Add environment variables

Add the following to your `.env` file:

```sh name=.env
############
# SAML SSO
############

SAML_ENABLED=true
SAML_PRIVATE_KEY=<your-base64-encoded-private-key>

# Optional: accept encrypted SAML assertions from IdPs (default: false)
# SAML_ALLOW_ENCRYPTED_ASSERTIONS=false

# Optional: how long relay state tokens remain valid (default: 2m0s)
# SAML_RELAY_STATE_VALIDITY_PERIOD=2m0s

# Optional: rate limit on the ACS endpoint (requests per second, default: 15)
# SAML_RATE_LIMIT_ASSERTION=15
```

## Step 3: Pass SAML variables to the Auth container

In `docker-compose.yml`, add the SAML environment variables to the `auth` service. Auth expects the `GOTRUE_` prefix for all of its configuration variables:

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

    # SAML SSO
    GOTRUE_SAML_ENABLED: ${SAML_ENABLED}
    GOTRUE_SAML_PRIVATE_KEY: ${SAML_PRIVATE_KEY}
    # GOTRUE_SAML_ALLOW_ENCRYPTED_ASSERTIONS: ${SAML_ALLOW_ENCRYPTED_ASSERTIONS}
    # GOTRUE_SAML_RELAY_STATE_VALIDITY_PERIOD: ${SAML_RELAY_STATE_VALIDITY_PERIOD}
    # GOTRUE_SAML_RATE_LIMIT_ASSERTION: ${SAML_RATE_LIMIT_ASSERTION}
```

## Step 4: Restart the containers

Apply the configuration changes:

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

Verify the Auth service is healthy:

```sh
sh run.sh status auth
```

## Step 5: Retrieve your service provider metadata

Once SAML is enabled, your Supabase instance exposes service provider (SP) metadata at `{API_EXTERNAL_URL}/sso/saml/metadata`.

Verify it using `curl`:

```sh
curl http://<your-domain>/auth/v1/sso/saml/metadata
```

This returns an XML document containing your SP entity ID, ACS endpoint URL, and signing certificate. You will need to provide this to your IdP.

Note: Add `?download=true` to the request URL to get the metadata as a downloadable XML file with a 5-year validity period - this is useful for IdPs that require a file upload instead of a URL.

Key values in the metadata:

| Field               | Value                                  |
| ------------------- | -------------------------------------- |
| Entity ID           | `{API_EXTERNAL_URL}/sso/saml/metadata` |
| ACS URL             | `{API_EXTERNAL_URL}/sso/saml/acs`      |
| NameID formats      | `persistent`, `emailAddress`           |
| Signing certificate | Derived from your `SAML_PRIVATE_KEY`   |

## Step 6: Register an identity provider

Use the Auth admin API to register your IdP. You need your project's secret key, `SUPABASE_SECRET_KEY`, for authentication.

### Option A: Register with a metadata URL (recommended)

If your IdP provides a metadata URL, Auth will fetch and cache the metadata automatically and refresh it when it becomes stale:

```sh
curl -X POST 'http://<your-domain>/auth/v1/admin/sso/providers' \
  -H 'Content-Type: application/json' \
  -H 'apikey: your-supabase-secret-key' \
  -d '{
    "type": "saml",
    "metadata_url": "https://idp.example.com/saml/metadata",
    "domains": ["example.com"],
    "attribute_mapping": {
      "keys": {
        "email": {
          "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
        },
        "name": {
          "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
        }
      }
    }
  }'
```

### Option B: Register with inline metadata XML

If you have the IdP metadata as an XML string:

```sh
curl -X POST 'http://<your-domain>/auth/v1/admin/sso/providers' \
  -H 'Content-Type: application/json' \
  -H 'apikey: your-supabase-secret-key' \
  -d '{
    "type": "saml",
    "metadata_xml": "<EntityDescriptor ...>...</EntityDescriptor>",
    "domains": ["example.com"]
  }'
```

Note: When using `metadata_url`, the URL must use HTTPS. Auth validates the metadata XML format and checks that the EntityID is unique across all registered providers.

The response includes the provider `id` (UUID) - save this for use in your application or for later management:

```json
{
  "id": "d3f5a1b2-...",
  "resource_id": null,
  "disabled": false,
  "saml": {
    "entity_id": "https://idp.example.com/saml",
    "metadata_url": "https://idp.example.com/saml/metadata"
  },
  "domains": [{ "domain": "example.com" }],
  "created_at": "...",
  "updated_at": "..."
}
```

### Registration parameters reference

| Parameter           | Required     | Description                                                                                   |
| ------------------- | ------------ | --------------------------------------------------------------------------------------------- |
| `type`              | Yes          | Must be `"saml"`                                                                              |
| `metadata_url`      | One of these | HTTPS URL to the IdP's SAML metadata (auto-refreshed)                                         |
| `metadata_xml`      | One of these | Raw IdP metadata XML string                                                                   |
| `domains`           | No           | Array of email domains to associate (e.g., `["acme.com"]`). Used for domain-based SSO lookup. |
| `attribute_mapping` | No           | Map SAML attributes to user claims (see [Attribute mapping](#attribute-mapping))              |
| `name_id_format`    | No           | Request a specific NameID format: `persistent`, `emailAddress`, `transient`, or `unspecified` |
| `resource_id`       | No           | A custom external identifier for the provider                                                 |
| `disabled`          | No           | Set to `true` to register but disable the provider                                            |

## Step 8: Configure your identity provider

On the IdP side, create a new SAML application and configure it with your SP details:

| IdP setting             | Value                                                       |
| ----------------------- | ----------------------------------------------------------- |
| SP Entity ID / Audience | `{API_EXTERNAL_URL}/sso/saml/metadata`                      |
| ACS URL / Reply URL     | `{API_EXTERNAL_URL}/sso/saml/acs`                           |
| NameID format           | `persistent` (recommended) or `emailAddress`                |
| Signing certificate     | Upload from the SP metadata XML or provide the metadata URL |

### IdP-specific configuration

**Okta**

**Okta setup:**

- Create a "SAML 2.0" application
- Single Sign-On URL: `{API_EXTERNAL_URL}/sso/saml/acs`
- Audience URI (SP Entity ID): `{API_EXTERNAL_URL}/sso/saml/metadata`
- Default RelayState: leave blank
- Name ID format: `Persistent`

**Azure**

**Azure AD (Entra ID):**

- Create an "Enterprise Application" → "Non-gallery application"
- Set up single sign-on → SAML
- Identifier (Entity ID): `{API_EXTERNAL_URL}/sso/saml/metadata`
- Reply URL (ACS): `{API_EXTERNAL_URL}/sso/saml/acs`
- Metadata URL for Auth: App Federation Metadata URL (from the SAML Signing Certificate section)

**Google**

**Google Workspace:**

- Admin console → Apps → Web and mobile apps → Add custom SAML app
- ACS URL: `{API_EXTERNAL_URL}/sso/saml/acs`
- Entity ID: `{API_EXTERNAL_URL}/sso/saml/metadata`
- Name ID format: `PERSISTENT`

**Auth0**

**Auth0:**

- Dashboard → Applications → Applications → Create Application → Regular Web Application
- Settings tab → Advanced Settings → Endpoints tab
- Copy the **SAML Metadata URL** (this is the IdP metadata URL for Supabase)
- Settings tab → Advanced Settings → Add-ons → SAML2 Web App
- Enable SAML2 Web App add-on
- Settings tab in the add-on:
  - Application Callback URL (ACS): `{API_EXTERNAL_URL}/sso/saml/acs`
- Enable the add-on

## Attribute mapping

Attribute mapping lets you control how SAML assertion attributes are translated into Supabase user claims. If no mapping is provided, Auth uses sensible defaults:

**Default email detection order:**

1. `urn:oid:0.9.2342.19200300.100.1.3` (LDAP mail OID)
2. `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress`
3. `http://schemas.xmlsoap.org/claims/EmailAddress`
4. Attributes named `mail`, `Mail`, or `email`
5. Subject NameID (if it looks like an email address)

**Default user ID detection:**

1. `urn:oasis:names:tc:SAML:attribute:subject-id` attribute
2. Subject NameID (if format is `persistent`)

### Custom attribute mapping example

Map IdP-specific attributes to user metadata:

```json
{
  "attribute_mapping": {
    "keys": {
      "email": {
        "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
      },
      "name": {
        "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
      },
      "department": {
        "name": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/department",
        "default": "unknown"
      },
      "groups": {
        "name": "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups",
        "array": true
      },
      "role": {
        "names": ["http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "role", "Role"],
        "default": "member"
      }
    }
  }
}
```

Each mapping key supports:

| Field     | Description                                                                                                |
| --------- | ---------------------------------------------------------------------------------------------------------- |
| `name`    | Primary SAML attribute name to look for (matched against both `Name` and `FriendlyName`, case-insensitive) |
| `names`   | Array of fallback attribute names to try in order                                                          |
| `default` | Default value if the attribute is not present in the assertion                                             |
| `array`   | Set to `true` to collect all values (for multi-valued attributes like groups)                              |

Mapped attributes are stored in the user's `raw_user_meta_data` and are available via `user.user_metadata` in your application.

## Managing providers

### List all providers

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

Filter by resource ID using exact match:

```sh
curl 'http://<your-domain>/auth/v1/admin/sso/providers?resource_id=my-idp' \
  -H 'apikey: your-supabase-secret-key'
```

or prefix match:

```sh
curl 'http://<your-domain>/auth/v1/admin/sso/providers?resource_id_prefix=prod-' \
  -H 'apikey: your-supabase-secret-key'
```

### Get a specific provider

```sh
curl 'http://<your-domain>/auth/v1/admin/sso/providers/{provider_id}' \
  -H 'apikey: your-supabase-secret-key'
```

### Update a provider

```sh
curl -X PUT 'http://<your-domain>/auth/v1/admin/sso/providers/{provider_id}' \
  -H 'Content-Type: application/json' \
  -H 'apikey: your-supabase-secret-key' \
  -d '{
    "domains": ["example.com", "subsidiary.com"],
    "attribute_mapping": {
      "keys": {
        "email": {
          "name": "mail"
        }
      }
    }
  }'
```

### Disable a provider (without deleting)

```sh
curl -X PUT 'http://<your-domain>/auth/v1/admin/sso/providers/{provider_id}' \
  -H 'Content-Type: application/json' \
  -H 'apikey: your-supabase-secret-key' \
  -d '{ "disabled": true }'
```

### Delete a provider

```sh
curl -X DELETE 'http://<your-domain>/auth/v1/admin/sso/providers/{provider_id}' \
  -H 'apikey: your-supabase-secret-key'
```

## Client-side integration

### Using supabase-js

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

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

// Option 1: SSO by email domain
const { data, error } = await supabase.auth.signInWithSSO({
  domain: 'example.com',
})

// Option 2: SSO by provider ID
const { data, error } = await supabase.auth.signInWithSSO({
  providerId: 'd3f5a1b2-...',
})

// Redirect the user to the IdP
if (data?.url) {
  window.location.href = data.url
}
```

Both methods return an object with a `url` property - redirect the user to this URL to begin authentication at the IdP.

### Using the REST API directly

By domain:

```sh
curl -X POST 'http://<your-domain>/auth/v1/sso' \
  -H 'Content-Type: application/json' \
  -H 'apikey: your-supabase-publishable-key' \
  -d '{
    "domain": "example.com",
    "skip_http_redirect": true
  }'
```

By provider ID:

```sh
curl -X POST 'http://<your-domain>/auth/v1/sso' \
  -H 'Content-Type: application/json' \
  -H 'apikey: your-supabase-publishable-key' \
  -d '{
    "provider_id": "d3f5a1b2-...",
    "skip_http_redirect": true
  }'
```

Both return `{ "url": "https://idp.example.com/sso?SAMLRequest=..." }`.

### Domain-based vs provider-based lookup

| Method       | Use case                                                                                                                                  |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `domain`     | Extract the domain from the user's email and let Auth find the right IdP. Best for sign-in forms where the user enters their email first. |
| `providerId` | Use when you know the exact provider - for example, a dedicated "Sign in with Okta" button.                                               |

## Test the sign-in flow

1. Open your application and trigger SSO sign-in (or use the curl command above)
2. You should be redirected to your IdP's sign-in page
3. After authenticating, the IdP posts back to the ACS endpoint
4. Auth processes the assertion and redirects you back to your `SITE_URL` (or `redirect_to` URL) with session tokens

To verify the session was created:

```sh
curl 'http://<your-domain>/auth/v1/user' \
  -H 'Authorization: Bearer user-session-token' \
  -H 'apikey: your-supabase-publishable-key'
```

The response should include `app_metadata.provider: "sso:saml"` and any mapped attributes in `user_metadata`.

## Environment variable reference

| Variable                           | Default | Description                                                                                                         |
| ---------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------- |
| `SAML_ENABLED`                     | `false` | Enable the SAML SSO engine                                                                                          |
| `SAML_PRIVATE_KEY`                 | -       | Base64-encoded PKCS#1 RSA private key (min 2048-bit). Used to sign SAML requests and optionally decrypt assertions. |
| `SAML_ALLOW_ENCRYPTED_ASSERTIONS`  | `false` | Accept encrypted SAML assertions from IdPs                                                                          |
| `SAML_RELAY_STATE_VALIDITY_PERIOD` | `2m0s`  | How long relay state tokens remain valid. Increase if users on slow networks time out during the IdP redirect.      |
| `SAML_RATE_LIMIT_ASSERTION`        | `15`    | Maximum ACS requests per second. Protects against assertion replay floods.                                          |

## Troubleshooting

### "SAML is not enabled on this server"

The `GOTRUE_SAML_ENABLED` variable is not set to `true`, or the Auth container did not pick up the change. Verify the env var is passed through `docker-compose.yml` and restart:

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

### "Invalid private key" on startup

The `GOTRUE_SAML_PRIVATE_KEY` value is malformed. Ensure it is:

- Base64-encoded (single line, no line breaks)
- In PKCS#1 format (`openssl pkey ... -traditional` output)
- At least 2048-bit RSA

Regenerate if needed:

```sh
openssl genpkey -algorithm RSA -out pk_pkcs8.pem -quiet && \
openssl pkey -in pk_pkcs8.pem -out pk_rsa1.der -outform DER -traditional && \
base64 -w 0 -i pk_rsa1.der
```

### IdP cannot reach the ACS endpoint

- Verify `API_EXTERNAL_URL` is set to a URL containing `/auth/v1` the IdP can reach (not `localhost` unless testing locally)
- Check that the API gateway routes for `/auth/v1/sso/saml/acs` and `/auth/v1/sso/saml/metadata` are configured as open (no `key-auth` plugin).
- Check the Auth container logs: `docker compose logs auth`

### "No SSO provider found for this domain"

- Verify the domain is registered: list providers and check the `domains` array
- Domain matching is exact and case-insensitive - `Example.com` matches `example.com`

### Assertion validation fails

- Ensure the IdP's signing certificate matches what is in the metadata registered with Auth
- If using `metadata_url`, Auth automatically refreshes stale metadata (after `ValidUntil`, `CacheDuration`, or 24 hours). Force a refresh by updating the provider.
- Check clock sync between your server and the IdP - SAML assertions have time-based validity windows (`NotBefore` / `NotOnOrAfter`)

### User is created but attributes are missing

- Check your `attribute_mapping` configuration. Use the IdP's SAML assertion viewer (most IdPs have one) to see the exact attribute names being sent.
- Attribute names are matched case-insensitively against both the `Name` and `FriendlyName` fields in the assertion.
- Mapped attributes appear in `user.user_metadata`.

### Relay state expired

The user took too long between initiating SSO and completing authentication at the IdP. Increase `GOTRUE_SAML_RELAY_STATE_VALIDITY_PERIOD` (default is 2 minutes).

## Additional resources

- [SSO with SAML 2.0](/docs/guides/auth/enterprise-sso/auth-sso-saml) - Client-side SAML integration guide
- [Auth server configuration reference](/docs/guides/self-hosting/auth/config) - Full list of Auth environment variables
- [SAML 2.0 specification](http://docs.oasis-open.org/security/saml/v2.0/) - The underlying standard
