La sicurezza multitenant in applicazioni SaaS richiede un approccio granulare e dinamico all’autenticazione, dove il token non è solo un portatore di identità, ma un veicolo di contesto che definisce chi può accedere a quali risorse. A differenza dei sistemi monolitici, dove un token unico valida l’utente in generale, un ambiente multitenant impone una validazione contestuale basata sul tenant, garantendo isolamento rigoroso tra clienti, agenzie o enti pubblici. Questo articolo approfondisce, passo dopo passo, come progettare e implementare un middleware di autenticazione in Node.js Express che integri token JWT firmati con RS256, validazione atomica del tenantId, rotazione sicura dei token e gestione avanzata dei ruoli, con riferimenti espliciti al Tier 1 (fondamenti) e Tier 2 (flusso operativo) del modello Tier.
—
1. Il contesto multitenant e il ruolo critico del token di autenticazione
In un’architettura multitenant, ogni tenant – che rappresenti un’agenzia, un ente pubblico o un cliente – condivide l’infrastruttura tecnica ma deve operare in ambiente isolato. Il token di accesso diventa quindi il fulcro per garantire che ogni richiesta sia autenticata, autorizzata e contestualizzata correttamente.
Il Tier 1 introduce il concetto di token come portatore di identità: un JWT firmato che contiene claim standard come `iss` (issuer), `sub` (subject) e `exp` (scadenza). Tuttavia, in un contesto multitenant, il token deve ampliare il proprio payload con claim specifici per il tenant, tra cui `tenantId` – un identificatore univoco che permette di tracciare l’origine e il contesto dell’accesso.
Il Tier 2 formalizza questo processo: l’autenticazione non è solo verifica di identità, ma decodifica contestuale del claim `tenantId`, garantendo che ogni operazione sia rimandata al tenant corretto e che non vi sia cross-tenant leakage.
**Takeaway operativo:** ogni token valido deve includere un `tenantId` non modificabile, che funge da chiave per l’isolamento logico delle risorse.
—
2. Fondamenti tecnici del middleware in Express e integrazione JWT con RS256
Il middleware Express è una pipeline asincrona che intercetta ogni richiesta HTTP, eseguendo operazioni di validazione prima di raggiungere il route handler. In un contesto multitenant, il middleware `authMiddleware` deve:
– Estrarre il token dalla header `Authorization: Bearer
– Verificare la firma con la chiave pubblica del tenant corrente (RS256: firma asimmetrica con algoritmo RS256)
– Decodificare il token, validando `tenantId`, `exp`, `iss`, `aud` e i ruoli associati
– Memorizzare il tenant in un contesto atomico (es. `req.tenant`) per uso downstream
**Dettaglio critico:** l’operazione di firma e verifica deve essere asincrona e protetta da timeout per evitare deadlock. L’uso di `jsonwebtoken` con `RS256` garantisce autenticità senza dover condividere chiavi segrete, riducendo il rischio di compromise.
// Esempio di middleware authMiddleware con RS256
const jwt = require(‘jsonwebtoken’);
const { promisify } = require(‘util’);
async function authMiddleware(req, res, next) {
const authHeader = req.headers[‘authorization’];
if (!authHeader || !authHeader.startsWith(‘Bearer ‘)) {
return res.status(401).json({ error: ‘Token mancante o malformato’ });
}
const token = authHeader.split(‘ ‘)[1];
try {
// Validazione firma asimmetrica con chiave pubblica per tenantId corrente
const decoded = await promisify(jwt.verify)(token, process.env.JWT_PUBLIC_KEY, {
algorithms: [‘RS256’],
audience: req.tenantId, // claims contestuali
issuer: ‘auth-service.it’,
});
// Estrazione e validazione tenantId atomica
if (!decoded.tenantId || typeof decoded.tenantId !== ‘string’) {
throw new Error(‘tenantId non valido o assente’);
}
req.tenant = decoded.tenantId;
req.user = decoded.sub;
req.scopes = (decoded.scopes || []).filter(s => s.includes(‘read:data’));
next();
} catch (err) {
if (err.name === ‘TokenExpiredError’) {
return res.status(401).json({ error: ‘Token scaduto’ });
}
if (err.name === ‘JsonWebTokenError’) {
return res.status(401).json({ error: ‘Token non autentico o alterato’ });
}
return res.status(500).json({ error: ‘Errore interno nell’autenticazione’ });
}
}
—
3. Progettazione dello schema del token multitenant
Il payload del token deve essere strutturato per supportare validazione contestuale e autorizzazione fine-grained. La struttura base include:
{
“tenantId”: “tenant-12345”,
“sub”: “user-789”,
“iss”: “auth-service.it”,
“exp”: 1706745600,
“aud”: “api.multitenant.it”,
“roles”: [“admin”, “data-reader”],
“scopes”: [“read:tenant-data”, “write:logs”],
“custom_claims”: {
“department”: “finance”,
“location”: “Roma”
}
}
– `tenantId`: chiave critica per l’isolamento, usata come claim contestuale
– `roles` e `scopes`: permettono il controllo di accesso dinamico in base al tenant e al profilo
– `custom_claims`: estensioni specifiche per policy aziendali (es. reparto, localizzazione)
**Fase 1: generazione token con tenantId dinamico**
Durante la registrazione o login, il token viene emesso con `tenantId` estratto dal contesto (es. sottodominio `api.agenzia.it` o header `X-Tenant-ID`).
Esempio in Node.js:
const token = jwt.sign({
tenantId: req.tenantId,
sub: user.id,
iss: ‘auth-service.it’,
exp: Math.floor(Date.now() / 1000) + (7 * 24 * 60 * 60), // 7 giorni
aud: ‘api.multitenant.it’,
roles: [‘admin’],
scopes: [‘read:client-data’],
custom_claims: {
department: ‘marketing’,
location: ‘Milano’,
},
}, process.env.JWT_PRIVATE_KEY, { algorithm: ‘RS256’, audience: req.tenantId });
**Fase 2: revoca e rotazione sicura**
In ambiente Tier 2, la rotazione richiede strategie avanzate:
– **Blacklist in memoria** con TTL breve (es. 15 min) per token scaduti
– **Token di refresh a lungo termine** (es. 30 giorni) con revoca centralizzata in Vault (AWS KMS o HashiCorp Vault)
– **Revoca immediata** su logout: invalidare `tenantId` + token via challenge (es. refresh token revocato)
—
4. Implementazione del middleware di autenticazione: passo dopo passo
**Passo 1: estrazione e validazione del token**
Il middleware intercetta la richiesta, estrae il token dalla header e lo verifica con `RS256` usando la chiave pubblica specifica del tenant.
const token = req.headers[‘authorization’]?.split(‘ ‘)[1]?.trim();
if (!token) throw new Error(‘Token mancante’);
try {
const decoded = await promisify(jwt.verify)(token, process.env.JWT_PUBLIC_KEY, { audience: req.tenantId });
req.tenant = decoded.tenantId;
} catch (err) {
throw new Error(‘Invalid or expired token’);
}
**Passo 2: validazione del tenantId atomica**
Verifica che `tenantId` sia presente, valido e corrisponda a quello richiesto dal tenant (es. sottodominio o header `X-Tenant-ID`).
if (!decoded.tenantId || typeof decoded.tenantId !== ‘string’) {
throw new Error(‘Invalid tenant identifier’);
}
**Passo 3: autorizzazione contestuale tramite ruoli e scope**
Estrae ruoli e scope, confrontandoli con policy aziendali memorizzate (es. JSON o DB).
const requiredScope = ‘write:client-data’;
if (!req.scopes.includes(requiredScope)) {
throw new Error(‘Access denied: insufficient scopes’);
}
**Passo 4: logging sicuro**
Nessun token, `tenantId` o payload sensibili devono apparire nei log. Usare formati anonimi o hash:
console.log(`[Auth] Tenant ${req.tenant} accessed resource with roles [${req.roles.join(‘, ‘)}]`);
—
5. Errori comuni e risoluzione avanzata
| Errore frequente | Causa principale | Soluzione pratica |
|——————————————|————————————————–|———————————————————————————–|
| `TokenExpiredError` | Token scaduto o con TTL breve