RFC 9068 - JWT Profile for OAuth 2.0 Access Tokens
Informations de base
- Numéro RFC: 9068
- Titre: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens
- Titre français: Profil JWT pour les jetons d'accès OAuth 2.0
- Date de publication: Octobre 2021
- Statut: PROPOSED STANDARD (Norme proposée)
- Auteurs: V. Bertocci (Auth0), B. Campbell (Ping Identity)
Résumé (Abstract)
Cette spécification définit un profil standard pour l'utilisation des jetons d'accès OAuth 2.0 au format JWT. Elle spécifie les revendications requises, les revendications recommandées et les exigences de validation pour les jetons d'accès JWT, permettant aux serveurs de ressources de valider et d'analyser directement les jetons d'accès sans avoir à appeler un point de terminaison d'introspection à chaque fois.
Aperçu des jetons d'accès JWT
Pourquoi avons-nous besoin de jetons d'accès JWT ?
Problèmes des jetons d'accès traditionnels:
Jeton opaque (Opaque Token):
- Format: Chaîne aléatoire (ex.: "SlAV32hkKG...XPw")
- Caractéristique: Ne peut pas être analysé directement
- Validation: Doit appeler le point d'introspection (RFC 7662)
Problèmes:
❌ Chaque appel API nécessite une introspection → requêtes réseau supplémentaires
❌ Charge élevée sur le serveur d'autorisation → goulot d'étranglement des performances
❌ Latence accrue → mauvaise expérience utilisateur
❌ Point de défaillance unique → panne du serveur d'autorisation affecte toutes les API
Flux d'exemple:
Client → [access_token] → Serveur de ressources
↓
Point d'introspection ← Serveur d'autorisation
↓
Réponse (active: true)
↓
Validation réussie
Avantages des jetons d'accès JWT:
Jeton JWT:
- Format: JSON Web Token auto-contenu
- Caractéristique: Peut être validé et analysé directement
- Validation: Vérification de signature avec clé publique
Avantages:
✓ Pas d'introspection nécessaire → zéro requête supplémentaire
✓ Charge réduite sur le serveur d'autorisation → haute performance
✓ Faible latence → réponse rapide
✓ Validation hors ligne → panne du serveur d'autorisation n'affecte pas l'API
✓ Contient des informations contextuelles → scope, ID utilisateur, etc.
Flux d'exemple:
Client → [JWT access_token] → Serveur de ressources
↓
1. Vérifier la signature (avec clé publique)
2. Vérifier l'expiration
3. Vérifier l'audience
↓
Validation réussie
↓
Lire scope/ID utilisateur depuis JWT
Jetons d'accès JWT vs jetons opaques
Comparaison:
Jeton opaque (Opaque Token):
Avantages:
+ Peut être révoqué à tout moment (contrôle côté serveur)
+ Petit jeton (généralement 20-40 caractères)
+ Ne divulgue pas d'informations (non analysable)
Inconvénients:
- Doit être validé par introspection (coût réseau)
- Pression élevée sur le serveur d'autorisation
- Pas de validation hors ligne
Cas d'utilisation:
- Scénarios nécessitant une révocation immédiate
- Exigences de haute sécurité (ne souhaite pas que le jeton soit analysable)
- Jetons à court terme
Jeton d'accès JWT:
Avantages:
+ Auto-contenu (pas d'introspection nécessaire)
+ Haute performance (validation hors ligne)
+ Contient le contexte (scope, claims)
+ Extensible
Inconvénients:
- Grand jeton (généralement 100-300+ caractères)
- Difficile à révoquer (nécessite des mécanismes supplémentaires)
- Informations visibles (décodage base64 lisible)
Cas d'utilisation:
- API haute performance
- Architecture microservices
- Systèmes distribués
- Jetons doivent contenir des informations contextuelles
Solution hybride:
- Jetons d'accès JWT à court terme (ex. 15 minutes)
- Jetons de rafraîchissement opaques à long terme
→ Combine les avantages des deux approches
Format des jetons d'accès JWT
Revendications requises (Required Claims)
{
"iss": "https://authorization-server.example.com",
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
"iat": 1639530000,
"jti": "token-789"
}
1. iss (Issuer - Émetteur):
"iss": "https://authorization-server.example.com"
Description:
- Identifiant du serveur d'autorisation
- Doit être une URL HTTPS
- Utilisé pour trouver la clé de vérification
Validation:
const payload = jwt.decode(token);
if (payload.iss !== expectedIssuer) {
throw new Error('Invalid issuer');
}
2. exp (Expiration Time - Temps d'expiration):
"exp": 1639533600 // Horodatage Unix
Description:
- Temps d'expiration du jeton
- Horodatage Unix (secondes)
- Doit être un temps futur
Validation:
const now = Math.floor(Date.now() / 1000);
if (payload.exp <= now) {
throw new Error('Token expired');
}
Durée de validité recommandée:
- Court terme: 5-15 minutes (haute sécurité)
- Moyen terme: 1 heure (équilibré)
- Long terme: 24 heures (faible sécurité, non recommandé)
3. aud (Audience - Public):
"aud": "https://api.example.com"
ou
"aud": ["https://api.example.com", "https://api2.example.com"]
Description:
- Serveur de ressources cible du jeton
- Peut être une chaîne ou un tableau de chaînes
- Empêche l'utilisation du jeton pour des ressources non prévues
Validation:
const expectedAudience = 'https://api.example.com';
if (Array.isArray(payload.aud)) {
if (!payload.aud.includes(expectedAudience)) {
throw new Error('Invalid audience');
}
} else {
if (payload.aud !== expectedAudience) {
throw new Error('Invalid audience');
}
}
4. sub (Subject - Sujet):
"sub": "user-123"
Description:
- Identifiant du sujet du jeton
- Généralement l'ID utilisateur
- Doit être unique dans le périmètre de l'émetteur
- Type chaîne
Utilisation:
// Obtenir l'ID utilisateur depuis le jeton
const userId = payload.sub;
const user = await getUserById(userId);
5. client_id (Client Identifier - Identifiant client):
"client_id": "client-456"
Description:
- ID du client demandant le jeton
- Utilisé pour le suivi et l'audit
- Distingue les requêtes de différents clients
Utilisation:
// Vérifier les autorisations du client
const client = await getClient(payload.client_id);
if (!client.hasPermission('write')) {
throw new Error('Client not authorized');
}
6. iat (Issued At - Émis à):
"iat": 1639530000 // Horodatage Unix
Description:
- Temps d'émission du jeton
- Horodatage Unix (secondes)
- Utilisé pour prévenir les attaques par rejeu
Validation:
const now = Math.floor(Date.now() / 1000);
const maxAge = 3600; // 1 heure
if (now - payload.iat > maxAge) {
throw new Error('Token too old');
}
7. jti (JWT ID - Identifiant JWT):
"jti": "token-789"
Description:
- Identifiant unique du jeton
- Utilisé pour prévenir les attaques par rejeu
- Peut être utilisé pour révoquer des jetons
Utilisation:
// Vérifier si le jeton a été révoqué
const isRevoked = await checkRevokedToken(payload.jti);
if (isRevoked) {
throw new Error('Token has been revoked');
}
Revendications optionnelles (Optional Claims)
8. scope (Scope - Portée):
"scope": "read write profile"
Description:
- Portée d'autorisation
- Chaîne séparée par des espaces
- Le serveur de ressources effectue le contrôle d'autorisation en conséquence
Utilisation:
const scopes = payload.scope.split(' ');
if (!scopes.includes('write')) {
return res.status(403).json({ error: 'Insufficient scope' });
}
9. Revendications personnalisées:
{
"iss": "https://authorization-server.example.com",
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
// Revendications personnalisées
"email": "[email protected]",
"name": "John Doe",
"roles": ["admin", "editor"],
"tenant_id": "tenant-789",
"permissions": ["read:articles", "write:articles"]
}
Description:
- Peut ajouter des revendications personnalisées arbitraires
- Utilisé pour transmettre des informations contextuelles
- Réduit les requêtes de base de données
Remarque:
⚠️ Limitation de taille du jeton (généralement < 8KB)
⚠️ Ne pas inclure d'informations sensibles (jeton peut être décodé)
⚠️ Considérer les coûts de transmission réseau
Exemple JWT complet
Jeton d'accès JWT complet:
eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJodHRwczovL2F1dGhvcml6YXRpb24tc2VydmVyLmV4YW1wbGUuY29tIiwiZXhwIjoxNjM5NTMzNjAwLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsInN1YiI6InVzZXItMTIzIiwiY2xpZW50X2lkIjoiY2xpZW50LTQ1NiIsImlhdCI6MTYzOTUzMDAwMCwianRpIjoidG9rZW4tNzg5Iiwic2NvcGUiOiJyZWFkIHdyaXRlIn0.signature
Décomposition:
Header (En-tête):
{
"alg": "RS256",
"typ": "at+jwt", ← Type: JWT de jeton d'accès
"kid": "123"
}
Payload (Charge utile):
{
"iss": "https://authorization-server.example.com",
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
"iat": 1639530000,
"jti": "token-789",
"scope": "read write"
}
Signature:
RSASSA-PKCS1-v1_5 using SHA-256
Signature et validation
Algorithmes recommandés
RFC 9068 recommande:
Recommandation prioritaire:
1. RS256 (RSA avec SHA-256)
- Chiffrement asymétrique
- Validation avec clé publique
- Le plus couramment utilisé
2. ES256 (ECDSA avec P-256 et SHA-256)
- Chiffrement asymétrique
- Signature plus courte
- Meilleures performances
Non recommandé:
❌ HS256 (HMAC avec SHA-256)
- Chiffrement symétrique
- Nécessite une clé partagée
- Sécurité moindre (tous les serveurs de ressources nécessitent la clé)
Pourquoi les algorithmes asymétriques sont-ils recommandés ?
→ Le serveur d'autorisation signe avec la clé privée
→ Le serveur de ressources valide avec la clé publique
→ La clé publique peut être distribuée publiquement
→ Pas de clé partagée nécessaire
Génération JWT (Serveur d'autorisation)
const jose = require('jose');
class JWTAccessTokenIssuer {
constructor(privateKey, issuer, keyId) {
this.privateKey = privateKey;
this.issuer = issuer;
this.keyId = keyId;
}
async issueAccessToken(userId, clientId, audience, scope, expiresIn = 900) {
const now = Math.floor(Date.now() / 1000);
const payload = {
// Revendications requises
iss: this.issuer,
exp: now + expiresIn, // Par défaut 15 minutes
aud: audience,
sub: userId,
client_id: clientId,
iat: now,
jti: this.generateJti(),
// Revendications optionnelles
scope: scope
};
// Générer JWT
const jwt = await new jose.SignJWT(payload)
.setProtectedHeader({
alg: 'RS256',
typ: 'at+jwt', // Important: Identifier comme jeton d'accès
kid: this.keyId
})
.sign(this.privateKey);
return jwt;
}
generateJti() {
return require('crypto').randomBytes(16).toString('hex');
}
}
// Exemple d'utilisation
const { generateKeyPair } = require('jose');
const { privateKey, publicKey } = await generateKeyPair('RS256');
const issuer = new JWTAccessTokenIssuer(
privateKey,
'https://authorization-server.example.com',
'key-123'
);
const accessToken = await issuer.issueAccessToken(
'user-123', // userId
'client-456', // clientId
'https://api.example.com', // audience
'read write', // scope
900 // 15 minutes
);
console.log('Access Token:', accessToken);
Validation JWT (Serveur de ressources)
const jose = require('jose');
class JWTAccessTokenValidator {
constructor(issuer, audience) {
this.issuer = issuer;
this.audience = audience;
this.jwksCache = null;
this.jwksCacheTime = 0;
}
async validate(token) {
try {
// Étape 1: Obtenir JWKS (ensemble de clés publiques)
const jwks = await this.getJWKS();
// Étape 2: Valider JWT
const { payload, protectedHeader } = await jose.jwtVerify(
token,
jwks,
{
issuer: this.issuer,
audience: this.audience,
typ: 'at+jwt' // Confirmer le type
}
);
// Étape 3: Validation supplémentaire
this.validatePayload(payload);
return {
valid: true,
payload,
header: protectedHeader
};
} catch (err) {
return {
valid: false,
error: err.message
};
}
}
async getJWKS() {
// Mettre en cache JWKS pendant 1 heure
const now = Date.now();
if (this.jwksCache && (now - this.jwksCacheTime < 3600000)) {
return this.jwksCache;
}
// Obtenir JWKS depuis le serveur d'autorisation
const jwksUrl = `${this.issuer}/.well-known/jwks.json`;
const jwks = jose.createRemoteJWKSet(new URL(jwksUrl));
this.jwksCache = jwks;
this.jwksCacheTime = now;
return jwks;
}
validatePayload(payload) {
// Vérifier les revendications requises
const required = ['iss', 'exp', 'aud', 'sub', 'client_id', 'iat', 'jti'];
for (const claim of required) {
if (!payload[claim]) {
throw new Error(`Missing required claim: ${claim}`);
}
}
// Vérifier le format client_id
if (typeof payload.client_id !== 'string') {
throw new Error('client_id must be a string');
}
// D'autres validations personnalisées peuvent être ajoutées...
}
// Extraire les scopes
getScopes(payload) {
if (!payload.scope) return [];
return payload.scope.split(' ');
}
// Vérifier l'autorisation
hasScope(payload, requiredScope) {
const scopes = this.getScopes(payload);
return scopes.includes(requiredScope);
}
}
// Exemple d'utilisation
const validator = new JWTAccessTokenValidator(
'https://authorization-server.example.com',
'https://api.example.com'
);
// Valider le jeton d'accès
const result = await validator.validate(accessToken);
if (result.valid) {
console.log('Token is valid');
console.log('User ID:', result.payload.sub);
console.log('Client ID:', result.payload.client_id);
console.log('Scopes:', result.payload.scope);
} else {
console.error('Token is invalid:', result.error);
}
Implémentation de middleware Express
Middleware de validation JWT
const express = require('express');
const jose = require('jose');
// Middleware de validation JWT
function jwtAuthMiddleware(options = {}) {
const {
issuer,
audience,
requiredScopes = []
} = options;
const validator = new JWTAccessTokenValidator(issuer, audience);
return async (req, res, next) => {
try {
// Étape 1: Extraire le jeton
const token = extractToken(req);
if (!token) {
return res.status(401).json({
error: 'invalid_request',
error_description: 'Missing access token'
});
}
// Étape 2: Valider le jeton
const result = await validator.validate(token);
if (!result.valid) {
return res.status(401).json({
error: 'invalid_token',
error_description: result.error
});
}
// Étape 3: Vérifier les scopes
if (requiredScopes.length > 0) {
const hasAllScopes = requiredScopes.every(scope =>
validator.hasScope(result.payload, scope)
);
if (!hasAllScopes) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scopes: ${requiredScopes.join(', ')}`
});
}
}
// Étape 4: Attacher à l'objet de requête
req.auth = {
token: result.payload,
userId: result.payload.sub,
clientId: result.payload.client_id,
scopes: validator.getScopes(result.payload)
};
next();
} catch (err) {
console.error('Auth middleware error:', err);
res.status(500).json({
error: 'server_error',
error_description: 'Internal server error'
});
}
};
}
// Fonction d'aide pour extraire le jeton
function extractToken(req) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return null;
}
const parts = authHeader.split(' ');
if (parts.length !== 2 || parts[0] !== 'Bearer') {
return null;
}
return parts[1];
}
// Exemple d'utilisation
const app = express();
// Configurer la validation JWT
const jwtAuth = jwtAuthMiddleware({
issuer: 'https://authorization-server.example.com',
audience: 'https://api.example.com'
});
// Point de terminaison public (pas d'authentification requise)
app.get('/api/public', (req, res) => {
res.json({ message: 'Public endpoint' });
});
// Point de terminaison protégé (authentification requise)
app.get('/api/protected', jwtAuth, (req, res) => {
res.json({
message: 'Protected endpoint',
user: req.auth.userId,
client: req.auth.clientId
});
});
// Point de terminaison nécessitant des scopes spécifiques
app.post('/api/articles',
jwtAuthMiddleware({
issuer: 'https://authorization-server.example.com',
audience: 'https://api.example.com',
requiredScopes: ['write', 'articles']
}),
(req, res) => {
res.json({
message: 'Article created',
author: req.auth.userId
});
}
);
// Vérification dynamique des scopes
app.delete('/api/articles/:id', jwtAuth, async (req, res) => {
const article = await getArticle(req.params.id);
// Vérifier l'autorisation: administrateur ou auteur
const isAdmin = req.auth.scopes.includes('admin');
const isAuthor = article.authorId === req.auth.userId;
if (!isAdmin && !isAuthor) {
return res.status(403).json({
error: 'insufficient_permissions',
error_description: 'You can only delete your own articles'
});
}
await deleteArticle(req.params.id);
res.json({ message: 'Article deleted' });
});
app.listen(3000, () => {
console.log('API server running on http://localhost:3000');
});
Décorateur de validation de scope
// Validation avancée des scopes
class ScopeValidator {
// Nécessite tous les scopes (logique ET)
static requireAll(...scopes) {
return (req, res, next) => {
const hasAll = scopes.every(scope =>
req.auth.scopes.includes(scope)
);
if (!hasAll) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scopes: ${scopes.join(' AND ')}`
});
}
next();
};
}
// Nécessite un scope quelconque (logique OU)
static requireAny(...scopes) {
return (req, res, next) => {
const hasAny = scopes.some(scope =>
req.auth.scopes.includes(scope)
);
if (!hasAny) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required scopes: ${scopes.join(' OR ')}`
});
}
next();
};
}
// Expression complexe
static requireExpression(expression) {
return (req, res, next) => {
const scopes = req.auth.scopes;
// Exemple: "(read AND write) OR admin"
const result = evaluateExpression(expression, scopes);
if (!result) {
return res.status(403).json({
error: 'insufficient_scope',
error_description: `Required: ${expression}`
});
}
next();
};
}
}
// Utilisation
app.get('/api/data',
jwtAuth,
ScopeValidator.requireAll('read', 'data'),
(req, res) => {
// Nécessite read ET data
}
);
app.post('/api/admin/users',
jwtAuth,
ScopeValidator.requireAny('admin', 'super_admin'),
(req, res) => {
// Nécessite admin OU super_admin
}
);
Stratégies de révocation de jetons
Problème et solutions
Dilemme de révocation JWT:
Problème:
JWT est auto-contenu, le serveur de ressources n'interroge pas le serveur d'autorisation
→ Impossible de révoquer immédiatement le jeton
Scénarios:
1. Utilisateur se déconnecte
2. Mot de passe modifié
3. Autorisations révoquées
4. Incident de sécurité
Solutions:
Solution 1: Jetons à court terme
// Émettre des jetons d'accès à court terme (5-15 minutes)
const accessToken = await issuer.issueAccessToken(
userId,
clientId,
audience,
scope,
900 // 15 minutes
);
Avantages:
✓ Implémentation simple
✓ Expiration automatique rapide
✓ Petite fenêtre de révocation
Inconvénients:
- Nécessite un rafraîchissement fréquent
- Expérience utilisateur affectée
Solution 2: Liste de révocation (Revocation List)
class TokenRevocationList {
constructor() {
this.revokedTokens = new Set();
// L'environnement de production devrait utiliser Redis ou un cache distribué similaire
}
// Révoquer le jeton
revoke(jti, expiresAt) {
this.revokedTokens.add(jti);
// Définir le temps d'expiration (nettoyage automatique après expiration du jeton)
setTimeout(() => {
this.revokedTokens.delete(jti);
}, (expiresAt * 1000) - Date.now());
}
// Vérifier si révoqué
isRevoked(jti) {
return this.revokedTokens.has(jti);
}
}
const revocationList = new TokenRevocationList();
// Vérifier dans le middleware de validation
async function checkRevocation(req, res, next) {
const jti = req.auth.token.jti;
if (revocationList.isRevoked(jti)) {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Token has been revoked'
});
}
next();
}
// Utilisation
app.use('/api', jwtAuth, checkRevocation);
Solution 3: Numéro de version/horodatage
// Ajouter un champ à la table utilisateur
{
userId: '123',
tokenVersion: 5, // Incrémenter à chaque déconnexion/changement de mot de passe
// ou
tokensInvalidBefore: 1639530000 // Jetons avant ce temps invalides
}
// Inclure le numéro de version lors de la génération du jeton
const accessToken = await issuer.issueAccessToken(
userId,
clientId,
audience,
scope,
900,
{ token_version: user.tokenVersion } // Revendication personnalisée
);
// Vérifier lors de la validation
async function checkTokenVersion(req, res, next) {
const userId = req.auth.userId;
const tokenVersion = req.auth.token.token_version;
const user = await getUserById(userId);
if (tokenVersion < user.tokenVersion) {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Token version outdated'
});
}
next();
}
Solution 4: Solution hybride (Recommandée)
// Combiner plusieurs stratégies
class HybridRevocationStrategy {
constructor() {
this.shortTermRevocations = new Set(); // Révocation récente (Redis)
this.userVersions = new Map(); // Version du jeton utilisateur (base de données)
}
async isValid(payload) {
// Vérification 1: JTI dans la liste de révocation
if (this.shortTermRevocations.has(payload.jti)) {
return false;
}
// Vérification 2: Version du jeton
const userVersion = await this.getUserTokenVersion(payload.sub);
if (payload.token_version < userVersion) {
return false;
}
// Vérification 3: Horodatage
const invalidBefore = await this.getUserTokensInvalidBefore(payload.sub);
if (payload.iat < invalidBefore) {
return false;
}
return true;
}
async revokeToken(jti, expiresAt) {
// Révocation à court terme (jusqu'à l'expiration du jeton)
this.shortTermRevocations.add(jti);
setTimeout(() => {
this.shortTermRevocations.delete(jti);
}, (expiresAt * 1000) - Date.now());
}
async revokeAllUserTokens(userId) {
// Incrémenter la version du jeton utilisateur
const currentVersion = await this.getUserTokenVersion(userId);
await this.setUserTokenVersion(userId, currentVersion + 1);
}
}
Optimisation des performances
Mise en cache JWKS
class JWKSCache {
constructor(jwksUrl, cacheDuration = 3600000) {
this.jwksUrl = jwksUrl;
this.cacheDuration = cacheDuration;
this.cache = null;
this.cacheTime = 0;
}
async getJWKS() {
const now = Date.now();
// Vérifier le cache
if (this.cache && (now - this.cacheTime < this.cacheDuration)) {
return this.cache;
}
// Obtenir nouveau JWKS
try {
const response = await fetch(this.jwksUrl);
const jwks = await response.json();
this.cache = jwks;
this.cacheTime = now;
return jwks;
} catch (err) {
// En cas d'échec de récupération mais avec ancien cache, continuer à l'utiliser
if (this.cache) {
console.warn('JWKS fetch failed, using cached version');
return this.cache;
}
throw err;
}
}
invalidate() {
this.cache = null;
this.cacheTime = 0;
}
}
Mise en cache des résultats de validation
class TokenValidationCache {
constructor(cacheDuration = 60000) { // 1 minute
this.cache = new Map();
this.cacheDuration = cacheDuration;
}
get(token) {
const entry = this.cache.get(token);
if (!entry) {
return null;
}
if (Date.now() > entry.expiresAt) {
this.cache.delete(token);
return null;
}
return entry.result;
}
set(token, result) {
this.cache.set(token, {
result,
expiresAt: Date.now() + this.cacheDuration
});
// Nettoyage régulier
this.cleanup();
}
cleanup() {
const now = Date.now();
for (const [token, entry] of this.cache.entries()) {
if (now > entry.expiresAt) {
this.cache.delete(token);
}
}
}
}
// Utilisation
const validationCache = new TokenValidationCache();
async function validateWithCache(token, validator) {
// Vérifier le cache
let result = validationCache.get(token);
if (!result) {
// Valider le jeton
result = await validator.validate(token);
// Mettre en cache uniquement les jetons valides
if (result.valid) {
validationCache.set(token, result);
}
}
return result;
}
Meilleures pratiques
1. Optimisation de la taille du jeton
// ❌ Mauvais: Jeton trop grand
{
"iss": "https://authorization-server.example.com",
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
"iat": 1639530000,
"jti": "token-789",
"scope": "read write",
// Trop de revendications personnalisées
"email": "[email protected]",
"full_name": "John Robert Smith Junior",
"profile_picture": "https://cdn.example.com/users/123/profile/large.jpg",
"address": {
"street": "123 Main Street",
"city": "New York",
"state": "NY",
"zip": "10001",
"country": "USA"
},
"preferences": { /* beaucoup de données */ }
}
// Résultat: Jeton > 2KB
// ✓ Bon: Jeton compact
{
"iss": "https://auth.example.com", // Domaine court
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
"iat": 1639530000,
"jti": "token-789",
"scope": "read write",
// Inclure uniquement les revendications personnalisées nécessaires
"roles": ["admin"]
}
// Résultat: Jeton < 500 octets
Stratégie:
1. Utiliser une URL d'émetteur courte
2. Minimiser les revendications personnalisées
3. Stocker les grandes données côté serveur, inclure uniquement la référence dans le jeton
4. Utiliser des rôles plutôt que des autorisations détaillées
2. Sécurité
// ✓ Configuration de sécurité correcte
const securityConfig = {
// Jetons à court terme
accessTokenTTL: 900, // 15 minutes
// Utiliser un algorithme asymétrique
algorithm: 'RS256',
// En-tête typ
tokenType: 'at+jwt',
// Audience spécifique
audience: 'https://api.example.com', // Ne pas utiliser de caractères génériques
// Forcer HTTPS
requireHTTPS: true,
// Tolérance de décalage d'horloge
clockTolerance: 60 // 1 minute
};
// ❌ Configuration non sécurisée
const insecureConfig = {
accessTokenTTL: 86400, // 24 heures - trop long!
algorithm: 'HS256', // Algorithme symétrique - non recommandé!
tokenType: 'JWT', // Pas spécifique
audience: '*', // Caractère générique - dangereux!
requireHTTPS: false // HTTP - non sécurisé!
};
3. Gestion des erreurs
// Gestion complète des erreurs
class JWTErrorHandler {
static handle(err, req, res, next) {
if (err.name === 'JWTExpired') {
return res.status(401).json({
error: 'invalid_token',
error_description: 'The access token expired',
error_uri: 'https://docs.example.com/errors/token-expired'
});
}
if (err.name === 'JWTClaimValidationFailed') {
return res.status(401).json({
error: 'invalid_token',
error_description: `Token validation failed: ${err.claim}`,
error_uri: 'https://docs.example.com/errors/invalid-token'
});
}
if (err.name === 'JWSSignatureVerificationFailed') {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Token signature verification failed'
});
}
// Erreur générale
console.error('JWT error:', err);
res.status(401).json({
error: 'invalid_token',
error_description: 'Token validation failed'
});
}
}
app.use(JWTErrorHandler.handle);
Résumé
Points clés
✓ Les jetons d'accès JWT sont auto-contenus
✓ Pas d'introspection nécessaire → haute performance
✓ Validation hors ligne → évolutif
✓ Contient le contexte → réduit les requêtes de base de données
✓ Format standardisé → interopérabilité
Revendications requises:
- iss, exp, aud, sub, client_id, iat, jti
Pratiques recommandées:
✓ Utiliser les algorithmes RS256 ou ES256
✓ Jetons à court terme (5-15 minutes)
✓ Définir l'en-tête typ sur "at+jwt"
✓ Audience spécifique
✓ Minimiser la taille du jeton
✓ Implémenter une stratégie de révocation
Optimisation des performances:
✓ Mise en cache JWKS
✓ Mise en cache des résultats de validation
✓ Validation asynchrone
Considérations de sécurité:
✓ Forcer HTTPS
✓ Valider toutes les revendications
✓ Implémenter un mécanisme de révocation
✓ Surveiller les jetons anormaux
Références
RFCs connexes:
- [RFC 9068] JWT Profile for OAuth 2.0 Access Tokens ← Ce document
- [RFC 7519] JSON Web Token (JWT)
- [RFC 7515] JSON Web Signature (JWS)
- [RFC 6749] OAuth 2.0 Authorization Framework
- [RFC 7662] OAuth 2.0 Token Introspection
Bibliothèques connexes:
- jose - Bibliothèque JWT JavaScript
- jsonwebtoken - Bibliothèque JWT Node.js
Résumé: RFC 9068 définit le format standard pour les jetons d'accès OAuth 2.0 utilisant JWT. Grâce à l'auto-contention et à la validation hors ligne, les performances et l'évolutivité de l'API sont considérablement améliorées. En combinant des jetons à court terme avec des stratégies de révocation appropriées, une haute performance peut être maintenue tout en assurant la sécurité!