JWKS (JSON Web Key Set)

Autenticacion Security Notes Jan 6, 2025 JAVASCRIPT

Definicion

Imagina que eres un cajero de banco que necesita verificar que los cheques estan legitimamente firmados por los titulares de las cuentas. No tienes la firma de todos en archivo - en su lugar, tienes un directorio donde puedes buscar la firma oficial para cualquier numero de cuenta. Cuando llega un cheque, buscas la firma, la comparas con la del cheque, y la aceptas o rechazas. JWKS (JSON Web Key Set) es este directorio de firmas para el mundo digital - es un endpoint publico donde las aplicaciones pueden buscar las claves criptograficas necesarias para verificar que los JWTs (JSON Web Tokens) son autenticos.

Cuando inicias sesion usando “Iniciar sesion con Google” o cualquier proveedor OAuth/OpenID Connect, recibes JWTs que estan criptograficamente firmados. Pero como verifica tu aplicacion esa firma? Necesita la clave publica de Google. En lugar de codificar las claves (lo cual romperia cuando Google las rote), tu aplicacion obtiene el JWKS de Google desde un endpoint bien conocido como https://www.googleapis.com/oauth2/v3/certs. Este endpoint devuelve un objeto JSON conteniendo todas las claves publicas actuales de Google. Tu app encuentra la clave correcta (coincidiendo por kid - key ID en el header del JWT) y la usa para verificar la firma.

El “Set” en JWKS es crucial - los proveedores publican multiples claves para rotacion de claves. Google podria tener las claves “key-001” y “key-002” activas simultaneamente. Cuando rotan, anaden “key-003” y eventualmente eliminan “key-001”. Tu aplicacion no necesita saber sobre las rotaciones - simplemente obtiene el JWKS y encuentra la clave coincidente. Esta rotacion sin interrupciones es por que JWKS es el estandar para autenticacion distribuida. Cada proveedor de identidad importante (Google, Auth0, Okta, Azure AD) publica un endpoint JWKS, y cada libreria de JWT sabe como usarlo.

Ejemplo

Escenario Real 1: Verificando Tokens de Google Sign-In Tu app implementa “Iniciar sesion con Google”. Cuando los usuarios se autentican, Google envia un ID token (un JWT) a tu app. El header del JWT contiene "kid": "12345abc". Tu app obtiene el JWKS de Google desde https://www.googleapis.com/oauth2/v3/certs, encuentra la clave con el kid coincidente, y usa esa clave publica para verificar la firma del JWT. Si la verificacion tiene exito, sabes que el token genuinamente vino de Google.

Escenario Real 2: Configuracion Multi-Tenant de Auth0 Tu app SaaS usa Auth0 para autenticacion. Cada cliente (tenant) tiene su propio dominio de Auth0. Cuando llegan tokens, tu app determina el tenant del claim issuer (iss), construye la URL del JWKS (https://{tenant}.auth0.com/.well-known/jwks.json), obtiene las claves y verifica. El patron JWKS te permite soportar multiples proveedores de identidad con la misma logica de verificacion.

Escenario Real 3: Validacion de Tokens en Microservicios En una arquitectura de microservicios, cada servicio necesita validar JWT access tokens. En lugar de compartir secretos (pesadilla de seguridad), el servicio de auth publica un endpoint JWKS. Cada microservicio obtiene y cachea estas claves. Cuando el servicio de auth rota las claves, los servicios automaticamente obtienen las nuevas claves del JWKS. Cero coordinacion necesaria, cero tiempo de inactividad durante la rotacion.

Escenario Real 4: Rotacion de Claves en Produccion Tu proveedor de identidad necesita rotar claves (mejor practica de seguridad). Anaden una nueva clave al JWKS mientras mantienen la antigua. Los nuevos tokens usan la nueva clave (kid: "2024-key"), los tokens antiguos aun validan con la clave antigua (kid: "2023-key"). Despues de 24 horas, eliminan la clave antigua. Las aplicaciones con TTLs de cache de JWKS razonables manejan esto de forma transparente - sin cambios de codigo, sin interrupciones.

Analogia

El Directorio Publico de Notarios: Imagina un directorio de notarios publicos con sus patrones de sello oficiales. Cuando recibes un documento notariado, buscas el sello del notario en el directorio para verificar que es autentico. JWKS es este directorio - contiene los “sellos oficiales” (claves publicas) que los proveedores de identidad usan para firmar tokens.

El Registro de Sellos de Embajadas: Las embajadas usan sellos oficiales en documentos. Un registro central publica que patrones de sello pertenecen a que embajada. Cuando recibes un documento sellado, verificas contra el registro. JWKS es el registro de sellos para JWTs - te dice que claves publicas pertenecen a que emisores.

La Guia Telefonica de Firmas: En los viejos tiempos, los bancos mantenian tarjetas de firma en archivo. Cuando llegaba un cheque, comparaban la firma con la tarjeta. JWKS es una guia telefonica digital donde los proveedores de identidad publican sus patrones de firma (claves publicas). Cualquiera puede buscar el patron para verificar una firma.

La Autoridad Certificadora: HTTPS usa autoridades certificadoras para verificar sitios web. JWKS es como una mini-CA para JWTs - publica las claves publicas que verifican firmas de tokens. En lugar de confiar en una cadena de certificados, confias en el endpoint JWKS del emisor.

Ejemplo de Codigo


// Ejemplo de respuesta de endpoint JWKS
// GET https://oauth.provider.com/.well-known/jwks.json
{
  "keys": [
    {
      "kty": "RSA",                    // Tipo de clave
      "kid": "key-2024-001",           // Key ID (coincide con header del JWT)
      "use": "sig",                    // Uso de la clave: firma
      "alg": "RS256",                  // Algoritmo
      "n": "0vx7agoebGcQSuuPiLJXZpt...N4IOJnoEhw",  // Modulo RSA
      "e": "AQAB"                       // Exponente RSA
    },
    {
      "kty": "RSA",
      "kid": "key-2024-002",           // Segunda clave para rotacion
      "use": "sig",
      "alg": "RS256",
      "n": "xK94kVtxLhP...gqI7Y",
      "e": "AQAB"
    }
  ]
}

// Obtener y usar JWKS para verificar JWT
const jwksClient = require('jwks-rsa');
const jwt = require('jsonwebtoken');

// Crear cliente con cache
const client = jwksClient({
  jwksUri: 'https://oauth.provider.com/.well-known/jwks.json',
  cache: true,                    // Cachear claves
  cacheMaxAge: 600000,            // Cache de 10 minutos
  rateLimit: true,                // Prevenir DoS en endpoint JWKS
  jwksRequestsPerMinute: 10
});

// Funcion para obtener clave de firma por kid
function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    if (err) {
      console.error('Clave no encontrada:', header.kid);
      return callback(err);
    }
    const signingKey = key.getPublicKey();
    callback(null, signingKey);
  });
}

// Verificar token usando JWKS
async function verifyToken(token) {
  return new Promise((resolve, reject) => {
    jwt.verify(token, getKey, {
      algorithms: ['RS256'],
      audience: 'tu-client-id',
      issuer: 'https://oauth.provider.com'
    }, (err, decoded) => {
      if (err) {
        return reject(new Error(`Verificacion de token fallida: ${err.message}`));
      }
      resolve(decoded);
    });
  });
}

// Middleware para Express
const verifyJWT = async (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Token faltante' });
  }

  const token = authHeader.substring(7);

  try {
    req.user = await verifyToken(token);
    next();
  } catch (error) {
    res.status(401).json({ error: 'Token invalido' });
  }
};

// Discovery: Encontrar URL de JWKS desde Configuracion OpenID
// GET https://oauth.provider.com/.well-known/openid-configuration
{
  "issuer": "https://oauth.provider.com",
  "jwks_uri": "https://oauth.provider.com/.well-known/jwks.json",
  "authorization_endpoint": "...",
  "token_endpoint": "..."
}

Diagrama

sequenceDiagram
    participant App as Tu App
    participant IDP as Proveedor de Identidad
    participant JWKS as Endpoint JWKS

    Note over App: Recibe JWT con kid en header

    App->>App: 1. Decodificar header del JWT
kid: "key-2024-001" App->>JWKS: 2. GET /.well-known/jwks.json JWKS->>App: 3. Devolver set de claves Note over App: {
"keys": [
{ "kid": "key-2024-001", "n": "...", "e": "..." },
{ "kid": "key-2024-002", "n": "...", "e": "..." }
]
} App->>App: 4. Encontrar clave que coincide con kid App->>App: 5. Verificar firma JWT con clave publica alt Firma Valida App->>App: 6. Aceptar token, extraer claims else Firma Invalida App->>App: 6. Rechazar token end Note over JWKS: Las claves rotan periodicamente.
Multiples claves permiten rotacion sin interrupciones.

Notas de Seguridad

SECURITY NOTES
CRITICO - Siempre obtener JWKS sobre HTTPS solo de emisores de confianza. Verificar que la URL del endpoint JWKS coincide exactamente con el claim iss en el JWT. Implementar cache con TTL razonable (10-60 minutos) para evitar DoS en el endpoint JWKS y mejorar rendimiento. Validar que kid (key ID) existe en el set JWK antes de usar. Comprobar que el algoritmo de la clave (alg) coincide con el valor esperado - nunca confiar solo en el alg del header del JWT. Implementar soporte de rotacion de claves (manejar multiples claves en el set). Establecer logica de fallback/retry para fallos del endpoint JWKS. Validar que el uso de la clave (claim use) coincide con la intencion (sig para firma). Considerar key pinning local para sistemas criticos. Monitorear cambios de clave inesperados. Implementar rate limiting en peticiones al endpoint JWKS. Nunca cachear claves indefinidamente - respetar la rotacion.

Estandares y RFCs