Pular para o conteúdo principal

Adicionar autenticação ao seu aplicativo Supabase (Add authentication to your Supabase application)

Conceitos básicos do Supabase (Supabase basics)

O Supabase utiliza a Segurança em Nível de Linha do Postgres (Row-Level Security) para controlar as permissões de acesso aos dados. Em termos simples, ao criar políticas de Segurança em Nível de Linha para tabelas no banco de dados, podemos restringir e gerenciar quem pode ler, escrever e atualizar dados em uma tabela.

Vamos supor que você tenha uma tabela chamada "posts" em seu banco de dados, com o seguinte conteúdo:

Tabela de posts

O campo user_id na tabela representa o usuário ao qual cada dado de post pertence. Você pode restringir cada usuário para acessar apenas seus próprios dados de post com base no campo user_id.

No entanto, antes que isso possa ser implementado, o Supabase precisa ser capaz de identificar o usuário atual que está acessando o banco de dados.

Adicionar dados do usuário às requisições do Supabase (Add user data to the Supabase requests)

Graças ao suporte do Supabase para JWT, quando nosso aplicativo interage com o Supabase, podemos gerar um JWT contendo dados do usuário usando o segredo JWT fornecido pelo Supabase. Em seguida, usamos esse JWT como o header de Autenticação ao fazer requisições. Ao receber a requisição, o Supabase verifica automaticamente a validade do JWT e permite o acesso aos dados contidos nele durante os processos subsequentes.

Primeiramente, podemos obter o segredo JWT fornecido pelo Supabase em “Configurações do Projeto” no painel do Supabase:

Página de configurações da API do Supabase

Depois, ao usar o SDK do Supabase para fazer requisições, utilizamos esse segredo para gerar nosso JWT e anexá-lo como header de Autenticação à requisição. (Observe que esse processo ocorre no serviço backend do seu aplicativo, e o segredo JWT nunca deve ser exposto a terceiros).

import { createClient } from '@supabase/supabase-js';
import { sign } from 'jsonwebtoken';

/
* Nota:
* Você pode encontrar o SUPABASE_URL, SUPABASE_ANON_KEY no mesmo local onde encontra o JWT Secret.
*/
const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY;

const SUPABASE_JWT_SECRET = process.env.SUPABASE_JWT_SECRET;

export const getSupabaseClient = (userId) => {
const jwtPayload = {
userId,
};

const jwt = sign(jwtPayload, SUPABASE_JWT_SECRET, {
expiresIn: '1h', // Apenas para demonstração
});

const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
global: {
headers: {
Authorization: `Bearer ${jwt}`,
},
},
});

return client;
};

Em seguida, acesse o Editor SQL no painel do Supabase e crie uma função para recuperar o userId carregado na requisição:

Criar função para obter user ID

O código utilizado na imagem é o seguinte:

create or replace function auth.user_id() returns text as $$
select nullif(current_setting('request.jwt.claims', true)::json->>'userId', '')::text;
$$ language sql stable;

Como mostra o código, no Supabase, você pode recuperar o payload do JWT que geramos chamando request.jwt.claims. O campo userId dentro do payload é o valor que definimos.

Com essa função, o Supabase pode determinar o usuário que está acessando o banco de dados no momento.

Criar política de Segurança em Nível de Linha (Row-Level Security policy)

Agora, podemos criar uma política de Segurança em Nível de Linha para restringir cada usuário a acessar apenas seus próprios dados de post com base no campo user_id da tabela posts.

  1. Acesse a página Editor de Tabelas no painel do Supabase e selecione a tabela posts.
  2. Clique em "Add RLS Policy" no topo da tabela.
  3. Na janela exibida, clique em "Create policy".
  4. Insira um nome para a política e escolha o comando SELECT Policy.
  5. No bloco using do código abaixo, insira:
auth.user_id() = user_id
Criar política RLS

Ao utilizar tais políticas, o controle de acesso aos dados dentro do Supabase é alcançado.

Em aplicações reais, você criaria várias políticas para restringir ações do usuário, como inserção e modificação de dados. No entanto, isso está além do escopo deste artigo. Para mais informações sobre Segurança em Nível de Linha (RLS), consulte Proteja seus dados usando Row Level Security do Postgres.

Processo básico de integração com Logto (Basic integration process with Logto)

Como mencionado anteriormente, como o Supabase utiliza RLS para seu controle de acesso, o segredo para integrar com o Logto (ou qualquer outro serviço de autenticação) está em obter o id do usuário autorizado e enviá-lo ao Supabase. Todo o processo está ilustrado no diagrama abaixo:

A seguir, explicaremos como integrar Logto com Supabase com base neste diagrama de processo.

Integração com Logto (Logto integration)

O Logto oferece guias de integração para diversos frameworks e linguagens de programação.

Geralmente, aplicativos construídos com esses frameworks e linguagens se enquadram em categorias como aplicativos Nativos, SPA (aplicativos de página única), aplicativos web tradicionais e aplicativos M2M (máquina para máquina). Você pode visitar a página de início rápido do Logto para integrar o Logto ao seu aplicativo com base na stack tecnológica que está usando. Depois, siga as instruções abaixo para integrar o Logto ao seu projeto conforme o tipo do seu aplicativo.

Aplicativo nativo ou SPA (Native app or SPA)

Tanto aplicativos nativos quanto SPAs rodam em seu dispositivo, e as credenciais (token de acesso) obtidas após o login são armazenadas localmente no seu dispositivo.

Portanto, ao integrar seu app com o Supabase, você precisa interagir com o Supabase através do seu serviço backend, pois não pode expor informações sensíveis (como o segredo JWT do Supabase) em cada dispositivo de usuário.

Vamos supor que você está construindo sua SPA usando React e Express. Você já integrou o Logto ao seu aplicativo seguindo o Guia do Logto React SDK (você pode consultar o código em nosso exemplo react). Além disso, você adicionou a validação do token de acesso do Logto ao seu servidor backend conforme o guia validar tokens de acesso.

Agora, você usará o token de acesso obtido do Logto para solicitar dados do usuário ao seu servidor backend:

import { useLogto } from '@logto/react';
import { useState, useEffect } from 'react';
import PostList from './PostList';

const endpoint = '<https://www.mysite.com/api/posts>';
const resource = '<https://www.mysite.com/api>';

function PostPage() {
const { isAuthenticated, getAccessToken } = useLogto();
const [posts, setPosts] = useState();

useEffect(() => {
const fetchPosts = async () => {
const response = await fetch(endpoint, {
headers: {
Authorization: `Bearer ${await getAccessToken(resource)}`,
},
});
setPosts(response.json());
};

if (isAuthenticated) {
void fetchPosts();
}
}, [isAuthenticated, getAccessToken]);

return <PostList posts={posts} />;
}

export default PostPage;

No seu servidor backend, você já extraiu o id do usuário logado do token de acesso usando um middleware:

// auth-middleware.ts
import { createRemoteJWKSet, jwtVerify } from 'jose';

//...

export const verifyAuthFromRequest = async (ctx, next) => {
// Extrair o token
const token = extractBearerTokenFromHeaders(ctx.request.headers);

const { payload } = await jwtVerify(
token, // O Bearer Token bruto extraído do header da requisição
createRemoteJWKSet(new URL('https://<your-logto-domain>/oidc/jwks')), // gerar um jwks usando jwks_uri consultado do servidor Logto
{
// emissor esperado do token, deve ser emitido pelo servidor Logto
issuer: 'https://<your-logto-domain>/oidc',
// público esperado do token, deve ser o indicador de recurso da API atual
audience: '<your request listener resource indicator>',
}
);

// se você estiver usando RBAC
assert(payload.scope.includes('some_scope'));

// lógica personalizada do payload
ctx.auth = {
userId: payload.sub,
};

return next();
};

Agora, você pode usar o getSupabaseClient descrito acima para anexar o userId ao JWT usado nas requisições subsequentes ao Supabase. Alternativamente, você pode criar um middleware para criar um cliente Supabase para requisições que precisam interagir com o Supabase:

export const withSupabaseClient = async (ctx, next) => {
ctx.supabase = getSupabaseClient(ctx.auth.userId);

return next();
};

No fluxo de processamento subsequente, você pode chamar diretamente ctx.supabase para interagir com o Supabase:

const fetchPosts = async (ctx) => {
cosnt { data } = await ctx.supabase.from('posts').select('*');

return data;
}

Neste código, o Supabase retornará apenas os dados de post pertencentes ao usuário atual com base nas políticas definidas anteriormente.

Aplicativo web tradicional (Traditional web app)

A principal diferença entre um aplicativo web tradicional e um aplicativo Nativo ou SPA é que um aplicativo web tradicional renderiza e atualiza páginas exclusivamente no servidor web. Portanto, as credenciais do usuário são gerenciadas diretamente pelo servidor web, enquanto em aplicativos Nativos e SPAs, elas residem no dispositivo do usuário.

Ao integrar Logto com um aplicativo web tradicional no Supabase, você pode recuperar diretamente o id do usuário logado no backend.

Tomando um projeto Next.js como exemplo, após integrar Logto ao seu projeto seguindo o Guia do SDK Next.js, você pode usar o SDK do Logto para recuperar informações do usuário e construir o JWT correspondente para interagir com o Supabase.

import { getLogtoContext } from '@logto/next-server-actions';
import { logtoConfig } from '@/logto';
import { getSupabaseClient } from '@/utils';
import PostList from './PostList';

export default async function PostPage() {
const { cliams } = await getLogtoContext(logtoConfig);

// O valor `sub` em `cliams` é o id do usuário.
const supabase = getSupabaseClient(cliams.sub);

const { data: posts } = await supabase.from('posts').select('*');

return <PostList posts={posts} />;
}

Aplicativo máquina para máquina (Machine-to-machine app)

Máquina para máquina (M2M) é frequentemente usado quando seu aplicativo precisa se comunicar diretamente com servidores de recursos, como um serviço estático que busca posts diários, etc.

Você pode usar o guia Máquina para máquina: Autenticação com Logto para autenticação de aplicativos máquina para máquina. A integração entre Supabase e aplicativos Máquina para máquina é semelhante à de aplicativos Nativos e SPAs (como descrito na seção "Aplicativo nativo ou SPA"). Envolve obter um token de acesso do Logto e depois validá-lo por meio de uma API backend protegida.

No entanto, é importante observar que aplicativos Nativos e SPAs são normalmente projetados para usuários finais, então o id de usuário obtido representa o próprio usuário. Já o token de acesso para aplicativos máquina para máquina representa o próprio aplicativo, e o campo sub no payload do token de acesso é o client id do app M2M, não um usuário específico. Portanto, durante o desenvolvimento, é fundamental distinguir quais dados são destinados a aplicativos M2M.

Além disso, se você quiser que um aplicativo M2M específico acesse o Supabase em nome de todo o serviço para contornar as restrições de RLS, pode usar o segredo service_role do Supabase para criar um cliente Supabase. Isso é útil quando você deseja realizar tarefas administrativas ou automatizadas que exigem acesso a todos os dados sem ser restringido pelas políticas de Segurança em Nível de Linha configuradas para usuários individuais.

O segredo service_role pode ser encontrado na mesma página do segredo JWT:

Segredo service role

Ao criar um cliente Supabase, use o segredo service_role, então esse cliente poderá acessar todos os dados no banco de dados:

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

// ...
const SUPABASE_SERVICE_ROLE_SCRET = process.env.SUPABASE_SERVICE_ROLE_SCRET;

const client = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_SCRET, {
// ...options
});