Testes de autenticação e autorização no back-end

Capítulo 16

Tempo estimado de leitura: 10 minutos

+ Exercício

O que testar em autenticação e autorização (e por quê)

Em back-ends, falhas de autenticação/autorização costumam aparecer em três pontos: (1) regras de decisão (quem pode fazer o quê), (2) fluxos completos (login/refresh/logout) e (3) validações de segurança (tokens inválidos, expirados, adulterados, ausência de credenciais). A estratégia mais eficiente é combinar testes unitários (rápidos e determinísticos) para regras e testes de integração (realistas) para fluxos e bordas de segurança.

Este capítulo foca em como estruturar e implementar esses testes, incluindo dados de teste, simulação de tempo (expiração) e rotação de chaves, além de uma matriz de casos e critérios de aceitação (401 vs 403, mensagens e headers).

Pirâmide de testes aplicada a auth

  • Unitários (regras/políticas): validam RBAC/ABAC sem rede, sem banco (ou com repositórios fake). Objetivo: garantir que a decisão de acesso é correta para combinações de papéis/atributos.
  • Integração (fluxos): exercitam endpoints reais (login/refresh/logout) com middleware/guards, validação de token e persistência (idealmente com DB em memória ou container de teste). Objetivo: garantir contratos HTTP, cookies/headers, status codes e efeitos colaterais (revogação/blacklist/rotação).
  • Segurança básica (negativos): tentativas sem permissão, token expirado, token adulterado, assinatura inválida, kid desconhecido, refresh reutilizado. Objetivo: garantir falha segura e respostas consistentes.

Preparação do ambiente de testes

1) Separar responsabilidades para facilitar testes

Para testar bem, isole:

  • Decisor de autorização (policy engine): recebe subject (usuário), action, resource e context e retorna allow/deny + motivo.
  • Validador de token: valida assinatura, claims, expiração e retorna o principal (identidade) ou erro.
  • Camada HTTP: traduz erros em status codes (401/403) e headers (ex.: WWW-Authenticate).

2) Dados de teste (fixtures) consistentes

Crie um conjunto pequeno e reutilizável de usuários/recursos:

  • Usuários: admin, manager, userA, userB, disabledUser.
  • Recursos: project1 (owner=userA), project2 (owner=userB).
  • Contexto: tenant, horário, IP, device, etc. (quando suas regras dependem disso).

Evite gerar dados aleatórios sem seed; prefira factories determinísticas.

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

3) Simular tempo (expiração) de forma determinística

Não use sleep. Injete um Clock (ou provider de tempo) no código e substitua por um fake nos testes.

// Exemplo conceitual (TypeScript/Node) de Clock injetável
export interface Clock { now(): number } // epoch ms
export class SystemClock implements Clock { now() { return Date.now() } }
export class FakeClock implements Clock {
  private t: number
  constructor(startMs: number) { this.t = startMs }
  now() { return this.t }
  advance(ms: number) { this.t += ms }
}

Use o FakeClock para criar tokens com expiração curta e avançar o tempo para validar respostas 401 e headers.

4) Rotação de chaves (JWT) em testes

Para testar rotação, modele um KeyStore com múltiplas chaves ativas e um kid no header do token. Em teste, você controla:

  • Chave atual para assinar (ex.: kid=key-2).
  • Conjunto de chaves aceitas para validar (ex.: key-1 e key-2 durante janela de transição).
  • Remoção de chave antiga (tokens antigos passam a falhar).
// Pseudocódigo de KeyStore fake
class FakeKeyStore {
  signingKid = 'key-1'
  keys = new Map([['key-1', 'secret1']])
  rotateTo(kid, secret) { this.signingKid = kid; this.keys.set(kid, secret) }
  retire(kid) { this.keys.delete(kid) }
  getSigningKey() { return { kid: this.signingKid, secret: this.keys.get(this.signingKid) } }
  getValidationKey(kid) { return this.keys.get(kid) }
}

Testes unitários: regras RBAC/ABAC

O que validar

  • Permissões positivas: papéis/atributos corretos permitem a ação.
  • Negativos: papéis insuficientes, atributos incompatíveis, recurso de outro owner/tenant.
  • Precedência: regras de deny explícito vencem allow (se aplicável).
  • Campos obrigatórios: ausência de atributo no contexto deve negar (fail closed).
  • Motivos: retorno inclui razão (útil para logs e para mapear 401/403 corretamente na camada HTTP).

Estrutura recomendada do decisor

Um formato simples e testável:

type Decision = { allowed: boolean; reason: string }

function authorize(input): Decision {
  // validações de integridade
  // regras RBAC
  // regras ABAC
  // retorno determinístico
}

Exemplos de casos unitários (tabela)

CategoriaCasoEntradaEsperado
RBACAdmin pode deletar projetorole=admin, action=delete, resource=projectallowed=true
RBACUser não pode deletar projetorole=user, action=deleteallowed=false, reason=insufficient_role
ABACOwner pode editar próprio projetouserId=userA, project.ownerId=userAallowed=true
ABACNão-owner não pode editaruserId=userA, project.ownerId=userBallowed=false, reason=not_owner
Fail closedSem tenant no contextocontext.tenantId ausenteallowed=false, reason=missing_context

Passo a passo: escrevendo um teste unitário de política

  1. Arrange: monte subject, resource e context com fixtures.
  2. Act: chame authorize().
  3. Assert: valide allowed e reason. Se sua política retorna escopo (ex.: campos permitidos), valide também.
// Exemplo conceitual (Jest)
test('owner pode editar próprio projeto', () => {
  const subject = { id: 'userA', roles: ['user'] }
  const resource = { id: 'project1', ownerId: 'userA', tenantId: 't1' }
  const context = { tenantId: 't1' }

  const decision = authorize({ subject, action: 'project:update', resource, context })

  expect(decision.allowed).toBe(true)
  expect(decision.reason).toBe('allowed')
})

Testes de integração: fluxos de login/refresh/logout

O que deve ser verificado em integração

  • Contrato HTTP: status, body, headers, cookies, CORS (se aplicável).
  • Persistência: criação/atualização de sessão/refresh token, marcação de revogação, rotação.
  • Middleware/guards: endpoint protegido realmente exige credenciais.
  • Idempotência: logout repetido não deve vazar informação sensível.

Setup típico

  • Subir a aplicação em modo teste (in-memory DB ou container).
  • Usar client HTTP de teste (ex.: supertest, pytest client, etc.).
  • Injetar FakeClock e FakeKeyStore para controlar expiração e rotação.

Passo a passo: teste de login

  1. Crie usuário fixture no banco (com senha já hashada via helper de teste).
  2. Faça POST /auth/login com credenciais válidas.
  3. Valide 200 (ou 201 conforme contrato), presença de access_token no body (ou cookie), e refresh token (cookie HttpOnly ou retorno conforme seu design).
  4. Valide headers relevantes: se usar Bearer, normalmente não há WWW-Authenticate em sucesso; se usar cookie, valide Set-Cookie com flags esperadas.
// Exemplo conceitual (supertest)
const res = await request(app)
  .post('/auth/login')
  .send({ email: 'userA@mail.com', password: 'P@ssw0rd!' })

expect(res.status).toBe(200)
expect(res.body.access_token).toBeDefined()
expect(res.headers['set-cookie']?.join(';')).toContain('HttpOnly')

Passo a passo: teste de refresh (incluindo rotação)

  1. Faça login e capture refresh token (cookie ou body).
  2. Chame POST /auth/refresh com refresh token.
  3. Valide que um novo access token é emitido.
  4. Se houver rotação de refresh: valide que o refresh antigo não funciona mais e que um novo refresh foi emitido.
// Fluxo: refresh antigo deve falhar após rotação
const login = await request(app).post('/auth/login').send(creds)
const refreshCookie1 = login.headers['set-cookie']

const r1 = await request(app).post('/auth/refresh').set('Cookie', refreshCookie1)
expect(r1.status).toBe(200)
const refreshCookie2 = r1.headers['set-cookie']

const r2 = await request(app).post('/auth/refresh').set('Cookie', refreshCookie1)
expect([401, 403]).toContain(r2.status) // conforme seu contrato (ver matriz)

Passo a passo: teste de logout

  1. Faça login.
  2. Chame POST /auth/logout enviando refresh token.
  3. Valide que o refresh token foi revogado (ex.: registro marcado como revoked, ou removido).
  4. Tente /auth/refresh novamente com o mesmo refresh e valide falha segura.

Testes de segurança básicos (negativos) para endpoints protegidos

Mapeamento correto: 401 vs 403

  • 401 Unauthorized: o cliente não está autenticado ou a autenticação é inválida (sem token, token expirado, assinatura inválida, token malformado). Deve incluir WWW-Authenticate quando usar Bearer.
  • 403 Forbidden: o cliente está autenticado, mas não tem permissão para a ação/recurso (RBAC/ABAC negou). Normalmente não inclui WWW-Authenticate.

Defina isso como critério de aceitação e teste explicitamente, para evitar inconsistências entre endpoints.

Casos essenciais

  • Sem credenciais: chamar endpoint protegido sem header/cookie → 401.
  • Token expirado: gerar token com exp no passado (via FakeClock) → 401 e erro específico (ex.: token_expired).
  • Token adulterado: alterar um caractere do token → 401 (assinatura inválida).
  • Token com kid desconhecido (se usar JWKS/kid): → 401 (chave não encontrada).
  • Token válido, sem permissão: usuário autenticado acessa recurso proibido → 403.
// Exemplo: token adulterado
const token = await issueAccessToken({ sub: 'userA' })
const tampered = token.slice(0, -1) + (token.slice(-1) === 'a' ? 'b' : 'a')

const res = await request(app)
  .get('/projects/project2')
  .set('Authorization', `Bearer ${tampered}`)

expect(res.status).toBe(401)
expect(res.headers['www-authenticate']).toContain('Bearer')

Estratégias práticas para simular expiração e janelas de tolerância

Expiração do access token

Com FakeClock:

  1. Emita token com exp = now + 60s.
  2. Valide acesso permitido imediatamente.
  3. Avance o relógio em 61s.
  4. Valide 401 e mensagem/erro esperado.

Clock skew (se aplicável)

Se seu validador aceita tolerância (ex.: 30s), crie testes nos limites:

  • exp = now - 10s ainda aceito (se tolerância permitir).
  • exp = now - 31s rejeitado.

Testando rotação de chaves (assinatura) de ponta a ponta

Cenário: duas chaves válidas durante transição

  1. Configure FakeKeyStore com key-1 e assine tokens com ela.
  2. Rotacione para key-2 (assinatura nova), mas mantenha key-1 no conjunto de validação.
  3. Valide que tokens assinados com key-1 e key-2 são aceitos.
  4. Retire key-1 do conjunto de validação.
  5. Valide que token antigo falha com 401 e que token novo continua válido.
// Pseudocódigo de asserções
keyStore.rotateTo('key-2', 'secret2')
// validar token com key-1 ainda funciona
// retirar key-1
keyStore.retire('key-1')
// token com kid=key-1 deve falhar (401)

Matriz de casos de teste (mínimo recomendado)

IDTipoEndpoint/ÁreaPré-condiçãoAçãoEsperadoHeaders/Detalhes
A1IntegraçãoPOST /auth/loginUsuário existeCredenciais válidas200 + tokens emitidosSet-Cookie (se cookie), sem WWW-Authenticate
A2IntegraçãoPOST /auth/loginUsuário existeSenha inválida401Mensagem genérica (não revelar qual campo falhou)
A3IntegraçãoPOST /auth/refreshRefresh válidoSolicitar refresh200 + novo accessSe rotação: novo refresh em Set-Cookie
A4SegurançaPOST /auth/refreshRefresh rotacionadoReusar refresh antigo401 (ou 403 conforme contrato)Não vazar motivo sensível; opcional: invalidar cadeia
A5IntegraçãoPOST /auth/logoutRefresh válidoLogout204 (ou 200)Set-Cookie limpando refresh (Max-Age=0) se cookie
S1SegurançaGET /recurso-protegidoNenhumaSem token401WWW-Authenticate: Bearer
S2SegurançaGET /recurso-protegidoToken expiradoEnviar token401Erro: token_expired (ou código padronizado)
S3SegurançaGET /recurso-protegidoToken adulteradoEnviar token alterado401WWW-Authenticate: Bearer error=invalid_token
S4SegurançaGET /recurso-protegidokid desconhecidoEnviar token com kid inválido401Falha segura, sem detalhes internos
Z1UnitárioPolicy RBACrole=useraction=admin:*Denyreason=insufficient_role
Z2UnitárioPolicy ABACowner mismatchupdate resourceDenyreason=not_owner
Z3IntegraçãoGET /resource/:idToken válido (userA)Acessar recurso de userB403Sem WWW-Authenticate
K1IntegraçãoValidação JWTRotação ativaToken assinado com key-1200 durante janelakid reconhecido
K2IntegraçãoValidação JWTkey-1 retiradaToken assinado com key-1401WWW-Authenticate: Bearer

Critérios de aceitação para respostas HTTP (padronização)

Status codes

  • 401 quando: ausência de credenciais, credenciais inválidas, token expirado, assinatura inválida, token malformado, kid desconhecido.
  • 403 quando: autenticado, mas política negou acesso ao recurso/ação.

Headers

  • Para Bearer: em 401, incluir WWW-Authenticate: Bearer e, se padronizado, parâmetros como error="invalid_token" e error_description (sem detalhes sensíveis).
  • Para cookie: em logout, incluir Set-Cookie limpando o cookie (ex.: Max-Age=0), e em login/refresh, flags esperadas (HttpOnly, Secure, SameSite conforme seu contrato).

Corpo de erro (mensagens)

Padronize um formato e teste a estabilidade do contrato. Exemplo:

{
  "error": "unauthorized",
  "code": "token_expired",
  "message": "Authentication required"
}
  • Mensagens devem ser genéricas em falhas de login e validação de token.
  • O campo code pode ser específico para facilitar observabilidade e testes, sem expor detalhes internos.

Dicas para manter testes rápidos e confiáveis

  • Evite dependências externas em unitários; use fakes para repositórios, clock e keystore.
  • Em integração, prefira DB efêmero (in-memory ou container) e limpe estado por teste (transações/rollback ou truncation).
  • Teste limites: expiração no exato segundo, troca de chave, refresh reutilizado, usuário desabilitado.
  • Asserte o que importa: status, headers críticos, formato do body e efeitos colaterais (revogação/rotação).

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

Ao estruturar testes de autenticação e autorização no back-end, qual combinação de tipos de teste é a mais eficiente para cobrir regras de decisão e fluxos completos com segurança?

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

Você errou! Tente novamente.

A abordagem recomendada combina unitários para regras/políticas (rápidos e determinísticos) e integração para fluxos reais e bordas de segurança, garantindo contratos HTTP e falha segura em cenários como token inválido ou expirado.

Próximo capitúlo

Arquiteturas e decisões: quando usar sessão, quando usar JWT e combinações seguras

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

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.