Pour un 401 Unauthorized avec JWT, je parcours la chaîne d’authentification dans l’ordre : transport HTTP, temps, signature, issuer, audience, type de token, puis seulement permissions.
Avec Expected Issuer/Audience connus, je commence par JWT Debugger.
1. Vérifier l’Authorization header
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Je cherche :
- header absent ;
Bearerincorrect ;- token entre guillemets ;
- reverse proxy qui retire
Authorization; - token ancien en cache ;
- ID Token envoyé à la place de l’Access Token.
Un bon JWT ne sert à rien s’il n’atteint jamais la couche de validation.
2. Lire WWW-Authenticate et les logs
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token"
Je cherche ensuite un détail plus précis :
token expired
invalid signature
invalid issuer
invalid audience
kid not found
token not active
invalid_token seul ne suffit pas à identifier la cause. Je garde aussi un request ID, la route, la méthode, l’issuer attendu et la catégorie de rejet, jamais le JWT complet.
3. Vérifier exp, nbf et l’horloge serveur
Avec JWT Expiry Checker :
now < nbf => pas encore actif
now >= exp => expiré
Je contrôle aussi seconds/milliseconds et clock skew, sur l’horloge du composant qui rejette réellement la requête.
4. Vérifier signature et algorithme
Je contrôle :
algautorisé ;- secret/public key de confiance ;
kid;- JWKS/cache ;
- key rotation ;
- octets
header.payloadoriginaux.
J’utilise JWT Signature Verifier ou JWKS Validator.
5. Comparer iss exactement
expected: https://login.example.com/prod
actual: https://login.example.com/dev
En cas de mismatch, je vérifie tenant, discovery et source de clés.
6. Vérifier aud
{ "aud": ["api://billing", "api://reporting"] }
Si Orders attend api://orders, le rejet est normal. Le guide audience mismatch couvre tableaux, environnements et ID/Access Token.
7. Distinguer 401 et 403
| Statut | Hypothèse de départ |
|---|---|
| 401 | token/identité non acceptés |
| 403 | identité acceptée mais permission insuffisante |
Cela évite de déboguer les rôles avant d’avoir une authentification valide.
8. Instrumenter la middleware sans journaliser les credentials
Avec Express, je préfère loguer l’étape du rejet :
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();
}
});
Je distingue au minimum expired, not_active, bad_signature, unknown_kid, issuer_mismatch, audience_mismatch et wrong_token_type.
9. Vérifier proxy/gateway avec les headers réellement transmis
Quand le token fonctionne en local mais pas derrière Nginx/CDN/API Gateway, je compare l’entrée et la sortie du proxy. Un exemple de configuration qui conserve le header :
proxy_set_header Authorization $http_authorization;
proxy_set_header X-Request-ID $request_id;
Je ne suppose pas que cette ligne est nécessaire partout : je vérifie le comportement réel. Je cherche aussi une double validation, un issuer différent au gateway ou une audience différente entre edge et backend.
10. Reproduire avec une requête minimale
curl -i https://api.example.com/orders \
-H 'Authorization: Bearer eyJ...'
Je garde le status, WWW-Authenticate, le request ID et l’heure UTC. Si le même token fonctionne sur un autre endpoint, je compare resource/audience/policy au lieu de régénérer le token au hasard.
Ce que je mesure en production
Sans stocker les JWT, je peux suivre :
- 401 par route ;
- 401 par catégorie de validation ;
unknown_kidpar issuer ;- expirés vs mauvaises signatures ;
- échecs/latence du refresh JWKS ;
- évolution des 401 pendant une rotation de clés.
Un pic d’unknown_kid après rotation pointe vers cache/JWKS. Un pic d’audience_mismatch après déploiement d’une nouvelle API pointe plutôt vers acquisition/configuration du token.
Tests négatifs avant déploiement
Authorization absent -> 401
Bearer vide -> 401
token expiré -> 401
nbf futur -> 401
signature modifiée -> 401
issuer incorrect -> 401
audience incorrecte -> 401
token valide sans permission -> 403 ou équivalent policy
Ils empêchent qu’un « correctif » consiste à désactiver des validations jusqu’à faire passer le happy path.
Arbre de décision
Authorization présent ?
non -> client/proxy
oui
↓
temps valide ?
non -> refresh/clock
oui
↓
signature valide avec key de confiance ?
non -> alg/kid/key/JWKS
oui
↓
iss correct ?
non -> issuer/tenant/environment
oui
↓
aud correct ?
non -> resource/token acquisition
oui
↓
token type + permissions
Mon checklist 401
- Je reproduis la requête et garde status, response headers, request ID et logs.
- Je confirme
Authorization: Bearerà l’entrée de l’API. - Je décode le token original avec JWT Decoder.
- Je vérifie
exp,nbfet server clock. - Je vérifie algorithme et signature.
- Je compare
issà la configuration. - Je compare
audà l’API attendue. - Je confirme Access Token vs ID Token.
- Je compare edge/gateway et application si le problème n’existe que derrière proxy.
- Ensuite seulement je vérifie scopes/roles/policies.
- Je corrige la couche fautive au lieu de désactiver une validation.
Le 401 devient ainsi une suite d’hypothèses vérifiables au lieu d’un message opaque.