Alternar tema

Configuração avançada do Supabase Auth: OAuth, SSO e controle de acesso

Easton editorial illustration: solo-founder business system console

Naquela tarde, um colega da equipe de vendas veio falar comigo: “O cliente exige login com Okta e precisamos colocar o SSO no ar em duas semanas.” Eu travei por um instante — meu aplicativo só aceitava cadastro por e-mail e Google OAuth. SAML? Eu nunca tinha trabalhado com isso.

Esse cenário é muito comum em SaaS B2B. Clientes empresariais não aceitam que seus funcionários criem contas separadas no seu produto. Eles já têm um sistema centralizado de identidade, como Okta, Azure AD ou Google Workspace, e esperam um redirecionamento de login em um clique, além da revogação automática do acesso quando alguém deixa a empresa.

Este artigo foi preparado justamente para esse caso. Começaremos pelo login social com OAuth, passaremos à integração corporativa com SAML SSO e, por fim, usaremos Row Level Security (RLS) para isolar permissões em um ambiente multitenant. O resultado é uma solução completa de autenticação e autorização, capaz de atender desde produtos de consumo até sistemas empresariais.

1. Configuração prática de vários provedores OAuth

O login social com OAuth é o ponto de partida da maioria dos aplicativos. O usuário não quer memorizar outra senha, e você também não quer cuidar do armazenamento e da validação de senhas. Deixar essa tarefa com Google ou GitHub facilita a vida dos dois lados.

O Supabase oferece suporte a muitos provedores OAuth: Google, GitHub, Apple, Facebook, Discord, Twitter e outros. Em produção, porém, Google e GitHub são os mais usados, enquanto Apple é uma exigência para apps iOS durante a análise da App Store. Vamos começar pelo Google.

Configuração do Google OAuth

Ao abrir o Google Cloud Console, aquela quantidade de menus pode parecer intimidadora. Não se preocupe: pesquise diretamente por “OAuth Client ID” para encontrar a opção correta.

Na criação, escolha o tipo Web application. A etapa mais importante é preencher o Authorized redirect URI:

https://<你的项目ref>.supabase.co/auth/v1/callback

Esse é o endereço no qual o serviço Supabase Auth recebe o callback do OAuth. O ref do projeto aparece no canto superior esquerdo do Supabase Dashboard e tem um formato semelhante a abcdefghijklmnop.

Depois de obter o Client ID e o Client Secret, volte ao Supabase Dashboard, acesse Authentication > Providers, ative o Google e informe os dois valores.

A chamada no código é simples:

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

const supabase = createClient(
  'https://abcdefghijklmnop.supabase.co',
  'your-anon-key'
)

// Inicia o login com OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: 'https://your-app.com/auth/callback',
    scopes: 'email profile'
  }
})

if (error) {
  console.error('登录失败:', error.message)
  return
}

// data.url é a URL de redirecionamento; basta usar window.location.href = data.url

redirectTo é o endereço no seu aplicativo que recebe o resultado do login. O Supabase enviará para ele os parâmetros code e state. Nessa página, chame supabase.auth.exchangeCodeForSession() para concluir o login:

// Na página /auth/callback
const { error } = await supabase.auth.exchangeCodeForSession()
if (!error) {
  // Login concluído; redireciona para a página inicial
  window.location.href = '/'
}

Configuração do GitHub OAuth

A configuração do GitHub é parecida. A opção fica em Settings > Developer settings > OAuth Apps. Ao criar o aplicativo, use novamente https://<项目ref>.supabase.co/auth/v1/callback como Authorization callback URL.

Há uma diferença: o GitHub OAuth App não tem uma tela de configuração de scopes. Eles são definidos no código:

await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: 'https://your-app.com/auth/callback',
    scopes: 'repo user'  // repo permite acessar repositórios privados
  }
})

Se o objetivo for apenas autenticar o usuário, os scopes padrão são suficientes. Se você precisar acessar dados da conta no GitHub, como sincronizar a lista de repositórios, será necessário incluir o scope repo.

Configuração do Apple OAuth

A configuração da Apple é a mais trabalhosa. Você precisa criar um Services ID no Apple Developer Portal e gerar uma chave privada, um arquivo .p8 que só pode ser baixado uma vez. Se perder o arquivo, será necessário gerar outra chave.

Parâmetros principais:

  • Services ID: equivalente ao Client ID
  • Team ID: disponível na página Membership
  • Key ID: o ID da chave privada
  • Private Key: o conteúdo do arquivo .p8 baixado

Ao informar esses valores no Supabase Dashboard, copie todo o conteúdo da Private Key, incluindo as linhas BEGIN e END.

A chamada é igual à usada com Google e GitHub:

await supabase.auth.signInWithOAuth({
  provider: 'apple',
  options: {
    redirectTo: 'https://your-app.com/auth/callback'
  }
})

Em apps iOS, existe outra opção: chamar diretamente a API nativa Sign in with Apple e enviar o identity token obtido ao Supabase:

// O frontend recebe o identity_token retornado pela Apple
const { data, error } = await supabase.auth.signInWithIdTokenCredentials({
  provider: 'apple',
  token: identityToken
})

Essa abordagem é adequada para um app iOS que já tenha o login nativo da Apple integrado, pois permite reutilizar a lógica existente.

2. Integração corporativa com SAML SSO

Voltemos ao cenário inicial: o cliente exige login com Okta. Nesse caso, OAuth já não é suficiente; você precisa de SAML 2.0 SSO.

O funcionamento do SAML é totalmente diferente do OAuth. No OAuth, o usuário autoriza um aplicativo de terceiros a acessar seus dados. No SAML, o provedor de identidade corporativo (IdP) envia as informações de identidade do usuário ao seu aplicativo, que atua como Service Provider (SP). Para a empresa, o SAML oferece mais controle: quando o IdP desativa a conta de um funcionário que saiu, o acesso dele é revogado automaticamente em todos os aplicativos integrados.

Preparação antes da configuração

Você precisa obter com o cliente o Metadata do IdP. Esse arquivo contém o certificado, os endpoints e outras informações do provedor. Okta, Azure AD e Google Workspace oferecem uma opção para exportar o Metadata.

URLs importantes fornecidas pelo Supabase:

EntityID (identificador do SP):
https://<项目ref>.supabase.co/auth/v1/sso/saml/metadata

ACS URL (endereço que recebe a SAML Response):
https://<项目ref>.supabase.co/auth/v1/sso/saml/acs

Metadata URL (para download):
https://<项目ref>.supabase.co/auth/v1/sso/saml/metadata?download=true

Envie essas URLs ao administrador de TI do cliente para que ele crie o aplicativo SAML no Okta ou Azure AD.

Configuração do SSO com a Supabase CLI

O Supabase Dashboard também permite configurar SAML, mas eu prefiro usar a CLI. A configuração de clientes empresariais costuma exigir vários ciclos de ajuste, e a linha de comando oferece mais controle.

# Adiciona uma conexão SAML
supabase sso add --type saml --project-ref <项目ref> \
  --metadata-url 'https://company.okta.com/app/exk123/saml/samlmetadata' \
  --domains company.com

O parâmetro --domains é importante. Ele informa ao Supabase que todos os usuários com e-mail no domínio company.com devem usar essa conexão SAML ao entrar.

Você também pode usar --metadata-file em vez de --metadata-url para enviar diretamente um arquivo XML:

supabase sso add --type saml --project-ref <项目ref> \
  --metadata-file ./okta-metadata.xml \
  --domains company.com

O comando retorna um sso_provider_id, semelhante a abc123def456. Esse ID será essencial para o isolamento de permissões com RLS.

Configuração do Attribute Mapping

A SAML Response contém informações do usuário, como e-mail, nome e departamento, mas os nomes dos campos podem variar. Você precisa informar ao Supabase como mapeá-los.

Crie um arquivo JSON:

{
  "keys": {
    "email": {
      "name": "email",
      "names": ["EmailAddress", "email", "mail"],
      "required": true
    },
    "first_name": {
      "name": "first_name",
      "names": ["FirstName", "givenName", "first_name"]
    },
    "last_name": {
      "name": "last_name",
      "names": ["LastName", "surname", "last_name"]
    }
  }
}

Em seguida, aplique a configuração com supabase sso update:

supabase sso update <sso_provider_id> \
  --project-ref <项目ref> \
  --attribute-mapping-file ./mapping.json

Configuração de SSO multitenant

Um projeto pode ter várias conexões SAML. Imagine, por exemplo, dois clientes empresariais: Acme Corp usa Okta, enquanto Globex Inc usa Azure AD.

# Adiciona o SSO da Acme Corp
supabase sso add --type saml --project-ref <项目ref> \
  --metadata-url 'https://acme.okta.com/.../metadata' \
  --domains acme.com

# Retorna sso_provider_id: provider_abc

# Adiciona o SSO da Globex Inc
supabase sso add --type saml --project-ref <项目ref> \
  --metadata-url 'https://globex.azure.com/.../metadata' \
  --domains globex.com

# Retorna sso_provider_id: provider_def

Agora, funcionários da Acme Corp usam provider_abc no login, enquanto funcionários da Globex Inc usam provider_def. Os dados dos usuários de cada conexão SSO ficam isolados.

Experiência de login do usuário

Depois da configuração, o fluxo de login acontece assim:

  1. O usuário informa o e-mail, por exemplo, [email protected].
  2. O Supabase detecta que o domínio acme.com tem uma configuração SSO.
  3. O usuário é redirecionado automaticamente à página de login da Acme no Okta.
  4. Ele informa usuário e senha no Okta; se já estiver autenticado, a etapa é concluída diretamente.
  5. O Okta envia uma SAML Response ao Supabase.
  6. Depois de validar a resposta, o Supabase cria a session e redireciona o usuário de volta ao aplicativo.

No código, inicie o login SSO desta forma:

// Chame este método depois que o usuário informar o e-mail
const { data, error } = await supabase.auth.signInWithSSO({
  domain: 'acme.com'
})

if (data?.url) {
  // Redireciona para a página de login SSO
  window.location.href = data.url
}

Também é possível usar diretamente o sso_provider_id:

const { data, error } = await supabase.auth.signInWithSSO({
  providerId: 'provider_abc'
})

Características dos usuários SSO

Usuários criados por login SSO são diferentes dos usuários comuns:

  • O e-mail é administrado pelo IdP: o usuário não pode alterá-lo no aplicativo.
  • Não há senha no Supabase: ela fica armazenada no IdP, e o Supabase não participa da validação.
  • O e-mail não precisa ser verificado no Supabase: o IdP já fez essa verificação.

Portanto, se o usuário precisar alterar o endereço de e-mail, o administrador de TI do cliente deverá fazer isso no Okta ou Azure AD.

3. Uso avançado de Row Level Security

A autenticação responde à pergunta “quem é o usuário?”. A autorização responde a “o que esse usuário pode fazer?”.

Na abordagem tradicional, as permissões são verificadas no código de negócio: cada API precisa conferir se o usuário atual pode acessar determinado dado. Isso gera repetição, facilita omissões e pode prejudicar o desempenho, pois cada requisição precisa consultar o banco para decidir o acesso.

O Row Level Security (RLS) do PostgreSQL transfere essa verificação para a camada do banco de dados. Cada consulta recebe automaticamente um filtro de permissões, sem exigir verificações no código de negócio.

Comportamento padrão depois de ativar o RLS

Muita gente tropeça neste detalhe: depois que o RLS é ativado, todo acesso é negado por padrão.

-- Ativa o RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Neste momento, qualquer consulta retorna vazio, inclusive para administradores
SELECT * FROM posts;  -- Retorna 0 linhas

É necessário criar uma Policy para permitir o acesso.

As duas partes principais de uma Policy

Uma Policy tem duas cláusulas: USING e WITH CHECK.

Cláusula USING: determina se o usuário pode “ver” ou “localizar” aquele registro. É usada em operações SELECT, UPDATE e DELETE. O usuário precisa conseguir ver o dado antes de alterá-lo ou excluí-lo.

Cláusula WITH CHECK: determina se o usuário pode “gravar” aquele registro. É usada em operações INSERT e UPDATE. O dado resultante da gravação precisa atender à condição.

-- Usuários só podem manipular os próprios posts
CREATE POLICY "Users manage own posts" ON posts
FOR ALL TO authenticated
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);

-- Permite apenas visualizar, não alterar
CREATE POLICY "Users view own posts" ON posts
FOR SELECT TO authenticated
USING (auth.uid() = author_id);

-- Permite apenas inserir, sem visualizar posts de outras pessoas
CREATE POLICY "Users insert posts" ON posts
FOR INSERT TO authenticated
WITH CHECK (auth.uid() = author_id);

auth.uid() retorna o UUID do usuário autenticado. Para um usuário sem login, retorna null; por isso, TO authenticated garante que a Policy só seja aplicada a usuários autenticados.

RESTRICTIVE e PERMISSIVE

Uma tabela pode ter várias Policies. O padrão é o modo PERMISSIVE: basta atender a uma delas para obter acesso.

-- Policy 1: o usuário pode ver os próprios dados
CREATE POLICY "Own data" ON posts
FOR SELECT USING (auth.uid() = author_id);

-- Policy 2: qualquer pessoa pode ver posts públicos
CREATE POLICY "Public posts" ON posts
FOR SELECT USING (is_public = true);

-- Duas Policies PERMISSIVE: basta atender a uma delas

Uma Policy RESTRICTIVE, por sua vez, define uma condição obrigatória. Ela é combinada com Policies PERMISSIVE para criar restrições mais rigorosas.

-- Todo acesso precisa atender a esta condição, usada como um filtro global
CREATE POLICY "Tenant isolation" ON posts
AS RESTRICTIVE TO authenticated
USING (tenant_id = (
  SELECT tenant_id FROM users WHERE id = auth.uid()
));

-- Uma Policy PERMISSIVE adicional controla uma operação específica
CREATE POLICY "Authors edit own posts" ON posts
FOR UPDATE USING (auth.uid() = author_id);

O resultado dessa combinação é o seguinte: o usuário precisa pertencer ao tenant correto, por causa da RESTRICTIVE, e também ser o autor para editar, por causa da PERMISSIVE.

Padrão completo de isolamento multitenant

Suponha que você tenha um aplicativo SaaS no qual cada cliente empresarial é um tenant. A tabela de usuários tem um tenant_id, e todas as tabelas de negócio precisam ser isoladas por tenant.

-- Tabela de usuários
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL,
  email TEXT,
  sso_provider_id TEXT  -- Presente apenas para usuários SSO
);

-- Tabela de negócio
CREATE TABLE projects (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL,
  name TEXT,
  created_by UUID REFERENCES users(id)
);

-- Ativa o RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- Isolamento global por tenant (RESTRICTIVE)
CREATE POLICY "Tenant isolation" ON projects
AS RESTRICTIVE TO authenticated
USING (tenant_id = (
  SELECT tenant_id FROM users WHERE id = auth.uid()
));

-- Permissões de operações específicas (PERMISSIVE)
CREATE POLICY "Tenant users can view" ON projects
FOR SELECT TO authenticated
USING (true);  -- O filtro RESTRICTIVE já foi aplicado, então esta Policy permite a operação

CREATE POLICY "Project creators can edit" ON projects
FOR UPDATE TO authenticated
USING (created_by = auth.uid());

Com essa configuração, toda consulta recebe automaticamente o filtro do tenant. Mesmo que o código de negócio execute SELECT * FROM projects, o banco retorna somente os dados do tenant atual.

Isolamento multitenant para usuários SSO

Usuários autenticados por SSO podem ser isolados por sso_provider_id. O JWT tem um campo que armazena o método de login:

-- RLS Policy para usuários SSO
CREATE POLICY "SSO tenant isolation" ON organization_settings
AS RESTRICTIVE TO authenticated
USING (sso_provider_id = (
  SELECT auth.jwt()#>>'{amr,0,provider}'
));

auth.jwt()#>>'{amr,0,provider}' extrai do JWT o ID do provedor usado no login. Em um login SSO, esse valor corresponde ao sso_provider_id.

Pontos importantes para otimizar o desempenho

Uma RLS Policy funciona como uma cláusula WHERE implícita. Ela é adicionada automaticamente a cada consulta e pode conter subconsultas, o que afeta o desempenho.

Algumas recomendações:

1. Crie índices para os campos usados nas Policies

CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_projects_tenant ON projects(tenant_id);

2. Evite subconsultas nas Policies

Uma subconsulta pode ser executada para cada linha, gerando um custo alto. A alternativa melhor é armazenar tenant_id no JWT e extraí-lo diretamente com auth.jwt():

-- Não recomendado: subconsulta
USING (tenant_id = (SELECT tenant_id FROM users WHERE id = auth.uid()))

-- Recomendado: tenant_id armazenado no JWT
USING (tenant_id = (auth.jwt()->>'tenant_id')::uuid)

Mais adiante, veremos como usar um Custom Access Token Hook para incluir tenant_id no JWT.

3. Encapsule lógicas complexas em uma função SECURITY DEFINER

Uma lógica complexa de Policy pode ser encapsulada em uma função marcada como SECURITY DEFINER, evitando a repetição da execução:

CREATE FUNCTION current_tenant_id() RETURNS UUID
LANGUAGE SQL STABLE SECURITY DEFINER AS $$
  SELECT tenant_id FROM users WHERE id = auth.uid();
$$;

-- Chama a função na Policy
CREATE POLICY "Tenant isolation" ON projects
AS RESTRICTIVE USING (tenant_id = current_tenant_id());

STABLE indica que o valor retornado pela função não muda durante a mesma transação, permitindo que o PostgreSQL otimize a chamada para executá-la uma única vez.

4. Implementação de Custom Claims e RBAC

Por padrão, o JWT contém apenas os campos internos do Supabase, como ID do usuário, e-mail e papel authenticated ou anon. Em muitos casos, porém, você precisa de mais informações, como papel do usuário (admin ou moderator), ID do tenant e lista de permissões.

O Supabase oferece o Custom Access Token Hook, que permite modificar o conteúdo do JWT antes de sua emissão.

Por que usar Custom Claims

Alguns casos comuns:

  1. Controle de acesso com RBAC: armazene o papel do usuário no JWT para que as Policies RLS decidam as permissões com base nele.
  2. Isolamento multitenant: armazene tenant_id no JWT para evitar subconsultas nas Policies.
  3. Redução do tamanho do JWT: o JWT padrão contém muitos campos; você pode remover os desnecessários para reduzir o custo de transmissão em cenários SSR.

O JWT padrão do Supabase tem diversos campos, como session_id, aal e amr, e acompanha todas as requisições. Se o aplicativo tiver muitas páginas SSR e transmitir o JWT em cookies, o tamanho do token poderá afetar o desempenho.

Implementação do Custom Access Token Hook

O Hook é uma função PL/pgSQL executada antes da geração do JWT. Ela pode adicionar, alterar ou remover claims.

-- Cria a função do Hook
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
  claims jsonb;
  user_role text;
  tenant_id uuid;
BEGIN
  -- Obtém os claims do event
  claims := event->'claims';

  -- Obtém o papel do usuário na tabela user_roles
  SELECT role INTO user_role
  FROM public.user_roles
  WHERE user_id = (event->>'user_id')::uuid;

  -- Adiciona o papel aos claims, se existir
  IF user_role IS NOT NULL THEN
    claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
  END IF;

  -- Obtém tenant_id na tabela users
  SELECT tenant_id INTO tenant_id
  FROM public.users
  WHERE id = (event->>'user_id')::uuid;

  IF tenant_id IS NOT NULL THEN
    claims := jsonb_set(claims, '{tenant_id}', to_jsonb(tenant_id));
  END IF;

  -- Retorna os claims modificados
  RETURN jsonb_build_object('claims', claims);
END;
$$;

-- Concede ao supabase_auth_admin permissão para executar a função
GRANT EXECUTE ON FUNCTION public.custom_access_token_hook(jsonb)
TO supabase_auth_admin;

-- Ative este Hook no Supabase Dashboard
-- Authentication > Hooks > Custom Access Token > selecione a função acima

O parâmetro de entrada event do Hook contém:

  • user_id: UUID do usuário atual
  • claims: claims atuais do JWT
  • authentication_method: método de login, como password, oauth ou sso/saml

Os claims retornados são mesclados ao JWT final.

Estrutura das tabelas RBAC

Um sistema completo de papéis e permissões precisa de algumas tabelas:

-- Define o tipo de papel
CREATE TYPE app_role AS ENUM ('admin', 'moderator', 'user');

-- Define o tipo de permissão
CREATE TYPE app_permission AS ENUM (
  'posts.delete',      -- Excluir qualquer post
  'posts.pin',         -- Fixar um post
  'users.manage',      -- Gerenciar usuários
  'settings.edit'      -- Editar configurações
);

-- Tabela de usuários e papéis
CREATE TABLE public.user_roles (
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  role app_role NOT NULL,
  PRIMARY KEY (user_id, role)
);

-- Tabela de papéis e permissões
CREATE TABLE public.role_permissions (
  role app_role NOT NULL,
  permission app_permission NOT NULL,
  PRIMARY KEY (role, permission)
);

-- Insere as permissões padrão
INSERT INTO role_permissions (role, permission) VALUES
  ('admin', 'posts.delete'),
  ('admin', 'posts.pin'),
  ('admin', 'users.manage'),
  ('admin', 'settings.edit'),
  ('moderator', 'posts.delete'),
  ('moderator', 'posts.pin');

Função authorize() para verificar permissões

Além das tabelas de papéis e permissões, precisamos de uma função que verifique se o usuário tem determinada permissão:

CREATE OR REPLACE FUNCTION public.authorize(
  requested_permission app_permission
)
RETURNS boolean
LANGUAGE plpgsql
STABLE SECURITY DEFINER
AS $$
DECLARE
  user_role app_role;
BEGIN
  -- Obtém o papel do usuário no JWT
  SELECT (auth.jwt()->>'user_role')::app_role
  INTO user_role;

  -- Retorna false se o JWT não contiver um papel
  IF user_role IS NULL THEN
    RETURN false;
  END IF;

  -- Verifica se o papel contém a permissão solicitada
  RETURN EXISTS (
    SELECT 1 FROM public.role_permissions
    WHERE role = user_role
    AND permission = requested_permission
  );
END;
$$;

-- Concede permissão ao papel authenticated
GRANT EXECUTE ON FUNCTION public.authorize(app_permission)
TO authenticated;

Uso de authorize() em uma RLS Policy

Agora podemos chamar authorize() em uma Policy para verificar permissões:

-- Apenas admin e moderator podem excluir posts
CREATE POLICY "Role-based delete" ON posts
FOR DELETE TO authenticated
USING (
  authorize('posts.delete') OR auth.uid() = author_id
);

-- Apenas admin pode fixar posts
CREATE POLICY "Admin pin posts" ON posts
FOR UPDATE TO authenticated
USING (
  NOT is_pinned OR authorize('posts.pin')
);

A lógica da primeira Policy é esta: o usuário pode excluir o post se tiver a permissão posts.delete, concedida a admin e moderator, ou se for o autor.

A segunda Policy determina que, ao alterar um post para definir is_pinned como true, o usuário precisa ter a permissão posts.pin.

Leitura de Custom Claims no frontend

Os Custom Claims ficam no JWT. Para acessá-los no frontend, decodifique o access_token:

import { jwtDecode } from 'jwt-decode'

// Obtém a session
const { data: { session } } = await supabase.auth.getSession()

if (session) {
  const decoded = jwtDecode(session.access_token)
  console.log('用户角色:', decoded.user_role)
  console.log('租户ID:', decoded.tenant_id)
}

Observe que jwtDecode apenas decodifica o JWT; ele não valida a assinatura. O frontend não precisa fazer essa validação, pois ela é realizada no serviço do Supabase.

5. Cenário completo de um SaaS empresarial

Agora vamos combinar tudo o que vimos para montar uma solução completa de autenticação para um SaaS empresarial.

Nesse cenário, o produto SaaS tem dois tipos de usuário:

  1. Usuários empresariais: entram pelo SSO da empresa cliente, como Okta ou Azure AD, e pertencem a um tenant.
  2. Usuários individuais: entram com Google ou GitHub OAuth, não pertencem a um tenant e só podem acessar recursos públicos.

O requisito de isolamento é claro: usuários empresariais só podem ver os dados do próprio tenant, enquanto usuários individuais não podem acessar dados empresariais.

Estrutura da tabela de usuários

CREATE TABLE public.users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT NOT NULL,

  -- Origem da autenticação
  auth_type TEXT NOT NULL DEFAULT 'oauth',  -- 'oauth' ou 'sso'

  -- Campos exclusivos de usuários SSO
  sso_provider_id TEXT,  -- ID da conexão SSO
  tenant_id UUID,        -- Tenant ao qual o usuário pertence

  -- Informações básicas
  full_name TEXT,
  avatar_url TEXT,

  -- Timestamps
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

-- Tabela de tenants
CREATE TABLE public.tenants (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  sso_provider_id TEXT NOT NULL UNIQUE,  -- Relacionado à conexão SSO
  plan_type TEXT NOT NULL DEFAULT 'team',  -- 'team' ou 'enterprise'
  created_at TIMESTAMPTZ DEFAULT now()
);

Tratamento unificado no Custom Access Token Hook

O Hook precisa diferenciar os dois tipos de usuário:

CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
  claims jsonb;
  user_record RECORD;
BEGIN
  claims := event->'claims';

  -- Obtém as informações do usuário
  SELECT auth_type, sso_provider_id, tenant_id, role
  INTO user_record
  FROM public.users
  WHERE id = (event->>'user_id')::uuid;

  -- Ignora se o usuário ainda não existir, como em um cadastro recente
  IF user_record IS NULL THEN
    RETURN jsonb_build_object('claims', claims);
  END IF;

  -- Adiciona o tipo de autenticação
  claims := jsonb_set(claims, '{auth_type}', to_jsonb(user_record.auth_type));

  -- Para usuários SSO, adiciona tenant_id e sso_provider_id
  IF user_record.auth_type = 'sso' THEN
    IF user_record.tenant_id IS NOT NULL THEN
      claims := jsonb_set(claims, '{tenant_id}', to_jsonb(user_record.tenant_id));
    END IF;
    IF user_record.sso_provider_id IS NOT NULL THEN
      claims := jsonb_set(claims, '{sso_provider_id}', to_jsonb(user_record.sso_provider_id));
    END IF;
  END IF;

  -- Adiciona o papel do usuário, se existir
  IF user_record.role IS NOT NULL THEN
    claims := jsonb_set(claims, '{user_role}', to_jsonb(user_record.role));
  END IF;

  RETURN jsonb_build_object('claims', claims);
END;
$$;

Fluxo de criação do usuário depois do login

Tanto o login por OAuth quanto por SSO cria um registro na tabela auth.users. Depois que o login for concluído, precisamos sincronizar as informações do usuário com a tabela public.users.

Isso pode ser tratado com o mfa_verification_hook do Auth Hooks ou no código de negócio:

// Na página de callback da autenticação, depois de concluir o login
const { data: { user } } = await supabase.auth.getUser()

if (user) {
  // Verifica se o usuário já existe em public.users
  const { data: existingUser } = await supabase
    .from('users')
    .select('id')
    .eq('id', user.id)
    .single()

  if (!existingUser) {
    // Identifica o método de login
    const authType = user.app_metadata?.provider || 'oauth'

    // Cria o registro do usuário
    await supabase.from('users').insert({
      id: user.id,
      email: user.email,
      auth_type: authType.startsWith('sso') ? 'sso' : 'oauth',
      sso_provider_id: user.app_metadata?.sso_provider_id,
      tenant_id: null,  // Será atribuído depois pelo administrador
      full_name: user.user_metadata?.full_name,
      avatar_url: user.user_metadata?.avatar_url
    })
  }
}

RLS Policy unificada

As tabelas de negócio precisam de uma RLS Policy unificada que trate usuários OAuth e SSO:

-- Considerando uma tabela projects
CREATE TABLE public.projects (
  id UUID PRIMARY KEY,
  tenant_id UUID REFERENCES tenants(id),
  name TEXT NOT NULL,
  is_public BOOLEAN DEFAULT false,
  created_by UUID REFERENCES users(id)
);

ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;

-- Policy de isolamento global (RESTRICTIVE)
CREATE POLICY "Tenant or public access" ON public.projects
AS RESTRICTIVE TO authenticated
USING (
  -- Usuário SSO: tenant_id precisa ser igual
  (auth.jwt()->>'auth_type' = 'sso'
   AND tenant_id = (auth.jwt()->>'tenant_id')::uuid)
  OR
  -- Usuário OAuth: só pode ver projetos públicos
  (auth.jwt()->>'auth_type' = 'oauth' AND is_public = true)
);

-- Permite visualizar (PERMISSIVE)
CREATE POLICY "Users can view" ON public.projects
FOR SELECT TO authenticated
USING (true);

-- Permite criar somente para usuários de um tenant
CREATE POLICY "Tenant users can create" ON public.projects
FOR INSERT TO authenticated
WITH CHECK (
  auth.jwt()->>'auth_type' = 'sso'
  AND tenant_id = (auth.jwt()->>'tenant_id')::uuid
);

-- Permite editar para o criador ou um administrador
CREATE POLICY "Creators or admins can edit" ON public.projects
FOR UPDATE TO authenticated
USING (
  created_by = auth.uid()
  OR authorize('projects.edit')
);

A lógica desse conjunto de Policies é:

  • Usuários SSO só podem ver os projetos do próprio tenant.
  • Usuários OAuth só podem ver projetos públicos.
  • Apenas usuários SSO podem criar projetos, pois precisam pertencer a um tenant.
  • A edição é permitida ao criador ou a um administrador com a permissão projects.edit.

Atribuição de tenant pelo administrador do cliente

No primeiro login, o tenant_id de um usuário SSO é null. Um administrador da empresa precisa atribuí-lo manualmente:

-- O administrador adiciona o usuário ao tenant
UPDATE public.users
SET tenant_id = '<租户UUID>'
WHERE id = '<用户UUID>';

-- Define o papel do usuário
INSERT INTO public.user_roles (user_id, role)
VALUES ('<用户UUID>', 'admin');

Esse processo pode ser transformado em uma tela administrativa ou automatizado com Auth Hooks, mapeando anonimamente o provedor SSO ao tenant correspondente.

Diagrama do fluxo completo

Login do usuário
   │
   ├─ Usuário OAuth
   │    │
   │    ├─ Callback do Google/GitHub
   │    ├─ Supabase cria auth.users
   │    ├─ Código de negócio cria public.users (auth_type='oauth')
   │    └─ JWT: { auth_type: 'oauth' }
   │    │
   │    └─ RLS: só pode ver dados com is_public=true
   │
   └─ Usuário SSO
        │
        ├─ Callback SAML do Okta/Azure AD
        ├─ Supabase cria auth.users (com sso_provider_id)
        ├─ Código de negócio cria public.users (auth_type='sso')
        ├─ Administrador atribui tenant_id
        ├─ Custom Hook adiciona tenant_id ao JWT
        └─ JWT: { auth_type: 'sso', tenant_id: '...' }
        │
        └─ RLS: só pode ver dados com tenant_id correspondente

Começamos pelo login social com OAuth, avançamos para a integração empresarial com SAML SSO e, por fim, construímos uma estrutura completa de permissões com RLS e Custom Claims. Essa solução atende a cenários que vão de produtos individuais a SaaS empresarial.

Alguns pontos decisivos:

Se a maioria dos usuários é composta por consumidores individuais: OAuth com Google ou GitHub é suficiente. A configuração é simples, o fluxo já é familiar para o usuário e o custo de manutenção é baixo. Uma implementação básica de RLS, na qual cada usuário só acessa os próprios dados, também basta.

Se você desenvolve um SaaS B2B: SSO é um recurso essencial. Clientes empresariais costumam exigi-lo e podem descartar uma solução que não o ofereça. Prepare as integrações com Okta, Azure AD e Google Workspace e planeje a arquitetura multitenant.

Se você desenvolve uma aplicação empresarial complexa: combinar RBAC e RLS é a abordagem padrão. Tabelas de papéis e permissões, Custom Claims e a função authorize() formam uma solução capaz de lidar com regras detalhadas, como “administradores podem excluir, mas não fixar”, ou “membros do tenant podem editar, mas não excluir”.

Como próximo passo, comece com OAuth e valide o fluxo básico. Quando o projeto amadurecer e surgirem clientes empresariais, acrescente SSO e RBAC. A arquitetura do Supabase permite evoluir gradualmente, sem configurar todos os recursos desde o início.

Processo completo de configuração empresarial do Supabase Auth

Etapas completas, do login social com OAuth à integração corporativa com SAML SSO e ao isolamento de permissões multitenant com RLS

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: Configurar provedores OAuth (Google/GitHub/Apple)

    1. Crie um OAuth App no console do provedor (Google Cloud Console / GitHub Settings)
    2. Defina a URL de callback: https://<项目ref>.supabase.co/auth/v1/callback
    3. Ative o provedor no Supabase Dashboard e informe Client ID e Secret
    4. Chame signInWithOAuth() no código, especificando scopes e redirectTo
  2. 2

    Step 2: Configurar a integração corporativa com SAML SSO

    1. Obtenha o arquivo ou a URL de Metadata do IdP corporativo (Okta/Azure AD)
    2. Adicione a conexão SSO pela CLI: supabase sso add --type saml --domains company.com
    3. Configure o Attribute Mapping para mapear campos como email e name
    4. Teste o fluxo de login: após informar o e-mail, o usuário deve ser redirecionado automaticamente ao IdP
  3. 3

    Step 3: Implementar isolamento multitenant com RLS

    1. Adicione o campo tenant_id às tabelas de negócio
    2. Ative o RLS: ALTER TABLE projects ENABLE ROW LEVEL SECURITY
    3. Crie uma Policy RESTRICTIVE para aplicar um filtro global por tenant
    4. Crie Policies PERMISSIVE para controlar permissões de operações específicas (SELECT/INSERT/UPDATE)
    5. Adicione um índice ao campo tenant_id para otimizar o desempenho
  4. 4

    Step 4: Configurar o Custom Access Token Hook

    1. Crie a função public.custom_access_token_hook()
    2. Adicione custom claims como tenant_id e user_role na função
    3. Conceda ao supabase_auth_admin permissão para executar a função
    4. Ative o Hook no Supabase Dashboard (Authentication > Hooks)
    5. Leia os custom claims no frontend com jwtDecode()
  5. 5

    Step 5: Implementar controle de acesso com RBAC

    1. Crie as tabelas user_roles e role_permissions
    2. Defina os tipos enumerados app_role e app_permission
    3. Crie a função authorize() para verificar as permissões do usuário
    4. Chame authorize('permission.name') nas Policies RLS
    5. Exiba ou oculte elementos da interface no frontend com base no user_role presente no JWT

FAQ

Qual é a diferença entre OAuth e SAML SSO? Qual devo escolher?
OAuth é adequado a aplicativos voltados ao consumidor: o usuário entra com uma conta Google ou GitHub, a configuração é simples e o custo de manutenção é baixo. SAML SSO é adequado a aplicativos empresariais B2B, nos quais o cliente exige que os funcionários entrem com a conta corporativa centralizada (Okta/Azure AD); as contas são administradas pela empresa e perdem o acesso automaticamente quando o funcionário sai.

Se você desenvolve um SaaS B2B, SSO é um recurso essencial; para um produto voltado a pessoas físicas, OAuth costuma ser suficiente.
Ativei o RLS e a consulta retornou um resultado vazio. O que fazer?
Esse é o comportamento padrão do RLS: depois de ativado, ele nega todo acesso por padrão. É necessário criar uma Policy para permitir o acesso. Por exemplo: CREATE POLICY "Users view own posts" ON posts FOR SELECT TO authenticated USING (auth.uid() = author_id);
Como otimizar uma RLS Policy com baixo desempenho?
Há três caminhos principais:

• Crie índices para os campos usados na Policy: CREATE INDEX idx_tenant ON projects(tenant_id);
• Evite subconsultas na Policy: use um Custom Access Token Hook para incluir tenant_id no JWT e extraia o valor diretamente com auth.jwt()->>'tenant_id'
• Encapsule lógicas complexas em uma função SECURITY DEFINER; o PostgreSQL pode otimizá-la para executar apenas uma vez
Como armazenar campos personalizados no JWT, como tenant_id e user_role?
Use o Custom Access Token Hook do Supabase:

1. Crie a função PL/pgSQL custom_access_token_hook(event jsonb)
2. Adicione os custom claims na função com jsonb_set()
3. Ative o Hook no Supabase Dashboard (Authentication > Hooks > Custom Access Token)
4. Leia os custom claims do JWT no frontend com jwtDecode()
Como um SaaS multitenant pode isolar os dados de usuários empresariais e individuais?
A ideia central é armazenar auth_type ('oauth' ou 'sso') e tenant_id no JWT e fazer a RLS Policy filtrar os dados com base nesses dois campos:

• Usuários SSO: só podem ver dados cujo tenant_id corresponda ao seu tenant
• Usuários OAuth: só podem ver dados públicos com is_public=true

Use uma Policy RESTRICTIVE como filtro global e Policies PERMISSIVE para controlar operações específicas.
O Supabase oferece suporte ao Sign in with Apple? A configuração é complexa?
Sim, mas a configuração é mais complexa do que no Google ou GitHub:

1. Crie um Services ID no Apple Developer Portal
2. Gere uma chave privada (arquivo .p8, que só pode ser baixado uma vez)
3. Informe Team ID, Key ID, Services ID e o conteúdo da chave privada no Supabase Dashboard

Apps iOS podem usar a API nativa Sign in with Apple e, depois de obter o identity_token, enviá-lo ao método signInWithIdTokenCredentials() do Supabase.
Um cliente empresarial exige SSO, mas eu não sei qual tipo de IdP ele usa. O que fazer?
Peça ao administrador de TI do cliente o arquivo ou a URL de Metadata do IdP; Okta, Azure AD e Google Workspace permitem exportá-los. Com o Metadata em mãos, basta adicionar a conexão com o comando supabase sso add --metadata-file. O Supabase analisa automaticamente os endpoints e certificados contidos no Metadata.

22 min de leitura · Publicado em: 21 abr 2026 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog