Autenticação JWT: Melhores Práticas de Segurança em 2026
JSON Web Tokens estão por toda parte – mas a maioria das implementações tem pelo menos uma falha crítica de segurança. A superfície de ataque é maior do que parece: ataques de confusão de algoritmos, roubo de tokens via XSS, falta de validação de vencimento e gerenciamento inadequado de segredos estão entre as vulnerabilidades mais comuns encontradas em sistemas de produção. Acertar o JWT não é uma questão de ligar para uma biblioteca e seguir em frente; requer decisões deliberadas em todas as camadas.
Este guia cobre todo o ciclo de vida de segurança do JWT - desde a seleção do algoritmo de assinatura e estrutura de token até o armazenamento, rotação, revogação e padrões de implementação NestJS do mundo real - para que você possa construir uma autenticação que realmente resista a ataques.
Principais conclusões
- Utilize sempre RS256 (assimétrico) para sistemas distribuídos; HS256 somente quando o servidor API é emissor e verificador
- Armazene tokens em cookies HttpOnly, Secure, SameSite=Lax — nunca em localStorage ou sessionStorage
- Sempre valide as declarações
exp,iss,audealg— nunca confie no algoritmononenão assinado- Implementar rotação de token de atualização: cada atualização emite um novo par e invalida o token de atualização antigo
- Manter o TTL do token de acesso curto (15 minutos); use tokens de atualização opacos armazenados no banco de dados
- Nunca armazene dados confidenciais (senhas, SSNs, informações de pagamento) em cargas JWT — as cargas são base64, não criptografadas
- Implementar revogação de token por meio de uma lista de bloqueios do Redis ou contador de versões no banco de dados
- Registrar todos os eventos de emissão e atualização de tokens para trilhas de auditoria de segurança
Estrutura e reivindicações do JWT
Um JWT tem três partes: cabeçalho, carga útil e assinatura, separadas por pontos e codificadas em base64url.
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc0MjM0NTYwMCwiZXhwIjoxNzQyMzQ2NTAwLCJpc3MiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHBzOi8vYXBwLmV4YW1wbGUuY29tIn0.
[signature]
Carga útil decodificada:
{
"sub": "user_123",
"email": "[email protected]",
"role": "admin",
"iat": 1742345600,
"exp": 1742346500,
"iss": "https://api.example.com",
"aud": "https://app.example.com"
}
Reivindicações necessárias para produção:
sub— identificador exclusivo do usuário (nunca enviar e-mail sozinho — os e-mails mudam)exp— carimbo de data e hora de expiração (sempre obrigatório)iat— carimbo de data/hora emitido em (detecta distorção do relógio)iss— URL do emissor (validar em relação ao emissor esperado)aud— público (validar para evitar a reutilização de token entre serviços)jti— ID JWT (exclusivo por token, permite revogação exata)
RS256 vs HS256: Qual algoritmo usar
Esta é a decisão de segurança mais impactante na configuração do JWT.
HS256 (HMAC-SHA256) — Simétrico
// Both signing and verifying require the same secret
const token = jwt.sign(payload, process.env.JWT_SECRET, { algorithm: 'HS256' });
const verified = jwt.verify(token, process.env.JWT_SECRET);
Use somente quando: O serviço que assina os tokens é o mesmo serviço que os verifica. O HS256 é bom para APIs monolíticas, mas perigoso em microsserviços – qualquer serviço que possa verificar tokens também pode criá-los.
RS256 (RSA-SHA256) — Assimétrico
// Sign with private key (only the auth server holds this)
const privateKey = fs.readFileSync('/secrets/jwt-private.pem');
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256', keyid: 'key-2026-01' });
// Verify with public key (any service can do this safely)
const publicKey = fs.readFileSync('/secrets/jwt-public.pem');
const verified = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // NEVER omit this — prevents algorithm confusion
issuer: 'https://auth.example.com',
audience: 'https://api.example.com',
});
Gere um par de chaves RSA de produção:
# Generate 4096-bit RSA private key
openssl genrsa -out jwt-private.pem 4096
# Extract public key
openssl rsa -in jwt-private.pem -pubout -out jwt-public.pem
Rotação de chaves: Use keyid (kid) no cabeçalho. Publique as chaves públicas atuais em /.well-known/jwks.json. Os serviços armazenam em cache o JWKS e buscam em kid desconhecido.
Armazenamento de cookies somente Http
Isso não é negociável. Armazenar tokens em localStorage ou sessionStorage os torna acessíveis a qualquer JavaScript em execução na página – incluindo scripts injetados de ataques XSS.
// NestJS: set HttpOnly cookie after authentication
@Post('login')
async login(@Body() dto: LoginDto, @Res({ passthrough: true }) res: Response) {
const { accessToken, refreshToken } = await this.authService.login(dto);
const cookieBase = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax' as const,
path: '/',
domain: process.env.COOKIE_DOMAIN, // '.example.com' for subdomain sharing
};
res.cookie('access_token', accessToken, {
...cookieBase,
maxAge: 15 * 60 * 1000, // 15 minutes
});
res.cookie('refresh_token', refreshToken, {
...cookieBase,
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
path: '/auth/refresh', // Scope refresh token to only the refresh endpoint
});
return { message: 'Login successful' };
}
// Extract token from cookie in JWT strategy
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(req) => req?.cookies?.access_token, // Cookie first
ExtractJwt.fromAuthHeaderAsBearerToken(), // Bearer fallback for API clients
]),
secretOrKey: configService.get('JWT_PUBLIC_KEY'),
algorithms: ['RS256'],
issuer: configService.get('JWT_ISSUER'),
audience: configService.get('JWT_AUDIENCE'),
});
}
async validate(payload: JwtPayload): Promise<AuthenticatedUser> {
// Always check token version against database
const user = await this.usersService.findById(payload.sub);
if (!user || user.tokenVersion !== payload.tokenVersion) {
throw new UnauthorizedException('Token invalidated');
}
return { id: payload.sub, email: payload.email, role: payload.role };
}
}
Atualizar rotação de token
Nunca emita um token de acesso de longa duração. Em vez disso, mantenha os tokens de acesso de curta duração e alterne os tokens de atualização a cada uso.
// auth.service.ts
@Injectable()
export class AuthService {
async refreshTokens(refreshToken: string, ipAddress: string) {
// 1. Look up the refresh token in the database
const storedToken = await this.db.query.refreshTokens.findFirst({
where: and(
eq(refreshTokens.token, this.hashToken(refreshToken)),
eq(refreshTokens.revoked, false),
gt(refreshTokens.expiresAt, new Date())
),
with: { user: true },
});
if (!storedToken) {
// Possible reuse attack — revoke all tokens for this user
if (storedToken?.userId) {
await this.revokeAllUserTokens(storedToken.userId);
await this.alertService.send({
message: `Refresh token reuse detected for user ${storedToken.userId}`,
severity: 'high',
});
}
throw new UnauthorizedException('Invalid refresh token');
}
// 2. Revoke the used refresh token (rotation)
await this.db
.update(refreshTokens)
.set({ revoked: true, revokedAt: new Date(), revokedByIp: ipAddress })
.where(eq(refreshTokens.id, storedToken.id));
// 3. Issue new token pair
const newAccessToken = this.issueAccessToken(storedToken.user);
const newRefreshToken = await this.issueRefreshToken(
storedToken.user.id,
ipAddress,
storedToken.family // Track token families for reuse detection
);
return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}
private hashToken(token: string): string {
return crypto.createHash('sha256').update(token).digest('hex');
}
}
O esquema do banco de dados para tokens de atualização:
CREATE TABLE refresh_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token VARCHAR(64) NOT NULL UNIQUE, -- SHA256 hash of the actual token
family UUID NOT NULL, -- Token family for reuse detection
expires_at TIMESTAMPTZ NOT NULL,
revoked BOOLEAN DEFAULT false,
revoked_at TIMESTAMPTZ,
revoked_by_ip INET,
created_at TIMESTAMPTZ DEFAULT now(),
created_by_ip INET
);
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_token ON refresh_tokens(token);
Estratégias de revogação de token
Os JWTs são apátridas por design – uma vez emitidos, você não pode “retirá-los” sem infraestrutura adicional. Aqui estão três abordagens classificadas por trade-off:
1. TTL curto (15 minutos)
A estratégia de revogação mais simples: os tokens de acesso expiram rápido o suficiente para que raramente seja necessário revogá-los. Combine com a revogação imediata do token de atualização no banco de dados.
2. Contador de versão do token
Armazene um tokenVersion na tabela de usuários. Aumente-o para invalidar todos os tokens existentes:
// Increment version to logout all sessions
await this.db
.update(users)
.set({ tokenVersion: sql`token_version + 1` })
.where(eq(users.id, userId));
// In JWT payload
const payload = {
sub: user.id,
tokenVersion: user.tokenVersion, // embedded at sign time
};
// In JwtStrategy.validate()
if (user.tokenVersion !== payload.tokenVersion) {
throw new UnauthorizedException();
}
Requer uma consulta de banco de dados por solicitação — aceitável para a maioria dos aplicativos.
3. Lista de bloqueios do Redis
Para revogação imediata sem pesquisas no banco de dados:
// Revoke specific token by JTI
async revokeToken(jti: string, expiresIn: number): Promise<void> {
const key = `token:revoked:${jti}`;
await this.redis.set(key, '1', 'EX', expiresIn);
}
// Check in JWT strategy before accepting
async validate(payload: JwtPayload): Promise<AuthenticatedUser> {
const revoked = await this.redis.get(`token:revoked:${payload.jti}`);
if (revoked) {
throw new UnauthorizedException('Token revoked');
}
return this.buildUser(payload);
}
Compensação: uma pesquisa do Redis por solicitação, mas o Redis é O(1) e inferior a milissegundos. Aceitável para endpoints de alta segurança.
Vulnerabilidades e Mitigações Comuns
Ataque de confusão de algoritmo
O ataque alg: "none": um invasor remove a assinatura e define alg como none e, em seguida, envia uma carga útil adulterada. Bibliotecas que aceitam tokens não assinados aceitarão qualquer carga útil.
// WRONG — never do this
jwt.verify(token, secret); // Accepts alg:none if library allows it
// CORRECT — always specify algorithms explicitly
jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Whitelist only what you use
});
Injeção de cabeçalho JWT (jwk/jku)
Os invasores criam um JWT com um cabeçalho jku (URL JWKS) ou jwk (chave embutida) apontando para seu próprio servidor de chaves e, em seguida, assinam com sua própria chave privada. Um verificador vulnerável busca as chaves do invasor e aceita o token.
// WRONG — never fetch keys from the token header
const jwksUri = decodedHeader.jku; // Attacker-controlled!
// CORRECT — always use a pinned, config-driven JWKS URI
const jwksClient = createRemoteJWKSet(new URL(configService.get('JWKS_URI')));
Segredos fracos para HS256
Um segredo ASCII de 32 caracteres para HS256 tem cerca de 190 bits de entropia – insuficiente. Use pelo menos 256 bits de uma fonte aleatória criptograficamente segura:
# Generate a strong HS256 secret
node -e "console.log(require('crypto').randomBytes(64).toString('base64url'))"
Configuração do módulo NestJS JWT
// auth.module.ts
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
privateKey: config.get('JWT_PRIVATE_KEY'),
publicKey: config.get('JWT_PUBLIC_KEY'),
signOptions: {
algorithm: 'RS256',
expiresIn: '15m',
issuer: config.get('JWT_ISSUER'),
audience: config.get('JWT_AUDIENCE'),
},
verifyOptions: {
algorithms: ['RS256'],
issuer: config.get('JWT_ISSUER'),
audience: config.get('JWT_AUDIENCE'),
},
}),
}),
],
})
export class AuthModule {}
Endpoint JWKS para distribuição de chave pública
// jwks.controller.ts
@Controller('.well-known')
export class JwksController {
@Get('jwks.json')
@Public()
async getJwks() {
const publicKeyPem = this.configService.get('JWT_PUBLIC_KEY');
// Convert PEM to JWK format using the 'jose' library
const publicKey = await importSPKI(publicKeyPem, 'RS256');
const jwk = await exportJWK(publicKey);
return {
keys: [
{
...jwk,
use: 'sig',
alg: 'RS256',
kid: this.configService.get('JWT_KEY_ID'), // e.g., 'key-2026-01'
},
],
};
}
}
Armazene esse endpoint em cache de forma agressiva – as chaves públicas raramente mudam. Defina Cache-Control: public, max-age=3600.
Lista de verificação de segurança
Antes de implantar a autenticação JWT na produção, verifique:
- [] Algoritmo definido explicitamente como
RS256ouHS256em chamadas de assinatura e verificação - []
exp,iss,audvalidados em cada solicitação - [] Token de acesso TTL ≤ 15 minutos
- [] Tokens armazenados em cookies HttpOnly, Secure, SameSite=Lax
- [] Rotação de token de atualização implementada - token antigo revogado em cada uso
- [] Atualizar token armazenado como hash SHA256 no banco de dados (não texto simples)
- [] declaração
jtiadicionada para capacidade de revogação direcionada - [] Lista de bloqueios do Redis verificada para endpoints de alta segurança
- [] Endpoint JWKS publicado para serviços distribuídos
- [] Chaves privadas armazenadas no gerenciador de segredos (AWS Secrets Manager, Vault)
- [] Procedimento de rotação de chave documentado e testado
- [] Todos os eventos de emissão e invalidação de token registrados
Perguntas frequentes
É seguro decodificar um JWT sem verificar a assinatura?
A decodificação sem verificação é segura para leitura da carga útil, mas nunca confie no conteúdo para autorização. Sempre verifique a assinatura antes de agir em relação às reivindicações. A função jwt.decode() na maioria das bibliotecas ignora a verificação - use-a apenas para diagnóstico ou para ler kid do cabeçalho antes de selecionar a chave correta para verificação.
Devo usar cookies ou cabeçalhos de autorização para aplicativos de navegador?
Cookies HttpOnly para aplicativos de navegador, cabeçalhos de autorização para aplicativos móveis nativos e chamadas de API de servidor para servidor. Os cookies são imunes à exfiltração XSS (o JavaScript não consegue ler cookies HttpOnly). Os aplicativos móveis não podem usar cookies de forma eficaz e usar tokens de portador armazenados no keystore seguro do dispositivo.
Como lidar com a expiração do token no frontend?
Intercepte respostas 401 e tente uma atualização silenciosa antes de tentar novamente a solicitação original. No React, use um Axios ou um interceptador de busca. Se a atualização também falhar (expirada ou revogada), redirecione para login. Mantenha uma única promessa de atualização durante o voo para evitar tempestades de atualização paralelas.
Qual é a diferença entre tokens de acesso e tokens de atualização?
Os tokens de acesso têm vida curta (15 minutos), não têm estado e são verificados com uma chave pública ou segredo compartilhado em cada solicitação de API. Os tokens de atualização são de longa duração (7 a 30 dias), opacos (strings aleatórias, não JWTs) e armazenados no lado do servidor no banco de dados. O ponto final do token de atualização é o único local onde os tokens de atualização são usados – escopo deles com um caminho de cookie estreito.
Posso armazenar funções de usuário na carga JWT?
Sim, mas esteja ciente de que as funções codificadas no token são armazenadas em cache até que o token expire. Se você revogar a função de administrador de um usuário, ele a manterá até que o token de acesso atual expire (até 15 minutos). Para alterações de função de alta segurança, adicione também o usuário a uma lista de bloqueios do Redis ou aumente a versão do token para forçar a reautenticação imediata.
Como implementar "lembrar de mim" com JWTs?
Emita um token de atualização de maior duração (90 dias) quando o usuário marca "lembrar de mim" em vez dos 30 dias padrão. Armazene um sinalizador persistent na linha do banco de dados do token de atualização para que você possa exibir e revogar sessões persistentes separadamente nas configurações de segurança do usuário. Nunca estenda o TTL do token de acesso – isso vai contra o propósito.
Próximas etapas
A autenticação JWT bem feita é a base de todo aplicativo da web seguro. Da assinatura RS256 ao armazenamento de cookies HttpOnly, rotação de token de atualização e estratégias de revogação, os padrões neste guia protegem seus usuários contra os ataques de autenticação mais comuns.
ECOSIRE implementa arquitetura de autenticação testada em batalha — incluindo integração OIDC com Authentik, fluxos de cookies HttpOnly e gerenciamento de token apoiado por Redis — em todos os nossos projetos de back-end. Explore nossos serviços de desenvolvimento focados em segurança para saber como podemos fortalecer sua camada de autenticação.
Escrito por
ECOSIRE Research and Development Team
Construindo produtos digitais de nível empresarial na ECOSIRE. Compartilhando insights sobre integrações Odoo, automação de e-commerce e soluções de negócios com IA.
Artigos Relacionados
API Rate Limiting: Patterns and Best Practices
Master API rate limiting with token bucket, sliding window, and fixed counter patterns. Protect your backend with NestJS throttler, Redis, and real-world configuration examples.
Authentik OIDC/SSO: Complete Integration Guide
Complete Authentik OIDC and SSO integration guide: OAuth2 provider setup, Next.js callback handling, NestJS JWT validation, user provisioning, and production configuration.
Cybersecurity Trends 2026-2027: Zero Trust, AI Threats, and Defense
The definitive guide to cybersecurity trends for 2026-2027—AI-powered attacks, zero trust implementation, supply chain security, and building resilient security programs.