Um guia prático para autenticação segura para desenvolvedores


A autenticação é um daqueles sistemas em que um erro não causa apenas um bug, mas também uma violação. A maioria das vulnerabilidades de autenticação não são ataques criptográficos exóticos, são um punhado de erros bem conhecidos, armazenar um token em algum lugar que o JavaScript possa lê-lo, confiar em um JWT sem validar seu algoritmo, esquecer de limitar a taxa de um endpoint de login, repetido entre projetos porque a versão segura não é significativamente mais difícil de escrever, apenas menos óbvia como o padrão. Este é um passo a passo prático das decisões de autenticação que realmente importam, com exemplos práticos de Node.js.

Sessões vs. JWTs não é realmente a questão

Muitos conselhos de autenticação enquadram cookies de sessão e JWTs como opções concorrentes. Na prática, a questão mais útil é onde reside o estado de autenticação e quem pode vê-lo, e ambas as abordagens podem ser implementadas de forma segura ou insegura, dependendo dessa resposta.

Uma sessão tradicional mantém o estado do lado do servidor, o cookie contém apenas um ID de sessão opaco e o guia do SitePoint para criando e verificando JWTs com Node cobre a alternativa: um token assinado e independente que o servidor pode validar sem uma consulta ao banco de dados. A compensação é real, as sessões são trivialmente revogáveis ​​através da exclusão do estado do lado do servidor, os JWTs não são revogáveis ​​sem infraestrutura extra, uma vez que o token em si permanece válido até expirar, independentemente do que acontece no lado do servidor.

Armazenando tokens onde o JavaScript não consegue lê-los

O erro mais comum do JWT não está na forma como o token é assinado, mas no local onde ele é armazenado após o cliente recebê-lo.

javascript



localStorage.setItem('authToken', token);


res.cookie('authToken', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 3600000,
});

localStorage é legível por qualquer JavaScript em execução na página, o que significa que uma única vulnerabilidade XSS em qualquer lugar do aplicativo, mesmo em um script de terceiros não relacionado, pode exfiltrar todos os tokens armazenados. Um HttpOnly O cookie nunca é exposto ao JavaScript, o que remove todo o caminho de ataque, independentemente do que mais der errado na página. Secure garante que o cookie seja transmitido apenas por HTTPS e SameSite: strict limita o envio em solicitações entre sites, o que ajuda contra CSRF.

Validando o algoritmo JWT explicitamente

Uma vulnerabilidade específica e bem documentada do JWT é a confusão de algoritmos: algumas bibliotecas, se não forem configuradas com cuidado, aceitarão o algoritmo autodeclarado de um token em vez de impor aquele que o servidor realmente espera. Um invasor que consegue fazer com que um servidor aceite um token não assinado ou assinado de forma diferente falsificou efetivamente a autenticação.

javascript

const jwt = require('jsonwebtoken');


const unsafeDecoded = jwt.verify(token, secret);

javascript


const decoded = jwt.verify(token, secret, { algorithms: ('HS256') });

Passagem algorithms significa explicitamente que o servidor rejeita um token assinado ou afirma usar um algoritmo diferente daquele realmente configurado, fechando uma classe de falsificação que depende da ambiguidade do algoritmo em vez de quebrar a assinatura em si.

Tokens de acesso de curta duração, tokens de atualização de vida mais longa

Um JWT válido por 30 dias é uma janela de exposição de 30 dias se for roubado. A mitigação comum é dividir a autenticação em um token de acesso de curta duração e um token de atualização separado e de maior duração, usado apenas para cunhar novos tokens de acesso.

javascript

function issueTokenPair(userId) {
  const accessToken = jwt.sign({ userId }, ACCESS_SECRET, { expiresIn: '15m' });
  const refreshToken = jwt.sign({ userId }, REFRESH_SECRET, { expiresIn: '7d' });
  return { accessToken, refreshToken };
}

app.post('/refresh', (req, res) => {
  const { refreshToken } = req.cookies;
  try {
    const { userId } = jwt.verify(refreshToken, REFRESH_SECRET, { algorithms: ('HS256') });
    const { accessToken } = issueTokenPair(userId);
    res.cookie('authToken', accessToken, { httpOnly: true, secure: true, sameSite: 'strict' });
    res.status(200).end();
  } catch {
    res.status(401).end();
  }
});

Um token de acesso roubado só é útil por 15 minutos. O token de atualização apresenta mais riscos se for roubado, pois tem vida mais longa, por isso vale a pena armazená-lo com o mesmo HttpOnly proteções de cookies e, idealmente, rastreamento de tokens de atualização emitidos no lado do servidor para que um token específico possa ser revogado se um dispositivo for comprometido.

Especificamente pontos finais de autenticação com limitação de taxa

Um endpoint de login sem limitação de taxa pode sofrer força bruta, independentemente da força do hash da senha abaixo dele. Isso merece limites mais rígidos do que a limitação geral da taxa de API, uma vez que o custo de um falso positivo, um usuário legítimo temporariamente bloqueado, é muito menor do que o custo de permitir suposições sustentadas.

javascript

const rateLimit = require('express-rate-limit');

const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Too many login attempts, please try again later' },
  skipSuccessfulRequests: true,
});

app.post('/login', loginLimiter, (req, res) => {
  
});

skipSuccessfulRequests: true O que importa aqui, significa que o limite conta apenas as tentativas malsucedidas, portanto, um usuário que digita incorretamente sua senha algumas vezes antes de ter sucesso não é penalizado da mesma forma que alguém que adivinha sistematicamente. Os fluxos de recuperação de conta merecem o mesmo tratamento e, especificamente para verificação de código de backup ou token de recuperação, o guia do SitePoint para Recuperação de senha WebAuthn abrange sessões de recuperação com limite de tempo e as mesmas preocupações de força bruta aplicadas a esse fluxo mais estreito e igualmente sensível.

Autenticação multifator como uma segunda verificação independente

A autenticação baseada em senha, por mais bem implementada que seja, é um fator único. Adicionar um segundo fator independente, um código único baseado em tempo, é um avanço significativo, especificamente porque não depende da senha permanecer secreta.

javascript

const speakeasy = require('speakeasy');

function verifyTotpCode(secret, userProvidedCode) {
  return speakeasy.totp.verify({
    secret,
    encoding: 'base32',
    token: userProvidedCode,
    window: 1, 
  });
}

O window O parâmetro controla quanto desvio de clock é tolerado entre o servidor e o aplicativo autenticador do usuário. Mantê-lo restrito, uma etapa em vez de várias, limita a janela de tempo durante a qual um código vazado permanece válido, sem ser tão rigoroso que pequenas diferenças de relógio bloqueiem usuários legítimos. O MFA também está dentro de um quadro mais amplo de gerenciamento de identidade e acesso que inclui privilégios mínimos, manipulação de sessão e ciclo de vida da conta, e os desenvolvedores que desejam entender como essas peças se relacionam podem começar com material de aprendizagem como Treinamento CISSP.

Perguntas frequentes

Devo usar cookies de sessão ou JWTs para autenticação? Ambos podem ser implementados com segurança. As sessões são mais fáceis de revogar imediatamente, pois o estado reside no lado do servidor; Os JWTs evitam uma consulta ao banco de dados por solicitação, mas são mais difíceis de revogar antes de expirarem. Escolha com base no fato de a revogabilidade imediata ser mais importante do que a compensação para o seu aplicativo.

Por que armazenar um JWT no localStorage é considerado arriscado? Qualquer JavaScript em execução na página, incluindo um script injetado de uma vulnerabilidade XSS em qualquer lugar do aplicativo, pode ler localStorage. Um cookie HttpOnly nunca é exposto ao JavaScript, o que remove totalmente esse caminho de ataque específico.

O que é confusão no algoritmo JWT? Uma classe de vulnerabilidade em que um servidor aceita qualquer algoritmo de assinatura que um token afirma usar, em vez de impor um algoritmo específico esperado, o que pode permitir que um invasor forje um token no qual o servidor confia incorretamente. Restringir explicitamente os algoritmos aceitos durante a verificação fecha essa lacuna.

Por que usar tokens separados de acesso e atualização em vez de um token de longa duração? Um token de acesso de curta duração limita a janela de exposição se for roubado, enquanto um token de atualização separado e de longa duração é usado apenas para criar novos tokens de acesso e pode ser rastreado e revogado individualmente no servidor, se necessário.

Resumo

A autenticação segura, na prática, se resume a um conjunto consistente de decisões: escolher onde os tokens residem com base em quem pode lê-los, validar algoritmos JWT explicitamente em vez de confiar no que um token afirma, dividir o acesso de longa duração em tokens de curta duração mais um mecanismo de atualização revogável, limitar a taxa de pontos de extremidade de autenticação de forma mais rígida do que o tráfego geral da API e tratar a MFA como um segundo fator genuinamente independente, em vez de uma caixa de seleção. Nenhuma dessas técnicas são avançadas, e é exatamente por isso que vale a pena construí-las como padrões, em vez de exceções aplicadas somente depois que algo dá errado.



Source link