Projeto final guiado: implementar autenticação e autorização completas em uma API

Capítulo 18

Tempo estimado de leitura: 11 minutos

+ Exercício

Escopo do projeto e decisões de arquitetura

Neste projeto final, você vai implementar uma camada completa de autenticação e autorização em uma API, com requisitos de segurança e qualidade (documentação, checklist e testes). A ideia é consolidar tudo em um fluxo funcional e auditável, com validações e casos de erro bem definidos.

Stack sugerida (adaptável)

  • API: Node.js + Express (ou framework equivalente)
  • Banco: PostgreSQL (ou equivalente relacional)
  • Autenticação: JWT de curta duração + Refresh Token (rotacionado)
  • Autorização: RBAC para regras gerais + 1 regra ABAC para um recurso específico
  • Proteções: rate limiting, validações, headers de segurança, mitigação de CSRF/XSS quando aplicável
  • Testes: suíte mínima de integração para fluxos críticos

Escolha orientada: JWT vs sessão

Para este projeto guiado, a escolha recomendada é JWT com refresh token, porque facilita testar expiração/renovação e cenários de revogação sem depender de estado de sessão no servidor. Se você optar por cookies httpOnly para o refresh token, trate CSRF no endpoint de refresh/logout. Se optar por armazenar refresh token fora de cookie (ex.: header), o risco de XSS aumenta e você deve reforçar a estratégia de armazenamento e CSP no front-end (quando existir).

Modelo de dados (usuários, papéis e tokens)

Tabelas mínimas

Modele as entidades para suportar: cadastro/login, papéis, revogação/rotação de refresh token, auditoria básica e uma regra ABAC.

-- users: identidade e credenciais (hash/salt já tratados pela lib escolhida no código, não no SQL puro aqui) CREATE TABLE users (  id UUID PRIMARY KEY,  email TEXT UNIQUE NOT NULL,  password_hash TEXT NOT NULL,  status TEXT NOT NULL DEFAULT 'ACTIVE', -- ACTIVE | DISABLED  created_at TIMESTAMP NOT NULL DEFAULT now(),  updated_at TIMESTAMP NOT NULL DEFAULT now()); -- roles e associação (RBAC) CREATE TABLE roles (  id UUID PRIMARY KEY,  name TEXT UNIQUE NOT NULL -- ex.: 'admin', 'editor', 'viewer'); CREATE TABLE user_roles (  user_id UUID REFERENCES users(id) ON DELETE CASCADE,  role_id UUID REFERENCES roles(id) ON DELETE CASCADE,  PRIMARY KEY (user_id, role_id)); -- refresh tokens: armazenar hash do token, metadados e revogação/rotação CREATE TABLE refresh_tokens (  id UUID PRIMARY KEY,  user_id UUID REFERENCES users(id) ON DELETE CASCADE,  token_hash TEXT NOT NULL,  family_id UUID NOT NULL, -- para rotação e detecção de reutilização  issued_at TIMESTAMP NOT NULL DEFAULT now(),  expires_at TIMESTAMP NOT NULL,  revoked_at TIMESTAMP NULL,  replaced_by UUID NULL, -- aponta para o novo refresh token (rotação)  user_agent TEXT NULL,  ip TEXT NULL); CREATE INDEX idx_refresh_tokens_user ON refresh_tokens(user_id); CREATE INDEX idx_refresh_tokens_family ON refresh_tokens(family_id); -- recurso de exemplo para RBAC+ABAC: documents CREATE TABLE documents (  id UUID PRIMARY KEY,  owner_id UUID REFERENCES users(id) ON DELETE CASCADE,  title TEXT NOT NULL,  content TEXT NOT NULL,  visibility TEXT NOT NULL DEFAULT 'PRIVATE', -- PRIVATE | ORG | PUBLIC  created_at TIMESTAMP NOT NULL DEFAULT now(),  updated_at TIMESTAMP NOT NULL DEFAULT now());

Regra ABAC escolhida (exemplo)

Além do RBAC (ex.: admin pode tudo; editor pode criar/editar), implemente uma regra ABAC simples e verificável: um usuário pode atualizar um documento se (a) tiver papel editor e (b) for o owner do documento, ou se (c) tiver papel admin. Isso força a checagem de atributo do recurso (owner_id) e do sujeito (roles).

Endpoints do projeto (contrato e códigos de erro)

Defina desde já o contrato de inputs/outputs e códigos de erro. Isso orienta implementação e testes.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

Baixar o aplicativo

Autenticação

MétodoRotaDescriçãoSucessoErros comuns
POST/auth/registerCadastro201 + user400 validação, 409 email em uso
POST/auth/loginLogin e emissão de tokens200 + accessToken (+ refresh)400 validação, 401 credenciais inválidas, 423 usuário desabilitado
POST/auth/refreshRotaciona refresh e emite novo access200 + accessToken (+ novo refresh)401 refresh inválido/expirado, 403 reutilização detectada
POST/auth/logoutRevoga refresh atual204401 não autenticado
GET/mePerfil do usuário autenticado200 + user401 token ausente/inválido

Recursos protegidos (RBAC + ABAC)

MétodoRotaProteçãoSucessoErros comuns
POST/documentsRBAC: editor/admin201 + document401, 403
GET/documents/:idRegra de visibilidade + auth opcional200 + document404, 403
PATCH/documents/:idABAC: editor dono OU admin200 + document401, 403, 404
DELETE/documents/:idRBAC: admin204401, 403, 404

Formato de erro padronizado

Padronize respostas de erro para facilitar debug e testes.

{  "error": {    "code": "VALIDATION_ERROR",    "message": "Campo email é obrigatório",    "details": [{ "field": "email", "issue": "required" }]  }}

Implementação passo a passo

Passo 1: estrutura do projeto e camadas

Separe responsabilidades para evitar autorização espalhada e inconsistências.

  • routes/: definição de rotas e middlewares
  • controllers/: parsing de input e resposta HTTP
  • services/: regras de negócio (login, refresh, autorização)
  • repositories/: acesso a dados (users, tokens, documents)
  • middlewares/: auth, rate limit, validação, tratamento de erros

Passo 2: validação de entrada (register/login)

Implemente validação consistente (ex.: Zod/Joi/class-validator). Regras mínimas: email válido, senha com política definida, campos obrigatórios, limites de tamanho.

// Exemplo (pseudo) de schema const RegisterSchema = {  email: "email|required|max:254",  password: "string|required|min:12|max:72" };

Passo 3: cadastro (register)

  • Normalizar email (lowercase, trim)
  • Verificar unicidade
  • Gerar hash de senha (bcrypt/argon2)
  • Criar usuário com status ACTIVE
  • Atribuir papel padrão (ex.: viewer)
// Controller (pseudo) POST /auth/register input: { email, password } output 201: { id, email, roles: [...] }

Passo 4: login e emissão de tokens

No login, você deve emitir: access token curto (ex.: 10–15 min) e refresh token longo (ex.: 7–30 dias) com rotação.

// Payload sugerido do access token {  sub: user.id,  roles: ["viewer", "editor"],  iat: 1700000000,  exp: 1700000900,  jti: "uuid" }

Decisão de transporte do refresh token (recomendado): cookie httpOnly + SameSite + Secure. O access token pode ir no body e ser enviado no header Authorization nas chamadas subsequentes.

// Resposta 200 (exemplo) {  "accessToken": "eyJ...",  "tokenType": "Bearer",  "expiresIn": 900 }

Passo 5: middleware de autenticação (access token)

  • Ler Authorization: Bearer <token>
  • Validar assinatura e claims obrigatórias
  • Carregar contexto do usuário (id e roles do token; opcionalmente checar status no banco)
  • Falhar com 401 em token ausente/inválido/expirado
// req.auth = { userId, roles } next()

Passo 6: refresh token com rotação e detecção de reutilização

Implemente o endpoint /auth/refresh com as seguintes regras:

  • Recebe refresh token (cookie ou body)
  • Calcula hash e busca em refresh_tokens por token_hash
  • Verifica: não revogado, não expirado, usuário ativo
  • Gera novo refresh token (mesma family_id) e marca o antigo como revogado e replaced_by apontando para o novo
  • Emite novo access token

Detecção de reutilização: se um refresh token já estiver revogado e ainda assim for apresentado, trate como evento suspeito e revogue toda a família (family_id) do usuário (ou ao menos invalide a sessão/token chain).

// Pseudo fluxo de rotação refresh(oldToken): 1) row = findByHash(hash(oldToken)) 2) if !row or row.expires_at < now: 401 3) if row.revoked_at != null:    revokeFamily(row.family_id); return 403 4) newToken = randomString(64) 5) insert new row with same family_id, token_hash=hash(newToken) 6) update old row set revoked_at=now, replaced_by=newId 7) return accessToken + set-cookie refresh=newToken

Passo 7: logout (revogação)

No logout, revogue o refresh token atual (ou toda a família, se quiser logout global). Se estiver em cookie, limpe o cookie.

// POST /auth/logout -> 204 // Ação: marcar refresh token como revoked_at=now

Passo 8: autorização RBAC (middlewares/guards)

Crie um middleware requireRoles(...roles) para proteger endpoints. Ele deve:

  • Exigir autenticação (middleware anterior)
  • Verificar interseção entre roles do usuário e roles exigidos
  • Responder 403 se não autorizado
// Exemplo de uso routes.post("/documents", auth, requireRoles("editor","admin"), createDocument)

Passo 9: autorização ABAC (regra por atributo do recurso)

Implemente a regra ABAC no update de documento. Evite confiar no client para informar owner; busque o documento no banco e compare.

// PATCH /documents/:id // Regra: admin pode; editor só se doc.owner_id == req.auth.userId service.updateDocument(id, input, auth):  doc = repo.findById(id)  if !doc: throw 404  if hasRole(auth, "admin"): return repo.update(...)  if hasRole(auth, "editor") && doc.owner_id == auth.userId: return repo.update(...)  throw 403

Passo 10: rate limiting e proteção contra abuso

Aplique rate limiting com granularidade por rota:

  • /auth/login: mais restrito (ex.: 5–10 tentativas/min por IP + por email)
  • /auth/refresh: moderado (ex.: 30/min)
  • Demais rotas: limites razoáveis por IP/usuário

Retorne 429 com cabeçalhos de rate limit (quando suportado) e mensagem padronizada.

{ "error": { "code": "RATE_LIMITED", "message": "Muitas requisições, tente novamente em instantes" } }

Passo 11: CSRF/XSS (quando aplicável)

Se o refresh token estiver em cookie e o endpoint /auth/refresh aceitar credenciais automaticamente, proteja contra CSRF:

  • Use SameSite=Lax ou Strict quando possível
  • Para cenários cross-site, implemente token anti-CSRF (double submit cookie ou header custom) e valide no servidor
  • Exija Content-Type: application/json e recuse requests sem header esperado

Para XSS (principalmente se houver front-end), evite armazenar access token em local acessível por JS quando possível; prefira mantê-lo em memória e renovar via refresh. No back-end, garanta que respostas não reflitam entradas sem sanitização quando renderização HTML existir (em API pura, o risco é menor, mas logs e mensagens de erro ainda devem ser cuidadosos).

Passo 12: política de senha (aplicação no cadastro e troca)

Aplique a política definida no projeto (ex.: mínimo 12 caracteres, bloquear senhas comuns, limitar tamanho máximo, impedir senha igual ao email). Garanta que erros de validação retornem 400 com detalhes por campo.

// Exemplo de validação adicional if isCommonPassword(password): 400 if password contains emailLocalPart: 400

Documentação dos endpoints (modelo obrigatório)

Documente cada endpoint com: autenticação requerida, roles, input, output e erros. Você pode usar OpenAPI, mas aqui vai um modelo textual mínimo que deve existir no repositório (ex.: docs/api.md).

Exemplo: POST /auth/login

  • Auth: não
  • Rate limit: 10/min por IP e 5/min por email
  • Input:
{ "email": "user@example.com", "password": "string" }
  • Output 200:
{ "accessToken": "...", "tokenType": "Bearer", "expiresIn": 900 }
  • Erros:
  • 400 VALIDATION_ERROR
  • 401 INVALID_CREDENTIALS
  • 423 USER_DISABLED
  • 429 RATE_LIMITED

Exemplo: PATCH /documents/:id

  • Auth: sim (Bearer)
  • RBAC: editor ou admin
  • ABAC: se editor, deve ser owner
  • Input:
{ "title": "string?", "content": "string?", "visibility": "PRIVATE|ORG|PUBLIC?" }
  • Output 200:
{ "id": "uuid", "ownerId": "uuid", "title": "...", "content": "...", "visibility": "PRIVATE" }
  • Erros:
  • 401 UNAUTHORIZED
  • 403 FORBIDDEN
  • 404 NOT_FOUND
  • 400 VALIDATION_ERROR

Checklist de segurança (entregável obrigatório)

Inclua um arquivo SECURITY_CHECKLIST.md com itens marcáveis. Exemplo:

  • [ ] Senhas armazenadas com hash forte (bcrypt/argon2) e parâmetros adequados
  • [ ] Validação de input em todas as rotas de escrita
  • [ ] Mensagens de erro não vazam se usuário existe (quando aplicável)
  • [ ] Access token curto e refresh token rotacionado
  • [ ] Refresh token armazenado como hash no banco
  • [ ] Reutilização de refresh token detectada e tratada (revogação por família)
  • [ ] Logout revoga refresh token (e limpa cookie, se usado)
  • [ ] Rate limiting aplicado em login/refresh e rotas sensíveis
  • [ ] CSRF mitigado no refresh/logout se usar cookies
  • [ ] Cabeçalhos de segurança básicos configurados (ex.: no proxy/app)
  • [ ] Logs não registram tokens nem senhas
  • [ ] RBAC aplicado em rotas administrativas
  • [ ] ABAC implementado e testado (owner check)

Suíte mínima de testes (entregável obrigatório)

Crie testes de integração cobrindo fluxos e erros. Abaixo está um conjunto mínimo recomendado (ex.: Jest + Supertest, pytest, etc.).

Autenticação

  • Cadastro: 201; 409 email duplicado; 400 senha fraca
  • Login: 200; 401 senha errada; 423 usuário desabilitado; 429 após exceder limite
  • Access token: 401 sem token; 401 token inválido; 401 token expirado (simulado)

Refresh/rotação

  • Refresh válido: retorna novo access e novo refresh; antigo fica revogado e com replaced_by
  • Refresh expirado: 401
  • Reutilização do refresh antigo: 403 e família revogada (ou política definida)
  • Logout: 204 e refresh revogado; refresh após logout: 401/403 conforme política

Autorização RBAC/ABAC

  • Viewer não cria documento: 403
  • Editor cria documento: 201
  • Editor atualiza próprio documento: 200
  • Editor tenta atualizar documento de outro: 403 (ABAC)
  • Admin atualiza documento de outro: 200
  • Admin deleta documento: 204; editor deleta: 403

Exemplo de caso de teste (pseudo)

it("editor não pode editar documento de outro usuário", async () => {  const editor = await registerAndLogin("e@x.com", "StrongPass...", ["editor"])  const other = await registerAndLogin("o@x.com", "StrongPass...", ["editor"])  const doc = await createDocument(other.accessToken, { title:"A", content:"B" })  const res = await request(app)    .patch(`/documents/${doc.id}`)    .set("Authorization", `Bearer ${editor.accessToken}`)    .send({ title: "hack" })  expect(res.status).toBe(403)  expect(res.body.error.code).toBe("FORBIDDEN")})

Critérios claros de entrega (o que será avaliado)

Fluxo funcional

  • Cadastro e login funcionando com validações
  • Access token protege endpoints
  • Refresh token renova sessão com rotação
  • Logout revoga corretamente

Cobertura de casos de erro

  • Erros 400/401/403/404/409/423/429 implementados onde aplicável
  • Formato de erro padronizado
  • Mensagens seguras (sem vazar detalhes sensíveis)

Validações corretas e segurança

  • Rate limiting ativo em rotas sensíveis
  • CSRF mitigado se refresh/logout usam cookie
  • RBAC aplicado em rotas protegidas
  • ABAC aplicado no recurso escolhido (owner check) e testado
  • Checklist de segurança preenchido

Documentação e testes

  • Documentação de endpoints com inputs/outputs e códigos de erro
  • Suíte mínima de testes passando (incluindo refresh/rotação e ABAC)
  • Instruções de execução (ex.: README com setup, variáveis de ambiente e comandos)

Agora responda o exercício sobre o conteúdo:

Ao implementar o endpoint de refresh com rotação e detecção de reutilização, qual ação é a mais adequada quando um refresh token já revogado é apresentado novamente?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Se um refresh token revogado reaparece, isso indica possível reutilização indevida. A resposta esperada é 403 e a revogação da família (family_id) para invalidar a cadeia e reduzir o impacto do incidente.

Capa do Ebook gratuito Autenticação e Autorização no Back-end: Sessões, JWT e Boas Práticas
100%

Autenticação e Autorização no Back-end: Sessões, JWT e Boas Práticas

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.