JWT Authentication: Security Best Practices in 2026

Secure your APIs with JWT best practices: RS256 vs HS256, HttpOnly cookies, token rotation, refresh patterns, and common vulnerabilities to avoid in 2026.

E
ECOSIRE Research and Development Team
|19 de marzo de 202611 min de lectura2.5k Palabras|

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, aud y alg; nunca confíe en el algoritmo none sin 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 RS256 o HS256 tanto en llamadas de firma como de verificación
  • exp, iss, aud validados 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)
  • jti reclamo 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.

E

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.

Chatea en whatsapp