Enterprise SSO integration with Supabase

更新时间:
复制 MD 格式

Cloud-native data warehouse AnalyticDB for PostgreSQL integrates with the open-source Supabase authentication system, enabling enterprises to configure SAML 2.0 SSO. This is ideal for building private Coding Agent platforms with unified identity authentication.

Prerequisites

Procedure

Step 1: Obtain Supabase project information

After you create a Supabase project, obtain the following information.

Item

Description

Example

Supabase URL

Your Supabase project URL.

https://spb-xxxxx.supabase.example.com

Anon Key (public key)

A public key safe to use in frontend code.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Service Role Key (secret key)

A secret key for backend administrative operations only. Never expose in frontend code.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Verify project status

curl 'https://<your-supabase-url>/auth/v1/health' \
  -H 'apikey: <your-anon-key>'

Example response

{"version":"vunspecified","name":"GoTrue","description":"GoTrue is a user registration and authentication API"}

Step 2: Obtain SP metadata

The SAML service provider (SP) metadata is publicly accessible at:

https://<your-supabase-url>/sso/saml/metadata

Configure your IdP with the following URLs:

URL type

Value

ACS URL

https://<your-supabase-url>/sso/saml/acs

Entity ID

https://<your-supabase-url>/sso/saml/metadata

Step 3: Configure redirect URL

  1. Log in to the Supabase Dashboard. In the sidebar, click Authentication > URL Configuration.

  2. In the Site URL section, enter your frontend domain URL and click Save changes.

Step 4: Register identity provider (IdP)

Register your IdP with Supabase using the Management API.

Method 1: Register via metadata URL

If your IdP provides a public metadata URL, run:

curl -X POST 'https://<your-supabase-url>/auth/v1/admin/sso/providers' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "saml",
    "metadata_url": "https://<your-idp>/samlp/metadata/<client-id>",
    "domains": ["yourdomain.com"]
  }'

Method 2: Register via metadata XML

If the metadata URL is not publicly accessible, provide the XML content directly:

# 1. Obtain the metadata XML from an environment that can access the IdP.
curl 'https://<your-idp>/samlp/metadata/<client-id>' > idp-metadata.xml

# 2. Register with Supabase.
curl -X POST 'https://<your-supabase-url>/auth/v1/admin/sso/providers' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "saml",
    "metadata_xml": "<EntityDescriptor ...>...</EntityDescriptor>",
    "domains": ["yourdomain.com"]
  }'

Example: Auth0 registration

Auth0 metadata URL format:

https://<tenant>.auth0.com/samlp/metadata/<client-id>

Complete registration command:

curl -X POST 'https://<your-supabase-url>/auth/v1/admin/sso/providers' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "type": "saml",
    "metadata_url": "https://dev-xxx.auth0.com/samlp/metadata/YOUR_CLIENT_ID",
    "domains": ["example.com"]
  }'

Save returned provider ID

Important

Save the id field from the response. You need this ID to initiate SSO sign-in from the frontend.

Example response:

{
  "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "type": "saml",
  "saml": {
    "entity_id": "urn:dev-xxx.auth0.com"
  },
  "domains": [{"domain": "example.com"}]
}

Step 5: Configure SP information in IdP

After registration, configure the Supabase SP information in your IdP.

Required configuration items

Parameter

Value

ACS URL (callback URL)

https://<your-supabase-url>/sso/saml/acs

Entity ID (SP Entity ID)

https://<your-supabase-url>/sso/saml/metadata

NameID Format

emailAddress

Frontend callback URL

https://<your-app-domain>

Example: Configuration steps for Auth0

  1. Log in to the Auth0 Dashboard.

  2. Go to Applications and click the target application.

  3. Click the Addons tab and enable SAML2 Web App.

  4. In the dialog box that appears, configure the following settings:

    • Application Callback URL

    • Settings

  5. Click Enable, and then click Save.

  6. On the application's main page, add your frontend URL to Allowed Callback URLs.

Step 6: Frontend integration

  1. Install the dependency.

    npm install @supabase/supabase-js
  2. Initialize the client. SAML SSO must use the implicit flow.

    import { createClient } from '@supabase/supabase-js'
    
    const supabase = createClient(
      'https://<your-supabase-url>',
      '<your-anon-key>',
      {
        auth: {
          flowType: 'implicit',
          detectSessionInUrl: true,
          persistSession: true,
          autoRefreshToken: true,
        }
      }
    )
  3. Initiate SSO sign-in.

    • Initiate sign-in using the provider ID:

      const { data, error } = await supabase.auth.signInWithSSO({
        providerId: '<your-provider-id>',
        options: {
          redirectTo: window.location.origin
        }
      })
      
      if (error) {
        console.error('Sign-in failed:', error.message)
        return
      }
      
      if (data?.url) {
        window.location.href = data.url
      }
    • Alternatively, automatically match the provider using a domain:

      const { data, error } = await supabase.auth.signInWithSSO({
        domain: 'yourdomain.com'
      })
  4. Handle the sign-in callback.

    After the IdP authenticates the user, it redirects back to the frontend. The URL has the following format:

    https://<your-app-domain>/#access_token=<jwt>&refresh_token=<token>&expires_in=3600&token_type=bearer

    Because supabase-js initializes asynchronously, manually parse the URL hash:

    function decodeJWT(token) {
      try {
        return JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')))
      } catch {
        return null
      }
    }
    
    async function init() {
      const hash = window.location.hash.substring(1)
      if (hash.includes('access_token')) {
        const params = new URLSearchParams(hash)
        const accessToken = params.get('access_token')
        const refreshToken = params.get('refresh_token')
        const expiresAt = params.get('expires_at')
        
        window.history.replaceState({}, '', window.location.pathname)
        
        const payload = decodeJWT(accessToken)
        if (payload) {
          const session = {
            access_token: accessToken,
            refresh_token: refreshToken || '',
            expires_at: expiresAt ? parseInt(expiresAt) : payload.exp,
            token_type: 'bearer',
            user: {
              id: payload.sub,
              email: payload.email,
              role: payload.role,
              app_metadata: payload.app_metadata || {},
              user_metadata: payload.user_metadata || {},
            }
          }
          onLoginSuccess(session)
          return
        }
      }
    
      const { data: { session } } = await supabase.auth.getSession()
      if (session) {
        onLoginSuccess(session)
      } else {
        showLoginPage()
      }
    }
    
    document.addEventListener('DOMContentLoaded', init)
  5. View the JWT payload structure.

    After a successful sign-in, the decoded JWT contains these key fields:

    {
      "sub": "8c308927-4ba1-4507-b6f8-f751b791****",
      "email": "user@example.com",
      "role": "authenticated",
      "aal": "aal1",
      "amr": [{
        "method": "sso/saml",
        "timestamp": 1774928127,
        "provider": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      }],
      "app_metadata": {
        "provider": "sso:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
      },
      "session_id": "aeaf9cdb-dbcd-46ab-95a5-ff956e69****"
    }

    Verify SSO sign-in by checking amr[0].method === 'sso/saml'.

Complete example project

The following is a minimal, runnable example. For an online demo, see sso-demo.

  • main.js

    import { createClient } from '@supabase/supabase-js'
    
    const SUPABASE_URL = 'https://<your-supabase-url>'
    const SUPABASE_ANON_KEY = '<your-anon-key>'
    const AUTH0_PROVIDER_ID = '<auth0-provider-id>'
    
    const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
      auth: { flowType: 'implicit', detectSessionInUrl: true }
    })
    
    function decodeJWT(token) {
      try {
        return JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')))
      } catch { return null }
    }
    
    window.signInWithAuth0 = async () => {
      const { data, error } = await supabase.auth.signInWithSSO({
        providerId: AUTH0_PROVIDER_ID,
        options: { redirectTo: window.location.origin }
      })
      if (error) return alert(error.message)
      if (data?.url) window.location.href = data.url
    }
    
    window.signOut = async () => {
      await supabase.auth.signOut()
      window.location.reload()
    }
    
    async function init() {
      const hash = window.location.hash.substring(1)
      
      if (hash.includes('access_token')) {
        const p = new URLSearchParams(hash)
        const token = p.get('access_token')
        const payload = decodeJWT(token)
        window.history.replaceState({}, '', window.location.pathname)
        
        if (payload) {
          showUser({ token, payload, refresh_token: p.get('refresh_token') })
          return
        }
      }
      
      const { data: { session } } = await supabase.auth.getSession()
      if (session) {
        showUser({ token: session.access_token, payload: decodeJWT(session.access_token) })
      }
    }
    
    function showUser({ token, payload }) {
      document.getElementById('login').style.display = 'none'
      document.getElementById('user').innerHTML = `
        <p><strong>Email: </strong>${payload.email}</p>
        <p><strong>User ID: </strong>${payload.sub}</p>
        <p><strong>Authentication Method: </strong>${payload.amr?.[0]?.method}</p>
        <button onclick="signOut()">Sign out</button>
      `
    }
    
    document.addEventListener('DOMContentLoaded', init)
  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>SSO Login</title>
    </head>
    <body>
      <div id="login">
        <button onclick="signInWithAuth0()">Sign in with Auth0</button>
      </div>
      <div id="user"></div>
      <script type="module" src="/main.js"></script>
    </body>
    </html>

The code above implements an Auth0 SSO sign-in flow.

SSO sign-in page

Successful SSO sign-in

You can view SSO user records in the auth.users table of your Supabase project. The is_sso_user field for these users is set to TRUE.

SSO user records in auth.users table

Manage providers

Use the following API operations to manage SSO providers. All operations require the service role key.

List providers

curl 'https://<your-supabase-url>/auth/v1/admin/sso/providers' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>'

You can also check the auth.sso_domains and auth.sso_providers tables in the Supabase Dashboard to verify registered providers.

View SSO providers in Dashboard

Get provider

curl 'https://<your-supabase-url>/auth/v1/admin/sso/providers/<provider-id>' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>'

Update provider

curl -X PUT 'https://<your-supabase-url>/auth/v1/admin/sso/providers/<provider-id>' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>' \
  -H 'Content-Type: application/json' \
  -d '{"metadata_url": "https://<your-idp>/samlp/metadata/<client-id>"}'

Delete provider

curl -X DELETE 'https://<your-supabase-url>/auth/v1/admin/sso/providers/<provider-id>' \
  -H 'apikey: <your-service-role-key>' \
  -H 'Authorization: Bearer <your-service-role-key>'

FAQ

Page does not redirect after sign-in

Verify that flowType: 'implicit' is set in the createClient configuration. The default PKCE flow may cause issues in SAML callback scenarios.

Sign-in page remains after callback

The SDK initializes asynchronously. Manually parse the access_token from the URL hash on DOMContentLoaded. Do not rely solely on supabase.auth.getSession() to handle the callback.

Sign-in failed: No such SSO provider

  1. Check whether the providerId in your frontend code matches the ID that was returned during registration.

  2. Perform a hard refresh of your browser (Cmd+Shift+R or Ctrl+Shift+R) to clear the cache.

Sign-in failed: Failed to fetch

  1. Check your network connection to the Supabase project.

  2. Run a test command in the browser console to verify connectivity.

Incorrect redirect after sign-in

Set the frontend URL in options.redirectTo of signInWithSSO(). This overrides the default Site URL.

SAML RelayState has expired

SSO sign-in URLs expire after a few minutes. Call signInWithSSO() for each new attempt. Do not reuse old URLs.

Auth0 error: invalid_request

Check that Allowed Callback URLs in your Auth0 settings includes your frontend domain, and that the Application Callback URL is set to the Supabase ACS URL.