Passa al contenuto principale

RFC 9068 - Profilo JWT per i token di accesso OAuth 2.0

  • Stato: Proposed Standard
  • Pubblicato: October 2021
  • Stream: IETF
  • Errata: Nessun errata

Riepilogo (Abstract)​

Questa specifica definisce un profilo (Profile) per l'emissione di token di accesso (Access Token) OAuth 2.0 nel formato JSON Web Token (JWT). I server di autorizzazione (Authorization Server) e i server di risorse (Resource Server) di diversi fornitori possono utilizzare questo profilo per emettere e consumare token di accesso in modo interoperabile.


Stato di questo memorandum (Status of This Memo)​

Questo è un documento Internet Standards Track.

Questo documento è un prodotto dell'Internet Engineering Task Force (IETF). Rappresenta il consenso della comunità IETF. Ha ricevuto una revisione pubblica ed è stato approvato per la pubblicazione dall'Internet Engineering Steering Group (IESG). Ulteriori informazioni sugli standard Internet sono disponibili nella Sezione 2 di RFC 7841.

Le informazioni sullo stato attuale di questo documento, eventuali errata e come fornire feedback possono essere ottenute all'indirizzo: https://www.rfc-editor.org/info/rfc9068.


Indice (Contents)​


Copyright (c) 2021 IETF Trust e le persone identificate come autori del documento. Tutti i diritti riservati.

Questo documento è soggetto al BCP 78 e alle disposizioni legali dell'IETF Trust relative ai documenti IETF (https://trustee.ietf.org/license-info) in vigore alla data di pubblicazione di questo documento. Si prega di esaminare attentamente questi documenti, poiché descrivono i vostri diritti e le restrizioni relative a questo documento. I componenti di codice estratti da questo documento devono includere il testo della licenza BSD semplificata come descritto nella Sezione 4.e delle disposizioni legali del Trust e sono forniti senza garanzia come descritto nella licenza BSD semplificata.


Risorse​



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:


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!