Passa al contenuto principale

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!