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,resourceecontexte retornaallow/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.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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-1ekey-2durante 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)
| Categoria | Caso | Entrada | Esperado |
|---|---|---|---|
| RBAC | Admin pode deletar projeto | role=admin, action=delete, resource=project | allowed=true |
| RBAC | User não pode deletar projeto | role=user, action=delete | allowed=false, reason=insufficient_role |
| ABAC | Owner pode editar próprio projeto | userId=userA, project.ownerId=userA | allowed=true |
| ABAC | Não-owner não pode editar | userId=userA, project.ownerId=userB | allowed=false, reason=not_owner |
| Fail closed | Sem tenant no contexto | context.tenantId ausente | allowed=false, reason=missing_context |
Passo a passo: escrevendo um teste unitário de política
- Arrange: monte subject, resource e context com fixtures.
- Act: chame
authorize(). - Assert: valide
allowedereason. 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
FakeClockeFakeKeyStorepara controlar expiração e rotação.
Passo a passo: teste de login
- Crie usuário fixture no banco (com senha já hashada via helper de teste).
- Faça
POST /auth/logincom credenciais válidas. - Valide
200(ou201conforme contrato), presença deaccess_tokenno body (ou cookie), e refresh token (cookie HttpOnly ou retorno conforme seu design). - Valide headers relevantes: se usar Bearer, normalmente não há
WWW-Authenticateem sucesso; se usar cookie, valideSet-Cookiecom 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)
- Faça login e capture refresh token (cookie ou body).
- Chame
POST /auth/refreshcom refresh token. - Valide que um novo access token é emitido.
- 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
- Faça login.
- Chame
POST /auth/logoutenviando refresh token. - Valide que o refresh token foi revogado (ex.: registro marcado como revoked, ou removido).
- Tente
/auth/refreshnovamente 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-Authenticatequando 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
expno passado (viaFakeClock) →401e 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:
- Emita token com
exp = now + 60s. - Valide acesso permitido imediatamente.
- Avance o relógio em 61s.
- Valide
401e mensagem/erro esperado.
Clock skew (se aplicável)
Se seu validador aceita tolerância (ex.: 30s), crie testes nos limites:
exp = now - 10sainda aceito (se tolerância permitir).exp = now - 31srejeitado.
Testando rotação de chaves (assinatura) de ponta a ponta
Cenário: duas chaves válidas durante transição
- Configure
FakeKeyStorecomkey-1e assine tokens com ela. - Rotacione para
key-2(assinatura nova), mas mantenhakey-1no conjunto de validação. - Valide que tokens assinados com
key-1ekey-2são aceitos. - Retire
key-1do conjunto de validação. - Valide que token antigo falha com
401e 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)
| ID | Tipo | Endpoint/Área | Pré-condição | Ação | Esperado | Headers/Detalhes |
|---|---|---|---|---|---|---|
| A1 | Integração | POST /auth/login | Usuário existe | Credenciais válidas | 200 + tokens emitidos | Set-Cookie (se cookie), sem WWW-Authenticate |
| A2 | Integração | POST /auth/login | Usuário existe | Senha inválida | 401 | Mensagem genérica (não revelar qual campo falhou) |
| A3 | Integração | POST /auth/refresh | Refresh válido | Solicitar refresh | 200 + novo access | Se rotação: novo refresh em Set-Cookie |
| A4 | Segurança | POST /auth/refresh | Refresh rotacionado | Reusar refresh antigo | 401 (ou 403 conforme contrato) | Não vazar motivo sensível; opcional: invalidar cadeia |
| A5 | Integração | POST /auth/logout | Refresh válido | Logout | 204 (ou 200) | Set-Cookie limpando refresh (Max-Age=0) se cookie |
| S1 | Segurança | GET /recurso-protegido | Nenhuma | Sem token | 401 | WWW-Authenticate: Bearer |
| S2 | Segurança | GET /recurso-protegido | Token expirado | Enviar token | 401 | Erro: token_expired (ou código padronizado) |
| S3 | Segurança | GET /recurso-protegido | Token adulterado | Enviar token alterado | 401 | WWW-Authenticate: Bearer error=invalid_token |
| S4 | Segurança | GET /recurso-protegido | kid desconhecido | Enviar token com kid inválido | 401 | Falha segura, sem detalhes internos |
| Z1 | Unitário | Policy RBAC | role=user | action=admin:* | Deny | reason=insufficient_role |
| Z2 | Unitário | Policy ABAC | owner mismatch | update resource | Deny | reason=not_owner |
| Z3 | Integração | GET /resource/:id | Token válido (userA) | Acessar recurso de userB | 403 | Sem WWW-Authenticate |
| K1 | Integração | Validação JWT | Rotação ativa | Token assinado com key-1 | 200 durante janela | kid reconhecido |
| K2 | Integração | Validação JWT | key-1 retirada | Token assinado com key-1 | 401 | WWW-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: Bearere, se padronizado, parâmetros comoerror="invalid_token"eerror_description(sem detalhes sensíveis). - Para cookie: em logout, incluir
Set-Cookielimpando o cookie (ex.:Max-Age=0), e em login/refresh, flags esperadas (HttpOnly,Secure,SameSiteconforme 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
codepode 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).