RFC 9068 - JWT Profile for OAuth 2.0 Access Tokens
Informazioni di base
- Numero RFC: 9068
- Titolo: JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens
- Titolo italiano: Profilo JWT per token di accesso OAuth 2.0
- Data di pubblicazione: Ottobre 2021
- Stato: PROPOSED STANDARD (Standard proposto)
- Autori: V. Bertocci (Auth0), B. Campbell (Ping Identity)
Sommario (Abstract)
Questa specifica definisce un profilo standard per l'utilizzo dei token di accesso OAuth 2.0 in formato JWT. Specifica i claim richiesti, i claim raccomandati e i requisiti di convalida per i token di accesso JWT, consentendo ai server di risorse di convalidare e analizzare direttamente i token di accesso senza dover chiamare un endpoint di introspezione ogni volta.
Panoramica dei token di accesso JWT
Perché abbiamo bisogno di token di accesso JWT?
Problemi dei token di accesso tradizionali:
Token opachi (Opaque Token):
- Formato: Stringa casuale (es.: "SlAV32hkKG...XPw")
- Caratteristica: Non può essere analizzato direttamente
- Convalida: Deve chiamare l'endpoint di introspezione (RFC 7662)
Problemi:
❌ Ogni chiamata API richiede introspezione → richieste di rete aggiuntive
❌ Carico elevato sul server di autorizzazione → collo di bottiglia delle prestazioni
❌ Latenza aumentata → esperienza utente scarsa
❌ Punto di errore singolo → guasto del server di autorizzazione influisce su tutte le API
Flusso di esempio:
Client → [access_token] → Server di risorse
↓
Endpoint di introspezione ← Server di autorizzazione
↓
Risposta (active: true)
↓
Convalida riuscita
Vantaggi dei token di accesso JWT:
Token JWT:
- Formato: JSON Web Token autonomo
- Caratteristica: Può essere convalidato e analizzato direttamente
- Convalida: Verifica della firma con chiave pubblica
Vantaggi:
✓ Nessuna introspezione necessaria → zero richieste aggiuntive
✓ Carico ridotto sul server di autorizzazione → alte prestazioni
✓ Bassa latenza → risposta rapida
✓ Convalida offline → guasto del server di autorizzazione non influisce sull'API
✓ Contiene informazioni contestuali → scope, ID utente, ecc.
Flusso di esempio:
Client → [JWT access_token] → Server di risorse
↓
1. Verificare la firma (con chiave pubblica)
2. Verificare la scadenza
3. Verificare l'audience
↓
Convalida riuscita
↓
Leggere scope/ID utente da JWT
Token di accesso JWT vs token opachi
Confronto:
Token opachi (Opaque Token):
Vantaggi:
+ Possono essere revocati in qualsiasi momento (controllo lato server)
+ Token piccolo (solitamente 20-40 caratteri)
+ Nessuna fuga di informazioni (non analizzabile)
Svantaggi:
- Deve essere convalidato tramite introspezione (overhead di rete)
- Pressione elevata sul server di autorizzazione
- Nessuna convalida offline
Casi d'uso:
- Scenari che richiedono revoca immediata
- Requisiti di alta sicurezza (non si desidera che il token sia analizzabile)
- Token a breve termine
Token di accesso JWT:
Vantaggi:
+ Autonomo (nessuna introspezione necessaria)
+ Alte prestazioni (convalida offline)
+ Contiene contesto (scope, claims)
+ Estensibile
Svantaggi:
- Token grande (solitamente 100-300+ caratteri)
- Difficile da revocare (richiede meccanismi aggiuntivi)
- Informazioni visibili (decodifica base64 leggibile)
Casi d'uso:
- API ad alte prestazioni
- Architettura microservizi
- Sistemi distribuiti
- Token devono contenere informazioni contestuali
Soluzione ibrida:
- Token di accesso JWT a breve termine (es. 15 minuti)
- Token di aggiornamento opachi a lungo termine
→ Combina i vantaggi di entrambi gli approcci
Formato del token di accesso JWT
Claim richiesti (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 - Emittente):
"iss": "https://authorization-server.example.com"
Descrizione:
- Identificatore del server di autorizzazione
- Deve essere URL HTTPS
- Utilizzato per trovare la chiave di verifica
Convalida:
const payload = jwt.decode(token);
if (payload.iss !== expectedIssuer) {
throw new Error('Invalid issuer');
}
2. exp (Expiration Time - Tempo di scadenza):
"exp": 1639533600 // Timestamp Unix
Descrizione:
- Tempo di scadenza del token
- Timestamp Unix (secondi)
- Deve essere un tempo futuro
Convalida:
const now = Math.floor(Date.now() / 1000);
if (payload.exp <= now) {
throw new Error('Token expired');
}
Durata di validità consigliata:
- Breve termine: 5-15 minuti (alta sicurezza)
- Medio termine: 1 ora (bilanciato)
- Lungo termine: 24 ore (bassa sicurezza, non consigliato)
3. aud (Audience - Pubblico):
"aud": "https://api.example.com"
o
"aud": ["https://api.example.com", "https://api2.example.com"]
Descrizione:
- Server di risorse di destinazione del token
- Può essere stringa o array di stringhe
- Impedisce l'uso del token per risorse non previste
Convalida:
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 - Soggetto):
"sub": "user-123"
Descrizione:
- Identificatore del soggetto del token
- Solitamente ID utente
- Deve essere univoco nell'ambito dell'emittente
- Tipo stringa
Utilizzo:
// Ottenere l'ID utente dal token
const userId = payload.sub;
const user = await getUserById(userId);
5. client_id (Client Identifier - Identificatore client):
"client_id": "client-456"
Descrizione:
- ID del client che richiede il token
- Utilizzato per tracciamento e audit
- Distingue le richieste di diversi client
Utilizzo:
// Verificare le autorizzazioni del client
const client = await getClient(payload.client_id);
if (!client.hasPermission('write')) {
throw new Error('Client not authorized');
}
6. iat (Issued At - Emesso a):
"iat": 1639530000 // Timestamp Unix
Descrizione:
- Tempo di emissione del token
- Timestamp Unix (secondi)
- Utilizzato per prevenire attacchi replay
Convalida:
const now = Math.floor(Date.now() / 1000);
const maxAge = 3600; // 1 ora
if (now - payload.iat > maxAge) {
throw new Error('Token too old');
}
7. jti (JWT ID - Identificatore JWT):
"jti": "token-789"
Descrizione:
- Identificatore univoco del token
- Utilizzato per prevenire attacchi replay
- Può essere utilizzato per revocare token
Utilizzo:
// Verificare se il token è stato revocato
const isRevoked = await checkRevokedToken(payload.jti);
if (isRevoked) {
throw new Error('Token has been revoked');
}
Claim opzionali (Optional Claims)
8. scope (Scope - Ambito):
"scope": "read write profile"
Descrizione:
- Ambito di autorizzazione
- Stringa separata da spazi
- Il server di risorse esegue il controllo delle autorizzazioni di conseguenza
Utilizzo:
const scopes = payload.scope.split(' ');
if (!scopes.includes('write')) {
return res.status(403).json({ error: 'Insufficient scope' });
}
9. Claim personalizzati:
{
"iss": "https://authorization-server.example.com",
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
// Claim personalizzati
"email": "[email protected]",
"name": "John Doe",
"roles": ["admin", "editor"],
"tenant_id": "tenant-789",
"permissions": ["read:articles", "write:articles"]
}
Descrizione:
- Possono essere aggiunti claim personalizzati arbitrari
- Utilizzato per trasmettere informazioni contestuali
- Riduce le query al database
Nota:
⚠️ Limitazione di dimensione del token (solitamente < 8KB)
⚠️ Non includere informazioni sensibili (token può essere decodificato)
⚠️ Considerare i costi di trasmissione di rete
Esempio JWT completo
Token di accesso JWT completo:
eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCIsImtpZCI6IjEyMyJ9.eyJpc3MiOiJodHRwczovL2F1dGhvcml6YXRpb24tc2VydmVyLmV4YW1wbGUuY29tIiwiZXhwIjoxNjM5NTMzNjAwLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsInN1YiI6InVzZXItMTIzIiwiY2xpZW50X2lkIjoiY2xpZW50LTQ1NiIsImlhdCI6MTYzOTUzMDAwMCwianRpIjoidG9rZW4tNzg5Iiwic2NvcGUiOiJyZWFkIHdyaXRlIn0.signature
Scomposizione:
Header (Intestazione):
{
"alg": "RS256",
"typ": "at+jwt", ← Tipo: JWT token di accesso
"kid": "123"
}
Payload (Carico 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 (Firma):
RSASSA-PKCS1-v1_5 using SHA-256
Firma e convalida
Algoritmi raccomandati
RFC 9068 raccomanda:
Raccomandazione prioritaria:
1. RS256 (RSA con SHA-256)
- Crittografia asimmetrica
- Convalida con chiave pubblica
- Il più comunemente utilizzato
2. ES256 (ECDSA con P-256 e SHA-256)
- Crittografia asimmetrica
- Firma più breve
- Prestazioni migliori
Non raccomandato:
❌ HS256 (HMAC con SHA-256)
- Crittografia simmetrica
- Richiede chiave condivisa
- Sicurezza inferiore (tutti i server di risorse necessitano della chiave)
Perché sono raccomandati gli algoritmi asimmetrici?
→ Il server di autorizzazione firma con chiave privata
→ Il server di risorse convalida con chiave pubblica
→ La chiave pubblica può essere distribuita pubblicamente
→ Nessuna chiave condivisa necessaria
Generazione JWT (Server di autorizzazione)
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 = {
// Claim richiesti
iss: this.issuer,
exp: now + expiresIn, // Predefinito 15 minuti
aud: audience,
sub: userId,
client_id: clientId,
iat: now,
jti: this.generateJti(),
// Claim opzionali
scope: scope
};
// Generare JWT
const jwt = await new jose.SignJWT(payload)
.setProtectedHeader({
alg: 'RS256',
typ: 'at+jwt', // Importante: Identificare come token di accesso
kid: this.keyId
})
.sign(this.privateKey);
return jwt;
}
generateJti() {
return require('crypto').randomBytes(16).toString('hex');
}
}
// Esempio di utilizzo
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 minuti
);
console.log('Access Token:', accessToken);
Convalida JWT (Server di risorse)
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 {
// Passo 1: Ottenere JWKS (set di chiavi pubbliche)
const jwks = await this.getJWKS();
// Passo 2: Convalidare JWT
const { payload, protectedHeader } = await jose.jwtVerify(
token,
jwks,
{
issuer: this.issuer,
audience: this.audience,
typ: 'at+jwt' // Confermare il tipo
}
);
// Passo 3: Convalida aggiuntiva
this.validatePayload(payload);
return {
valid: true,
payload,
header: protectedHeader
};
} catch (err) {
return {
valid: false,
error: err.message
};
}
}
async getJWKS() {
// Memorizzare JWKS per 1 ora
const now = Date.now();
if (this.jwksCache && (now - this.jwksCacheTime < 3600000)) {
return this.jwksCache;
}
// Ottenere JWKS dal server di autorizzazione
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) {
// Verificare i claim richiesti
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}`);
}
}
// Verificare il formato client_id
if (typeof payload.client_id !== 'string') {
throw new Error('client_id must be a string');
}
// Altre convalide personalizzate possono essere aggiunte...
}
// Estrarre gli scopes
getScopes(payload) {
if (!payload.scope) return [];
return payload.scope.split(' ');
}
// Verificare l'autorizzazione
hasScope(payload, requiredScope) {
const scopes = this.getScopes(payload);
return scopes.includes(requiredScope);
}
}
// Esempio di utilizzo
const validator = new JWTAccessTokenValidator(
'https://authorization-server.example.com',
'https://api.example.com'
);
// Convalidare il token di accesso
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);
}
Implementazione middleware Express
Middleware di convalida JWT
const express = require('express');
const jose = require('jose');
// Middleware di convalida JWT
function jwtAuthMiddleware(options = {}) {
const {
issuer,
audience,
requiredScopes = []
} = options;
const validator = new JWTAccessTokenValidator(issuer, audience);
return async (req, res, next) => {
try {
// Passo 1: Estrarre il token
const token = extractToken(req);
if (!token) {
return res.status(401).json({
error: 'invalid_request',
error_description: 'Missing access token'
});
}
// Passo 2: Convalidare il token
const result = await validator.validate(token);
if (!result.valid) {
return res.status(401).json({
error: 'invalid_token',
error_description: result.error
});
}
// Passo 3: Verificare gli 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(', ')}`
});
}
}
// Passo 4: Allegare all'oggetto richiesta
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'
});
}
};
}
// Funzione di aiuto per estrarre il token
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];
}
// Esempio di utilizzo
const app = express();
// Configurare la convalida JWT
const jwtAuth = jwtAuthMiddleware({
issuer: 'https://authorization-server.example.com',
audience: 'https://api.example.com'
});
// Endpoint pubblico (nessuna autenticazione richiesta)
app.get('/api/public', (req, res) => {
res.json({ message: 'Public endpoint' });
});
// Endpoint protetto (autenticazione richiesta)
app.get('/api/protected', jwtAuth, (req, res) => {
res.json({
message: 'Protected endpoint',
user: req.auth.userId,
client: req.auth.clientId
});
});
// Endpoint che richiede scopes specifici
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
});
}
);
// Verifica dinamica degli scopes
app.delete('/api/articles/:id', jwtAuth, async (req, res) => {
const article = await getArticle(req.params.id);
// Verificare l'autorizzazione: amministratore o autore
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');
});
Decoratore di convalida scope
// Convalida avanzata degli scopes
class ScopeValidator {
// Richiede tutti gli scopes (logica E)
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();
};
}
// Richiede qualsiasi scope (logica O)
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();
};
}
// Espressione complessa
static requireExpression(expression) {
return (req, res, next) => {
const scopes = req.auth.scopes;
// Esempio: "(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();
};
}
}
// Utilizzo
app.get('/api/data',
jwtAuth,
ScopeValidator.requireAll('read', 'data'),
(req, res) => {
// Richiede read E data
}
);
app.post('/api/admin/users',
jwtAuth,
ScopeValidator.requireAny('admin', 'super_admin'),
(req, res) => {
// Richiede admin O super_admin
}
);
Strategie di revoca token
Problema e soluzioni
Dilemma di revoca JWT:
Problema:
JWT è autonomo, il server di risorse non interroga il server di autorizzazione
→ Impossibile revocare immediatamente il token
Scenari:
1. L'utente si disconnette
2. Password modificata
3. Autorizzazioni revocate
4. Incidente di sicurezza
Soluzioni:
Soluzione 1: Token a breve termine
// Emettere token di accesso a breve termine (5-15 minuti)
const accessToken = await issuer.issueAccessToken(
userId,
clientId,
audience,
scope,
900 // 15 minuti
);
Vantaggi:
✓ Implementazione semplice
✓ Scadenza automatica rapida
✓ Finestra di revoca piccola
Svantaggi:
- Richiede aggiornamento frequente
- Esperienza utente influenzata
Soluzione 2: Lista di revoca (Revocation List)
class TokenRevocationList {
constructor() {
this.revokedTokens = new Set();
// L'ambiente di produzione dovrebbe utilizzare Redis o cache distribuita simile
}
// Revocare il token
revoke(jti, expiresAt) {
this.revokedTokens.add(jti);
// Impostare il tempo di scadenza (pulizia automatica dopo la scadenza del token)
setTimeout(() => {
this.revokedTokens.delete(jti);
}, (expiresAt * 1000) - Date.now());
}
// Verificare se revocato
isRevoked(jti) {
return this.revokedTokens.has(jti);
}
}
const revocationList = new TokenRevocationList();
// Verificare nel middleware di convalida
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();
}
// Utilizzo
app.use('/api', jwtAuth, checkRevocation);
Soluzione 3: Numero di versione/timestamp
// Aggiungere campo alla tabella utente
{
userId: '123',
tokenVersion: 5, // Incrementare ad ogni disconnessione/cambio password
// o
tokensInvalidBefore: 1639530000 // Token prima di questo tempo non validi
}
// Includere il numero di versione durante la generazione del token
const accessToken = await issuer.issueAccessToken(
userId,
clientId,
audience,
scope,
900,
{ token_version: user.tokenVersion } // Claim personalizzato
);
// Verificare durante la convalida
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();
}
Soluzione 4: Soluzione ibrida (Consigliata)
// Combinare più strategie
class HybridRevocationStrategy {
constructor() {
this.shortTermRevocations = new Set(); // Revoca recente (Redis)
this.userVersions = new Map(); // Versione token utente (database)
}
async isValid(payload) {
// Verifica 1: JTI nella lista di revoca
if (this.shortTermRevocations.has(payload.jti)) {
return false;
}
// Verifica 2: Versione del token
const userVersion = await this.getUserTokenVersion(payload.sub);
if (payload.token_version < userVersion) {
return false;
}
// Verifica 3: Timestamp
const invalidBefore = await this.getUserTokensInvalidBefore(payload.sub);
if (payload.iat < invalidBefore) {
return false;
}
return true;
}
async revokeToken(jti, expiresAt) {
// Revoca a breve termine (fino alla scadenza del token)
this.shortTermRevocations.add(jti);
setTimeout(() => {
this.shortTermRevocations.delete(jti);
}, (expiresAt * 1000) - Date.now());
}
async revokeAllUserTokens(userId) {
// Incrementare la versione del token utente
const currentVersion = await this.getUserTokenVersion(userId);
await this.setUserTokenVersion(userId, currentVersion + 1);
}
}
Ottimizzazione delle prestazioni
Memorizzazione nella 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();
// Verificare la cache
if (this.cache && (now - this.cacheTime < this.cacheDuration)) {
return this.cache;
}
// Ottenere nuovo JWKS
try {
const response = await fetch(this.jwksUrl);
const jwks = await response.json();
this.cache = jwks;
this.cacheTime = now;
return jwks;
} catch (err) {
// In caso di errore di recupero ma con cache vecchia, continuare a utilizzarla
if (this.cache) {
console.warn('JWKS fetch failed, using cached version');
return this.cache;
}
throw err;
}
}
invalidate() {
this.cache = null;
this.cacheTime = 0;
}
}
Memorizzazione nella cache dei risultati di convalida
class TokenValidationCache {
constructor(cacheDuration = 60000) { // 1 minuto
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
});
// Pulizia regolare
this.cleanup();
}
cleanup() {
const now = Date.now();
for (const [token, entry] of this.cache.entries()) {
if (now > entry.expiresAt) {
this.cache.delete(token);
}
}
}
}
// Utilizzo
const validationCache = new TokenValidationCache();
async function validateWithCache(token, validator) {
// Verificare la cache
let result = validationCache.get(token);
if (!result) {
// Convalidare il token
result = await validator.validate(token);
// Memorizzare nella cache solo i token validi
if (result.valid) {
validationCache.set(token, result);
}
}
return result;
}
Best practice
1. Ottimizzazione dimensione token
// ❌ Male: Token troppo grande
{
"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",
// Troppi claim personalizzati
"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": { /* molti dati */ }
}
// Risultato: Token > 2KB
// ✓ Bene: Token snello
{
"iss": "https://auth.example.com", // Dominio breve
"exp": 1639533600,
"aud": "https://api.example.com",
"sub": "user-123",
"client_id": "client-456",
"iat": 1639530000,
"jti": "token-789",
"scope": "read write",
// Includere solo i claim personalizzati necessari
"roles": ["admin"]
}
// Risultato: Token < 500 byte
Strategia:
1. Utilizzare URL emittente breve
2. Minimizzare i claim personalizzati
3. Memorizzare grandi dati lato server, includere solo il riferimento nel token
4. Utilizzare ruoli invece di autorizzazioni dettagliate
2. Sicurezza
// ✓ Configurazione di sicurezza corretta
const securityConfig = {
// Token a breve termine
accessTokenTTL: 900, // 15 minuti
// Utilizzare algoritmo asimmetrico
algorithm: 'RS256',
// Intestazione typ
tokenType: 'at+jwt',
// Audience specifico
audience: 'https://api.example.com', // Non utilizzare caratteri jolly
// Forzare HTTPS
requireHTTPS: true,
// Tolleranza di scostamento dell'orologio
clockTolerance: 60 // 1 minuto
};
// ❌ Configurazione non sicura
const insecureConfig = {
accessTokenTTL: 86400, // 24 ore - troppo lungo!
algorithm: 'HS256', // Algoritmo simmetrico - non consigliato!
tokenType: 'JWT', // Non specifico
audience: '*', // Carattere jolly - pericoloso!
requireHTTPS: false // HTTP - non sicuro!
};
3. Gestione degli errori
// Gestione completa degli errori
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'
});
}
// Errore generale
console.error('JWT error:', err);
res.status(401).json({
error: 'invalid_token',
error_description: 'Token validation failed'
});
}
}
app.use(JWTErrorHandler.handle);
Sommario
Punti chiave
✓ I token di accesso JWT sono autonomi
✓ Nessuna introspezione necessaria → alte prestazioni
✓ Convalida offline → scalabile
✓ Contiene contesto → riduce le query al database
✓ Formato standardizzato → interoperabilità
Claim richiesti:
- iss, exp, aud, sub, client_id, iat, jti
Pratiche consigliate:
✓ Utilizzare gli algoritmi RS256 o ES256
✓ Token a breve termine (5-15 minuti)
✓ Impostare l'intestazione typ su "at+jwt"
✓ Audience specifico
✓ Minimizzare la dimensione del token
✓ Implementare una strategia di revoca
Ottimizzazione delle prestazioni:
✓ Memorizzazione nella cache JWKS
✓ Memorizzazione nella cache dei risultati di convalida
✓ Convalida asincrona
Considerazioni sulla sicurezza:
✓ Forzare HTTPS
✓ Convalidare tutti i claim
✓ Implementare un meccanismo di revoca
✓ Monitorare token anomali
Riferimenti
RFC correlati:
- [RFC 9068] JWT Profile for OAuth 2.0 Access Tokens ← Questo documento
- [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
Librerie correlate:
- jose - Libreria JWT JavaScript
- jsonwebtoken - Libreria JWT Node.js
Sommario: RFC 9068 definisce il formato standard per i token di accesso OAuth 2.0 utilizzando JWT. Grazie all'autonomia e alla convalida offline, le prestazioni e la scalabilità dell'API sono notevolmente migliorate. Combinando token a breve termine con strategie di revoca appropriate, è possibile garantire alte prestazioni mantenendo la sicurezza!