Autenticación JWT: mejores prácticas de seguridad en 2026
Los tokens web JSON están en todas partes, pero la mayoría de las implementaciones tienen al menos una falla de seguridad crítica. La superficie de ataque es mayor de lo que parece: ataques de confusión de algoritmos, robo de tokens a través de XSS, falta de validación de caducidad y gestión inadecuada de secretos se encuentran entre las vulnerabilidades más comunes encontradas en los sistemas de producción. Obtener JWT correctamente no es cuestión de llamar a una biblioteca y seguir adelante; requiere decisiones deliberadas en todos los niveles.
Esta guía cubre el ciclo de vida completo de la seguridad de JWT, desde la selección del algoritmo de firma y la estructura del token hasta el almacenamiento, la rotación, la revocación y los patrones de implementación de NestJS en el mundo real, para que pueda crear una autenticación que realmente resista los ataques.
Conclusiones clave
- Utilice siempre RS256 (asimétrico) para sistemas distribuidos; HS256 solo cuando el servidor API es a la vez emisor y verificador
- Almacenar tokens en cookies HttpOnly, Secure, SameSite=Lax, nunca en localStorage o sessionStorage
- Valide siempre las reclamaciones
exp,iss,audyalg; nunca confíe en el algoritmononesin firmar- Implementar la rotación del token de actualización: cada actualización emite un nuevo par e invalida el token de actualización anterior
- Mantenga el TTL del token de acceso breve (15 minutos); utilizar tokens de actualización opacos almacenados en la base de datos
- Nunca almacene datos confidenciales (contraseñas, números de seguro social, información de pago) en cargas útiles de JWT; las cargas útiles son base64, no están cifradas
- Implementar la revocación de token a través de una lista de denegación de Redis o un contador de versiones en la base de datos.
- Registre todos los eventos de emisión y actualización de tokens para seguimientos de auditoría de seguridad
Estructura y reclamos de JWT
Un JWT tiene tres partes: encabezado, carga útil y firma, separadas por puntos y codificadas en 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"
}
Reclamaciones requeridas para la producción:
sub: identificador de usuario único (nunca solo envíe un correo electrónico; los correos electrónicos cambian)exp— marca de tiempo de vencimiento (siempre requerida)iat— marca de tiempo emitida en (detectar desviación del reloj)iss— URL del emisor (valídelo con el emisor esperado)aud— audiencia (validar para evitar la reutilización de tokens entre servicios)jti— ID de JWT (único por token, permite la revocación exacta)
RS256 vs HS256: qué algoritmo utilizar
Esta es la decisión de seguridad más impactante en la configuración 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);
Usar solo cuando: El servicio que firma los tokens es el mismo servicio que los verifica. HS256 está bien para API monolíticas, pero es peligroso en microservicios: cualquier servicio que pueda verificar tokens también puede crearlos.
RS256 (RSA-SHA256) — Asimé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',
});
Genere un par de claves RSA de producción:
# 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
Rotación de clave: utilice keyid (kid) en el encabezado. Publicar las claves públicas actuales en /.well-known/jwks.json. Los servicios almacenan en caché el JWKS y lo recuperan en kid desconocido.
Almacenamiento de cookies HttpOnly
Esto no es negociable. Almacenar tokens en localStorage o sessionStorage los hace accesibles para cualquier JavaScript que se ejecute en la página, incluidos los scripts inyectados 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 };
}
}
Actualizar rotación de tokens
Nunca emita un token de acceso de larga duración. En su lugar, mantenga los tokens de acceso de corta duración y rote los tokens de actualización en 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');
}
}
El esquema de base de datos para tokens de actualización:
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);
Estrategias de revocación de tokens
Los JWT no tienen estado por diseño: una vez emitidos, no se pueden "retirar" sin infraestructura adicional. A continuación se presentan tres enfoques clasificados según sus compensaciones:
1. TTL corto (15 minutos)
La estrategia de revocación más simple: los tokens de acceso caducan lo suficientemente rápido como para que rara vez sea necesario revocarlos. Emparéjelo con la revocación inmediata del token de actualización en la base de datos.
2. Contador de versión de token
Almacene un tokenVersion en la tabla de usuarios. Incrementelo para invalidar todos los 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();
}
Requiere una búsqueda en la base de datos por solicitud: aceptable para la mayoría de las aplicaciones.
3. Lista de denegados de Redis
Para revocación inmediata sin búsquedas en bases de datos:
// 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);
}
Compensación: una búsqueda de Redis por solicitud, pero Redis es O(1) y submilisegundo. Aceptable para puntos finales de alta seguridad.
Vulnerabilidades y mitigaciones comunes
Ataque de confusión de algoritmos
El ataque alg: "none": un atacante elimina la firma y establece alg en none, luego envía una carga útil manipulada. Las bibliotecas que aceptan tokens sin firmar aceptarán cualquier 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
});
Inyección de encabezado JWT (jwk/jku)
Los atacantes crean un JWT con un encabezado jku (URL JWKS) o jwk (clave en línea) que apunta a su propio servidor de claves y luego firman con su propia clave privada. Un verificador vulnerable obtiene las claves del atacante y acepta el 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')));
Secretos débiles para HS256
Un secreto ASCII de 32 caracteres para HS256 tiene alrededor de 190 bits de entropía, lo cual es insuficiente. Utilice al menos 256 bits de una fuente aleatoria criptográficamente segura:
# Generate a strong HS256 secret
node -e "console.log(require('crypto').randomBytes(64).toString('base64url'))"
Configuración del 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 {}
Punto final JWKS para distribución de clave 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'
},
],
};
}
}
Almacene en caché este punto final de manera agresiva: las claves públicas rara vez cambian. Establezca Cache-Control: public, max-age=3600.
Lista de verificación de seguridad
Antes de implementar la autenticación JWT en producción, verifique:
- [] Algoritmo establecido explícitamente en
RS256oHS256tanto en llamadas de firma como de verificación -
exp,iss,audvalidados en cada solicitud - Token de acceso TTL ≤ 15 minutos
- [] Tokens almacenados en cookies HttpOnly, Secure, SameSite=Lax
- [] Se implementó la rotación del token de actualización: el token antiguo se revoca en cada uso
- [] Actualizar token almacenado como hash SHA256 en la base de datos (no texto sin formato)
-
jtireclamo agregado para capacidad de revocación específica - [] Lista de denegación de Redis comprobada en busca de puntos finales de alta seguridad
- [] Punto final JWKS publicado para servicios distribuidos
- [] Claves privadas almacenadas en el administrador de secretos (AWS Secrets Manager, Vault)
- [] Procedimiento de rotación de claves documentado y probado
- [] Todos los eventos de invalidación y emisión de tokens registrados
Preguntas frecuentes
¿Es seguro decodificar un JWT sin verificar la firma?
La decodificación sin verificación es segura para leer la carga útil, pero nunca confíes en el contenido para la autorización. Verifique siempre la firma antes de actuar sobre reclamos. La función jwt.decode() en la mayoría de las bibliotecas omite la verificación; úsela solo para diagnósticos o para leer kid del encabezado antes de seleccionar la clave correcta para la verificación.
¿Debo utilizar cookies o encabezados de autorización para las aplicaciones del navegador?
Cookies HttpOnly para aplicaciones de navegador, encabezados de autorización para aplicaciones móviles nativas y llamadas API de servidor a servidor. Las cookies son inmunes a la filtración XSS (JavaScript no puede leer las cookies HttpOnly). Las aplicaciones móviles no pueden utilizar cookies de forma eficaz y utilizan tokens de portador almacenados en el almacén de claves seguro del dispositivo.
¿Cómo manejo la caducidad del token en la interfaz?
Intercepte las respuestas 401 e intente una actualización silenciosa antes de volver a intentar la solicitud original. En React, use un Axios o un interceptor de recuperación. Si la actualización también falla (caducó o revocó), redirija para iniciar sesión. Mantenga una única promesa de actualización durante el vuelo para evitar tormentas de actualización paralelas.
¿Cuál es la diferencia entre tokens de acceso y tokens de actualización?
Los tokens de acceso son de corta duración (15 minutos), no tienen estado y se verifican con una clave pública o un secreto compartido en cada solicitud de API. Los tokens de actualización son de larga duración (entre 7 y 30 días), opacos (cadenas aleatorias, no JWT) y se almacenan en el lado del servidor en la base de datos. El punto final del token de actualización es el único lugar donde se utilizan los tokens de actualización; alcancelos con una ruta de cookies estrecha.
¿Puedo almacenar roles de usuario en la carga útil de JWT?
Sí, pero tenga en cuenta que los roles codificados en el token se almacenan en caché hasta que caduque el token. Si revoca la función de administrador de un usuario, este la conservará hasta que caduque su token de acceso actual (hasta 15 minutos). Para cambios de rol de alta seguridad, agregue también el usuario a una lista de denegación de Redis o incremente su versión de token para forzar una reautenticación inmediata.
¿Cómo implemento "recordarme" con JWT?
Emita un token de actualización de mayor duración (90 días) cuando el usuario marque "recordarme" en comparación con los 30 días estándar. Almacene un indicador persistent en la fila de la base de datos del token de actualización para que pueda mostrar y revocar sesiones persistentes por separado en la configuración de seguridad del usuario. Nunca extienda el token de acceso TTL, eso frustra el propósito.
Próximos pasos
La autenticación JWT bien realizada es la base de toda aplicación web segura. Desde la firma RS256 hasta el almacenamiento de cookies HttpOnly, la rotación de tokens de actualización y las estrategias de revocación, los patrones de esta guía protegen a sus usuarios de los ataques de autenticación más comunes.
ECOSIRE implementa una arquitectura de autenticación probada en batalla, incluida la integración OIDC con Authentik, flujos de cookies HttpOnly y administración de tokens respaldada por Redis, en todos nuestros proyectos backend. Explore nuestros servicios de desarrollo centrados en la seguridad para saber cómo podemos reforzar su capa de autenticación.
Escrito por
ECOSIRE Research and Development Team
Construyendo productos digitales de nivel empresarial en ECOSIRE. Compartiendo perspectivas sobre integraciones Odoo, automatización de eCommerce y soluciones empresariales impulsadas por IA.
Artículos 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.