Ante 401 Unauthorized con JWT no empiezo por scopes ni por editar claims. Recorro la cadena de autenticación desde fuera hacia dentro: HTTP header, tiempo, firma, issuer, audience, token type y solo después autorización.
Con Expected Issuer/Audience conocidos empiezo en JWT Debugger.
1. Confirmo el Authorization header
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Compruebo:
- header ausente;
Bearerincorrecto;- token entre comillas;
- reverse proxy elimina
Authorization; - cliente reusa un token viejo;
- ID Token enviado como Access Token.
Un JWT perfecto no sirve si nunca llega a la middleware que lo valida.
2. Leo WWW-Authenticate y logs
RFC 6750 define respuestas Bearer como:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token"
Busco detalle adicional:
token expired
invalid signature
invalid issuer
invalid audience
kid not found
token not active
invalid_token solo no identifica la causa. Registro también request ID, ruta, método, issuer esperado, audience esperada y categoría del fallo, pero nunca el JWT completo ni secretos.
3. Reviso exp, nbf y reloj del servidor
JWT Expiry Checker me permite separar:
now < nbf => todavía no activo
now >= exp => expirado
También descarto seconds/milliseconds y clock skew. Comparo siempre con el reloj del servidor que realmente rechaza la petición, no con el del navegador.
4. Verifico firma y algoritmo
Decodificar no autentica. Compruebo:
- algoritmo permitido;
- secret/public key correcto;
kid;- JWKS y cache;
- key rotation;
- bytes originales
header.payload.
Uso JWT Signature Verifier o JWKS Validator.
5. Comparo iss exactamente
expected: https://login.example.com/prod
actual: https://login.example.com/dev
Si iss cambia, reviso también tenant, discovery y la fuente de claves. Una firma con una clave de otro entorno no convierte ese token en válido para producción.
6. Valido aud para la API concreta
{ "aud": ["api://billing", "api://reporting"] }
Si Orders espera api://orders, rechazo. La guía de audience mismatch cubre arrays, environments y confusión entre ID/Access Token.
7. Distingo 401 de 403
| Estado | Diagnóstico inicial |
|---|---|
| 401 | token/identidad no aceptados |
| 403 | identidad aceptada, permiso insuficiente |
La implementación concreta puede variar, pero esta separación evita perder tiempo buscando roles cuando firma o audience ni siquiera pasan.
8. Instrumento la middleware para saber dónde falla
En Node/Express prefiero registrar la etapa, no el token:
app.use(async (req, res, next) => {
const auth = req.get("authorization");
if (!auth?.startsWith("Bearer ")) {
console.warn("auth_fail", { stage: "missing_bearer", requestId: req.id });
return res.status(401).end();
}
try {
req.auth = await verifyAccessToken(auth.slice(7));
next();
} catch (error) {
console.warn("auth_fail", {
stage: classifyJwtError(error),
requestId: req.id,
});
res.status(401).end();
}
});
Categorías útiles: missing_bearer, expired, not_active, bad_signature, unknown_kid, issuer_mismatch, audience_mismatch y wrong_token_type. Con eso puedo medir qué clase de 401 domina sin exponer credenciales.
9. Compruebo gateways y proxies con una comparación de headers
Si funciona localmente pero falla detrás de Nginx, CDN o API Gateway, comparo el request antes y después del proxy. Un ejemplo de configuración que preserva el header es:
proxy_set_header Authorization $http_authorization;
proxy_set_header X-Request-ID $request_id;
No asumo que todos los productos necesiten esta línea exacta; compruebo el comportamiento real del gateway. También descarto reglas que sobrescriben Authorization, validadores duplicados o audiences distintas entre edge y aplicación.
10. Reproduzco el fallo con curl
Un caso mínimo elimina ruido del frontend:
curl -i https://api.example.com/orders \
-H 'Authorization: Bearer eyJ...'
Guardo status, WWW-Authenticate, request ID y hora UTC. Si el mismo token funciona contra otro endpoint, comparo audience, gateway y policy del recurso en vez de volver a emitir tokens al azar.
Qué métricas me ayudan en producción
No necesito almacenar JWTs para observar autenticación. Suelo contar:
- 401 por ruta;
- 401 por categoría de validación;
unknown_kidpor issuer;- expirados vs firma inválida;
- latencia/fallos al refrescar JWKS;
- 401 antes y después de una rotación de claves.
Un pico de unknown_kid tras una rotación apunta a cache/JWKS. Un pico de audience_mismatch tras desplegar una API nueva apunta a adquisición/configuración del token.
Casos negativos que pruebo antes de desplegar
sin Authorization -> 401
Bearer vacío -> 401
token expirado -> 401
token nbf futuro -> 401
firma manipulada -> 401
issuer incorrecto -> 401
audience incorrecta -> 401
access token válido sin permiso -> 403 o policy equivalente
Estos tests protegen contra el peor “arreglo”: relajar validaciones hasta que el happy path pase.
Árbol de decisión
Authorization presente?
no -> cliente/proxy
sí
↓
tiempo válido?
no -> refresh/clock
sí
↓
firma válida con key confiable?
no -> alg/kid/key/JWKS
sí
↓
iss correcto?
no -> issuer/tenant/environment
sí
↓
aud correcto?
no -> resource/token acquisition
sí
↓
token type + permissions
Mi checklist de JWT 401
- Reproduzco request y guardo status, response headers, request ID y logs.
- Confirmo
Authorization: Beareral entrar en la API. - Decodifico el token original con JWT Decoder.
- Compruebo
exp,nbfy server clock. - Verifico algoritmo y firma con key confiable.
- Comparo
isscon la configuración. - Comparo
audcon la API esperada. - Confirmo que envío Access Token, no ID Token.
- Comparo edge/gateway y aplicación si el fallo solo aparece detrás del proxy.
- Solo después reviso scopes/roles/policies.
- Corrijo la capa que falla en vez de desactivar validaciones.
Así convierto un 401 genérico en una secuencia reproducible de hipótesis comprobables.