JWT e papéis
Contrato público. O formato do JWT é nosso e está documentado aqui; nenhuma biblioteca o determina.
Pré-requisitos
Você precisa de um banco PostgreSQL privado inicializado e uma chave de assinatura ou verificador compatível. Esta página descreve tokens/papéis nativos; o verificador nativo não exige uma política de emissor e audiência.
Papéis do Postgres
| Papel | Login | Usado por | RLS |
|---|---|---|---|
authenticator |
sim | a conexão da API (pool) | não se aplica |
anon |
não | requisições sem Authorization |
aplicada |
authenticated |
não | usuários finais autenticados | aplicada |
service_role |
não | backend confiável | ignorada (BYPASSRLS) |
nelcota_auth |
não | interno: o servidor lendo auth.* |
não se aplica (nenhum JWT pode assumir esse papel) |
authenticator é NOINHERIT: por si só, não tem nenhum privilégio. Ele é membro dos outros papéis e assume um deles a cada requisição.
Aviso: Um JWT com
role: service_roleignora toda a RLS. Ele NUNCA deve ir para o frontend, um aplicativo móvel ou qualquer lugar fora do seu servidor.
O que acontece em cada requisição
- A API lê
Authorization: Bearer <jwt>(o esquema não diferencia maiúsculas de minúsculas:bearer <jwt>também funciona).- Sem o cabeçalho: a requisição assume
anon. - Cabeçalho presente, mas inválido (assinatura,
exp, esquema,kiddesconhecido ou papel): 401. Nunca assumeanonsilenciosamente.
- Sem o cabeçalho: a requisição assume
- Abre uma transação e executa, com parâmetros:
SQL SELECT set_config('role', $1, true), -- = SET LOCAL ROLE set_config('request.jwt.claims', $2, true); -- full claims - Executa a consulta. A RLS do Postgres decide o que o papel pode ver.
- COMMIT (ou ROLLBACK em caso de erro). O papel e as claims terminam com a transação.
O acesso às linhas do aplicativo é determinado por GRANTs e RLS do PostgreSQL. O servidor também valida autenticação, sintaxe de consulta/caminho e fronteiras da operação antes de executar a requisição.
Assinatura
- EdDSA (Ed25519), o padrão. Chave privada em
NELCOTA_JWT_PRIVATE_KEY(PKCS#8 em PEM ou base64 em uma única linha). A chave pública é publicada emGET /auth/v1/.well-known/jwks.json(kty: OKP,crv: Ed25519,kid= impressão digital RFC 7638). O cabeçalho de cada token contém okid. - HS256, o modo simples/legado, com
NELCOTA_JWT_SECRET(≥ 32 caracteres). Quando há uma chave EdDSA, o segredo HS256 serve apenas para verificar tokens antigos, permitindo migrar sem encerrar a sessão de ninguém.
Gere uma chave: openssl genpkey -algorithm ed25519 (ou nelcota init, que gera uma).
Claims dos tokens de usuário (emitidos por /auth/v1)
{
"iss": "nelcota",
"aud": "authenticated",
"sub": "3f1c…-uuid",
"role": "authenticated",
"email": "[email protected]",
"session_id": "8a2d…-uuid",
"iat": 1767225600,
"exp": 1767226500
}Regras de validação (qualquer emissor)
| Claim | Obrigatória | Regra |
|---|---|---|
role |
sim | anon, authenticated ou service_role. Qualquer outro valor → 401. |
sub |
apenas para authenticated |
o UUID do usuário (auth.uid()). |
exp |
sim | época em segundos; tolerância de 30 s. |
iss, aud |
não | informativa: não é exigida na verificação. |
As outras claims são repassadas sem alterações para auth.jwt(). O logout revoga a sessão e os tokens de renovação; o JWT de acesso, que não mantém estado, continua válido até exp (padrão: 15 minutos, NELCOTA_JWT_EXPIRY_SECS).
Trocar de provedor de autenticação
Como as políticas dependem apenas de auth.uid(), auth.role() e auth.jwt(), qualquer emissor que produza um JWT com as claims acima funciona. A verificação fica por trás do trait JwtVerifier; aceitar um JWKS externo (Auth0, Keycloak, Zitadel...) requer uma nova implementação desse trait.
Funções SQL
auth.jwt() -> jsonb -- request claims ('{}' outside a request)
auth.uid() -> uuid -- the sub claim
auth.role() -> text -- the role claimExemplo de política:
CREATE POLICY owner ON public.todos
FOR SELECT TO authenticated
USING (user_id = auth.uid());Códigos de status HTTP
| Situação | Status |
|---|---|
| JWT inválido ou expirado, papel desconhecido ou esquema diferente de Bearer | 401 invalid_token |
anon sem permissão na tabela |
401 db_error (sqlstate 42501) |
authenticated sem permissão / violação de política na escrita |
403 db_error (sqlstate 42501) |
| A tabela não existe | 404 not_found |
O corpo é o JSON de erro da API (code, message e, para erros do banco, sqlstate/details/hint/constraint); veja API REST.
Toda resposta 401 inclui WWW-Authenticate: Bearer error="invalid_token" quando um token foi apresentado e rejeitado, ou apenas Bearer quando a requisição não tinha token (uma requisição anon que precisa se autenticar).
Guias relacionados
Veja schema de autenticação, API REST e deploy.