Creo applicazioni web moderne e strumenti digitali personalizzati per aiutare le attività a crescere attraverso l'innovazione tecnologica. La mia passione è unire informatica ed economia per generare valore reale.
La mia passione per l'informatica è nata tra i banchi dell'Istituto Tecnico Commerciale di Maglie, dove ho scoperto il potere della programmazione e il fascino di creare soluzioni digitali. Fin da subito, ho capito che l'informatica non era solo codice, ma uno strumento straordinario per trasformare idee in realtà.
Durante gli studi superiori in Sistemi Informativi Aziendali, ho iniziato a intrecciare informatica ed economia, comprendendo come la tecnologia possa essere il motore della crescita per qualsiasi attività. Questa visione mi ha accompagnato all'Università degli Studi di Bari, dove ho conseguito la Laurea in Informatica, approfondendo le mie competenze tecniche e la mia passione per lo sviluppo software.
Oggi metto questa esperienza al servizio di imprese, professionisti e startup, creando soluzioni digitali su misura che automatizzano processi, ottimizzano risorse e aprono nuove opportunità di business. Perché la vera innovazione inizia quando la tecnologia incontra le esigenze reali delle persone.
Le Mie Competenze
Analisi Dati & Modelli Previsionali
Trasformo i dati in insights strategici con analisi approfondite e modelli predittivi per decisioni informate
Automazione Processi
Creo strumenti personalizzati che automatizzano operazioni ripetitive e liberano tempo per attività a valore aggiunto
Sistemi Custom
Sviluppo sistemi software su misura, dalle integrazioni tra piattaforme alle dashboard personalizzate
Credo fermamente che l'informatica sia lo strumento più potente per trasformare le idee in realtà e migliorare la vita delle persone.
Democratizzare la Tecnologia
La mia missione è rendere l'informatica accessibile a tutti: dalle piccole imprese locali alle startup innovative, fino ai professionisti che vogliono digitalizzare la propria attività. Ogni realtà merita di sfruttare le potenzialità del digitale.
Unire Informatica ed Economia
Non è solo questione di scrivere codice: è capire come la tecnologia possa generare valore reale. Intrecciando competenze informatiche e visione economica, aiuto le attività a crescere, ottimizzare processi e raggiungere nuovi traguardi di efficienza e redditività.
Creare Soluzioni su Misura
Ogni attività è unica, e così devono esserlo le soluzioni. Sviluppo strumenti personalizzati che rispondono alle esigenze specifiche di ciascun cliente, automatizzando processi ripetitivi e liberando tempo per ciò che conta davvero: far crescere il business.
Trasforma la Tua Attività con la Tecnologia
Che tu gestisca un negozio, uno studio professionale o un'azienda, posso aiutarti a sfruttare le potenzialità dell'informatica per lavorare meglio, più velocemente e in modo più intelligente.
Il mio percorso accademico e le tecnologie che padroneggio
Certificazioni Professionali
8 certificazioni conseguite
Nuovo
Visualizza
Reinvention With Agentic AI Learning Program
Anthropic
Dicembre 2024
Nuovo
Visualizza
Agentic AI Fluency
Anthropic
Dicembre 2024
Nuovo
Visualizza
AI Fluency for Students
Anthropic
Dicembre 2024
Nuovo
Visualizza
AI Fluency: Framework and Foundations
Anthropic
Dicembre 2024
Nuovo
Visualizza
Claude with the Anthropic API
Anthropic
Dicembre 2024
Visualizza
Master SQL
RoadMap.sh
Novembre 2024
Visualizza
Oracle Certified Foundations Associate
Oracle
Ottobre 2024
Visualizza
People Leadership Credential
Connect
Settembre 2024
Linguaggi & Tecnologie
Java
Python
JavaScript
Angular
React
TypeScript
SQL
PHP
CSS/SCSS
Node.js
Docker
Git
💼
12/2024 - Presente
Custom Software Engineering Analyst
Accenture
Bari, Puglia, Italia · Ibrida
Analisi e sviluppo di sistemi informatici attraverso l'utilizzo di Java e Quarkus in Health and Public Sector. Formazione continua su tecnologie moderne per la creazione di soluzioni software personalizzate ed efficienti e sugli agenti.
💼
06/2022 - 12/2024
Analista software e Back End Developer Associate Consultant
Links Management and Technology SpA
Esperienza nell'analisi di sistemi software as-is e flussi ETL utilizzando PowerCenter. Formazione completata su Spring Boot per lo sviluppo di applicazioni backend moderne e scalabili. Sviluppatore Backend specializzato in Spring Boot, con esperienza in progettazione di database, analisi, sviluppo e testing dei task assegnati.
💼
02/2021 - 10/2021
Programmatore software
Adesso.it (prima era WebScience srl)
Esperienza nell'analisi AS-IS e TO-BE, evoluzioni SEO ed evoluzioni website per migliorare le performance e l'engagement degli utenti.
🎓
2018 - 2025
Laurea in Informatica
Università degli Studi di Bari Aldo Moro
Bachelor's degree in Computer Science, focusing on software engineering, algorithms, and modern development practices.
📚
2013 - 2018
Diploma - Sistemi Informativi Aziendali
Istituto Tecnico Commerciale di Maglie
Technical diploma specializing in Business Information Systems, combining IT knowledge with business management.
Contattami
Hai un progetto in mente? Parliamone! Compila il form qui sotto e ti risponderò al più presto.
* Campi obbligatori. I tuoi dati saranno utilizzati solo per rispondere alla tua richiesta.
04 - Autenticazione Sicura: Session, Cookie e Identita Moderna
L'autenticazione e il cancello d'ingresso della tua applicazione. Se e vulnerabile, ogni altra misura di sicurezza
può essere aggirata. Nel 2024, secondo il Verizon DBIR, il 74% dei data breach ha coinvolto credenziali
compromesse o attacchi legati all'identità. Eppure la maggior parte delle applicazioni implementa ancora
pattern di autenticazione del 2010: sessioni server-side con cookie non protetti, password in chiaro nei log,
nessun MFA, e token JWT memorizzati in localStorage.
Il panorama dell'autenticazione nel 2025 e radicalmente cambiato. OAuth 2.1 ha consolidato le best practice
di sicurezza rendendo PKCE obbligatorio per tutti i client. Le passkeys basate su WebAuthn stanno sostituendo
le password tradizionali, con Apple, Google e Microsoft che le promuovono come standard. Il NIST SP 800-63B
ha rivisto le linee guida sulle password eliminando i requisiti di rotazione periodica. Questo articolo ti
guida attraverso l'intera superficie di attacco dell'autenticazione con codice pratico, pitfall comuni e
una checklist di sicurezza per applicazioni Node.js.
JWT best practices e i 5 errori fatali che rendono i token insicuri
OAuth 2.1 con PKCE: authorization code flow per SPA e applicazioni mobile
WebAuthn e passkeys: implementazione pratica con SimpleWebAuthn
MFA con TOTP: integrazione con Google Authenticator e Authy via otpauth
RBAC, ABAC e ReBAC: modelli di autorizzazione per applicazioni moderne
Middleware di autenticazione Express.js completo con rate limiting e audit log
Checklist OWASP A07:2021 (Identification and Authentication Failures)
Session Management: Le Fondamenta
La gestione delle sessioni e il meccanismo con cui il server "ricorda" un utente autenticato attraverso
richieste HTTP stateless. Una sessione sicura richiede quattro proprietà fondamentali:
identificatori imprevedibili, trasmissione sicura,
scadenza controllata e invalidazione corretta al logout.
Il principale vettore di attacco e la session fixation: l'attaccante fornisce alla vittima
un session ID noto prima del login, poi riutilizza quello stesso ID dopo l'autenticazione per impersonare
l'utente. La contromisura e rigenerare sempre il session ID dopo il login. Un secondo vettore e il
session hijacking via XSS, mitigato dai cookie con flag HttpOnly. Un terzo
vettore sono le sessioni orfane: sessioni mai invalidate lato server che rimangono
attive anche dopo che il client ha cancellato il cookie, permettendo il replay di session ID rubati.
// Session management sicuro con express-session
// npm install express-session connect-pg-simple @types/express-session
import session from 'express-session';
import pgSession from 'connect-pg-simple';
import crypto from 'crypto';
const PgSession = pgSession(session);
// Configurazione sicura della sessione
app.use(session({
// Store PostgreSQL invece di MemoryStore (non usare in produzione)
store: new PgSession({
pool: dbPool,
tableName: 'user_sessions',
createTableIfMissing: true,
}),
// Secret forte: usa variabile d'ambiente, mai hardcoded
secret: process.env.SESSION_SECRET!, // min 32 caratteri random
// Non salvare sessioni non modificate
resave: false,
// Non creare sessioni vuote per utenti non autenticati
saveUninitialized: false,
// Configurazione cookie sicura
cookie: {
httpOnly: true, // Blocca accesso JavaScript (XSS protection)
secure: true, // Solo HTTPS (usa false solo in sviluppo locale)
sameSite: 'strict', // Blocca CSRF cross-origin
maxAge: 8 * 60 * 60 * 1000, // 8 ore in millisecondi
domain: process.env.COOKIE_DOMAIN, // Limita al dominio specifico
path: '/',
},
// Nome custom invece di 'connect.sid' (riduce fingerprinting)
name: '__session',
// Genera ID sicuro con crypto.randomBytes
genid: () => crypto.randomBytes(32).toString('hex'),
}));
// CRITICO: Rigenera session ID dopo login (previene session fixation)
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await authenticateUser(username, password);
if (!user) {
return res.status(401).json({ error: 'Credenziali non valide' });
}
// CRITICO: rigenerare SEMPRE il session ID dopo autenticazione
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.userId = user.id;
req.session.userRole = user.role;
req.session.loginTime = Date.now();
req.session.save((err) => {
if (err) return res.status(500).json({ error: 'Session save error' });
res.json({ success: true });
});
});
});
// Logout sicuro: distruggi sessione lato server
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) return res.status(500).json({ error: 'Logout error' });
// Elimina il cookie dal browser
res.clearCookie('__session', {
httpOnly: true,
secure: true,
sameSite: 'strict',
});
res.json({ success: true });
});
});
Anti-Pattern: MemoryStore in Produzione
Non usare mai il MemoryStore di default di express-session in produzione.
Causa memory leak, non scala orizzontalmente (sessioni perse al restart di un nodo),
e non persiste le sessioni tra deploy. Usa sempre un store esterno: Redis
(connect-redis), PostgreSQL (connect-pg-simple),
o MongoDB (connect-mongo). Redis e la scelta preferita per prestazioni
ottimali e TTL automatico delle chiavi scadute.
Cookie Security: I Flag Che Contano
I cookie di sessione devono essere configurati con almeno tre attributi di sicurezza. Ognuno mitiga
una categoria specifica di attacchi. La combinazione corretta e HttpOnly + Secure + SameSite,
configurati insieme per una protezione stratificata:
Attributo
Valore consigliato
Minaccia mitigata
HttpOnly
true
XSS: JavaScript non può leggere il cookie
Secure
true
MITM: cookie trasmesso solo su HTTPS
SameSite
Strict o Lax
CSRF: blocca invio cross-origin non autorizzato
MaxAge
8-24 ore per sessioni normali
Sessioni orfane: scadenza automatica lato browser
Domain
Dominio specifico, no wildcard
Cookie leakage su sottodomini compromessi
La scelta tra SameSite=Strict e SameSite=Lax dipende dal caso d'uso.
Strict offre la massima protezione ma blocca i cookie anche quando l'utente naviga
verso il tuo sito da un link esterno (es. email o altro sito), causando un redirect al login ogni volta.
Lax permette il cookie nelle navigazioni top-level (click su link) ma lo blocca per
richieste cross-origin di tipo POST/PUT/DELETE, offrendo un buon equilibrio tra sicurezza e usabilita.
Per API utilizzate da SPA su dominio diverso, usa SameSite=None; Secure con protezione
CSRF via token nel header.
JWT Best Practices e i 5 Errori Fatali
I JSON Web Token sono uno strumento potente per l'autenticazione stateless nelle API, ma la loro
implementazione errata e tra le cause più comuni di vulnerabilità critiche. Il NIST e OWASP
identificano cinque errori fatali che nullificano completamente la sicurezza dei JWT, ognuno
con conseguenze devastanti se sfruttato in produzione.
I 5 Errori Fatali dei JWT
Errore #1 - Algoritmo "none": Alcuni server accettano token con "alg": "none", senza firma. Un attaccante può forgiare qualsiasi payload.
Errore #2 - Algorithm Confusion: Un server RS256 che accetta HS256 permette all'attaccante di firmare con la chiave pubblica (nota).
Errore #3 - JWT in localStorage: Accessibile da qualsiasi script JS. Un XSS su qualsiasi dipendenza npm può rubare il token.
Errore #4 - Token senza scadenza: Un access token che non scade mai non può essere revocato in caso di compromissione.
Errore #5 - Secret debole per HS256: Secret brevi o prevedibili sono vulnerabili a attacchi offline di dictionary o brute force.
// JWT sicuro con jsonwebtoken - Node.js TypeScript
// npm install jsonwebtoken @types/jsonwebtoken
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
const JWT_CONFIG = {
// Usa RS256 o EdDSA per produzione, non HS256 se il secret può trapelare
// HS256 va bene solo se il secret e condiviso tra servizi fidati interni
algorithm: 'RS256' as const,
accessTokenExpiry: '15m', // Token di accesso: vita breve (15 minuti)
refreshTokenExpiry: '7d', // Refresh token: vita lunga, salvato in DB
};
// Carica chiave privata RSA da variabile d'ambiente
const PRIVATE_KEY = process.env.JWT_PRIVATE_KEY!;
const PUBLIC_KEY = process.env.JWT_PUBLIC_KEY!;
// Genera access token
function generateAccessToken(userId: string, role: string): string {
return jwt.sign(
{
sub: userId, // Subject: ID utente
role: role,
iat: Math.floor(Date.now() / 1000),
jti: crypto.randomUUID(), // JWT ID univoco (per blacklist/revoca)
},
PRIVATE_KEY,
{
algorithm: JWT_CONFIG.algorithm,
expiresIn: JWT_CONFIG.accessTokenExpiry,
issuer: 'https://auth.myapp.com',
audience: 'https://api.myapp.com',
}
);
}
// Verifica token - SPECIFICA SEMPRE l'algoritmo atteso
function verifyAccessToken(token: string): jwt.JwtPayload {
try {
return jwt.verify(token, PUBLIC_KEY, {
// CRITICO: whitelist di algoritmi, mai lasciare aperto o accettare "none"
algorithms: ['RS256'],
issuer: 'https://auth.myapp.com',
audience: 'https://api.myapp.com',
}) as jwt.JwtPayload;
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
throw new Error('TOKEN_EXPIRED');
}
if (err instanceof jwt.JsonWebTokenError) {
throw new Error('TOKEN_INVALID');
}
throw err;
}
}
// Refresh token rotation: ogni uso genera un nuovo refresh token
// Salva il refresh token nel DB con hash bcrypt (non in chiaro)
import bcrypt from 'bcrypt';
async function storeRefreshToken(
userId: string,
token: string
): Promise<void> {
const tokenHash = await bcrypt.hash(token, 12);
await db.query(
`INSERT INTO refresh_tokens (user_id, token_hash, expires_at, created_at)
VALUES ($1, $2, NOW() + INTERVAL '7 days', NOW())`,
[userId, tokenHash]
);
}
// Middleware di autenticazione Express
export const authenticateJWT = async (
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
// Leggi token da cookie HttpOnly (preferibile) o Authorization header
// EVITA localStorage: vulnerabile a XSS
const token = req.cookies['access_token'] ?? extractBearerToken(req);
if (!token) {
res.status(401).json({ error: 'Authentication required' });
return;
}
try {
const payload = verifyAccessToken(token);
req.user = { id: payload.sub!, role: payload.role };
next();
} catch (err) {
if ((err as Error).message === 'TOKEN_EXPIRED') {
res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
} else {
res.status(401).json({ error: 'Invalid token' });
}
}
};
function extractBearerToken(req: Request): string | null {
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
return authHeader.slice(7);
}
return null;
}
JWT Storage: Cookie HttpOnly vs localStorage
Molti tutorial consigliano di salvare JWT in localStorage perchè e semplice da usare
con le SPA. Non farlo mai per token di autenticazione. localStorage e accessibile
da qualsiasi script JavaScript in esecuzione sulla pagina, incluse librerie di terze parti compromesse.
Un attacco XSS su una qualsiasi dipendenza npm può rubare silenziosamente tutti i token.
La soluzione corretta e usare cookie HttpOnly + Secure + SameSite=Strict:
inaccessibili da JavaScript, trasmessi solo su HTTPS, e con protezione CSRF incorporata.
Per SPA che devono fare richieste API cross-origin, usa SameSite=None; Secure
con CSRF token nel header della richiesta.
OAuth 2.1 con PKCE: Autenticazione Federata Sicura
OAuth 2.1 (draft RFC, consolidato nel 2024) unifica le best practice di sicurezza accumulate in anni
di deployment OAuth 2.0, rendendo PKCE (Proof Key for Code Exchange) obbligatorio
per tutti i client, inclusi i client confidenziali con client secret. Elimina inoltre l'implicit flow
e il password grant, considerati intrinsecamente insicuri e deprecati.
Il flusso Authorization Code con PKCE funziona cosi: il client genera un code_verifier
random (43-128 caratteri), ne calcola il code_challenge come SHA-256 Base64URL,
invia il challenge all'authorization server, e dopo aver ricevuto l'authorization code, lo scambia
con il token presentando il verifier originale. Anche se un attaccante intercetta il code durante
il redirect, non può scambiarlo senza il verifier che rimane segreto nel client.
// OAuth 2.1 PKCE flow - Client SPA TypeScript
interface PKCEChallenge {
codeVerifier: string;
codeChallenge: string;
state: string;
}
// Genera PKCE challenge e state anti-CSRF
async function generatePKCEChallenge(): Promise<PKCEChallenge> {
// code_verifier: stringa random 64 caratteri (range accettato: 43-128)
const codeVerifier = generateRandomString(64);
// code_challenge = BASE64URL(SHA256(ASCII(code_verifier)))
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const digest = await window.crypto.subtle.digest('SHA-256', data);
const codeChallenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
// state: token anti-CSRF univoco per questa richiesta di autorizzazione
const state = generateRandomString(32);
return { codeVerifier, codeChallenge, state };
}
function generateRandomString(length: number): string {
const array = new Uint8Array(length);
window.crypto.getRandomValues(array);
return Array.from(array, (byte) =>
byte.toString(36).padStart(2, '0')
).join('').slice(0, length);
}
// Avvia il flusso OAuth 2.1 con redirect all'authorization server
async function startOAuthFlow(): Promise<void> {
const { codeVerifier, codeChallenge, state } = await generatePKCEChallenge();
// Salva temporaneamente in sessionStorage (non localStorage)
// sessionStorage viene cancellato alla chiusura del tab: meno rischio di leak
sessionStorage.setItem('pkce_verifier', codeVerifier);
sessionStorage.setItem('oauth_state', state);
const params = new URLSearchParams({
response_type: 'code',
client_id: process.env['VITE_OAUTH_CLIENT_ID']!,
redirect_uri: `
#123;window.location.origin}/auth/callback`,
scope: 'openid profile email',
code_challenge: codeChallenge,
code_challenge_method: 'S256', // Sempre S256, mai 'plain'
state: state,
});
window.location.href = `#123;AUTH_SERVER_URL}/authorize?#123;params}`;
}
// Callback handler: scambia authorization code con access token
async function handleOAuthCallback(): Promise<void> {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const returnedState = params.get('state');
const error = params.get('error');
if (error) {
throw new Error(`OAuth error: #123;error}: #123;params.get('error_description')}`);
}
// Verifica state anti-CSRF prima di procedere
const savedState = sessionStorage.getItem('oauth_state');
if (!code || returnedState !== savedState) {
throw new Error('Invalid OAuth callback: state mismatch (possibile attacco CSRF)');
}
const codeVerifier = sessionStorage.getItem('pkce_verifier');
if (!codeVerifier) {
throw new Error('Missing PKCE verifier');
}
// Pulizia immediata dei dati temporanei
sessionStorage.removeItem('pkce_verifier');
sessionStorage.removeItem('oauth_state');
// Scambia authorization code con access token
const response = await fetch(`#123;AUTH_SERVER_URL}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env['VITE_OAUTH_CLIENT_ID']!,
redirect_uri: `#123;window.location.origin}/auth/callback`,
code: code,
code_verifier: codeVerifier, // Prova che siamo noi i legittimi richiedenti
}),
});
if (!response.ok) {
const body = await response.json();
throw new Error(`Token exchange failed: #123;body.error}`);
}
const tokens = await response.json();
// Passa i token al backend via endpoint sicuro per setting cookie HttpOnly
await securelyStoreTokens(tokens);
}
WebAuthn e Passkeys: L'Autenticazione Passwordless
Le passkeys basate su WebAuthn (Web Authentication API, W3C standard) rappresentano il cambiamento
più significativo nell'autenticazione degli ultimi vent'anni. A differenza delle password, le passkeys
usano crittografia a chiave pubblica asimmetrica: la chiave privata rimane sul dispositivo dell'utente
protetta dal biometrico o dal PIN del dispositivo, mentre il server conosce solo la chiave pubblica.
Il risultato e una resistenza assoluta al phishing (la chiave e crittograficamente
legata al dominio originale), nessuna password da rubare nei database breach, e UX superiore con
Face ID o Touch ID al posto di password + 2FA. Nel 2025, oltre 15 miliardi di account
supportano le passkeys. La libreria SimpleWebAuthn semplifica enormemente
l'implementazione.
L'autenticazione a più fattori (MFA) e la contromisura più efficace contro il credential stuffing e
il phishing classico basato su password. Secondo Microsoft, MFA blocca oltre il 99.9% degli
attacchi automatizzati agli account. TOTP (Time-based One-Time Password, RFC 6238) e lo
standard de facto per il secondo fattore software, compatibile con Google Authenticator, Authy,
1Password e qualsiasi app TOTP conforme.
Un aspetto critico spesso trascurato: il flow MFA deve essere atomico e non aggirabile.
Non creare mai una sessione parzialmente autenticata dopo il primo fattore che permetta di fare
qualsiasi operazione. Usa invece una sessione temporanea con un flag mfaPending: true,
e concedi accesso completo solo dopo la verifica del secondo fattore.
// MFA TOTP implementation - Node.js
// npm install otpauth qrcode
// NOTA: speakeasy non e più mantenuto dal 2017, usa 'otpauth' invece
import * as OTPAuth from 'otpauth';
import QRCode from 'qrcode';
import bcrypt from 'bcrypt';
import crypto from 'crypto';
// STEP 1: Abilita 2FA - genera secret e QR code per l'utente
app.post('/auth/2fa/setup', requireAuth, async (req, res) => {
const user = req.user!;
// Genera nuovo secret TOTP con randomness crittografico
const totp = new OTPAuth.TOTP({
issuer: 'MyApp',
label: user.email,
algorithm: 'SHA1', // SHA1 e lo standard TOTP (RFC 6238)
digits: 6,
period: 30, // 30 secondi per OTP (standard)
secret: OTPAuth.Secret.fromRandom(20), // 160 bit di entropia
});
// Salva secret temporaneo (non confermato ancora)
const secretBase32 = totp.secret.base32;
await savePending2FASecret(user.id, secretBase32);
// Genera QR code per l'app authenticator
const otpAuthUrl = totp.toString();
const qrCodeDataUrl = await QRCode.toDataURL(otpAuthUrl);
res.json({
secret: secretBase32, // Mostra per inserimento manuale
qrCode: qrCodeDataUrl, // URL del QR code (base64 data URL)
});
});
// STEP 2: Verifica e conferma attivazione 2FA
app.post('/auth/2fa/verify-setup', requireAuth, async (req, res) => {
const { token } = req.body;
const user = req.user!;
const pendingSecret = await getPending2FASecret(user.id);
if (!pendingSecret) {
return res.status(400).json({ error: '2FA setup not initiated' });
}
const totp = new OTPAuth.TOTP({
secret: OTPAuth.Secret.fromBase32(pendingSecret),
algorithm: 'SHA1',
digits: 6,
period: 30,
});
// Valida il codice con window=1 (accetta ±30 secondi per clock skew)
const delta = totp.validate({ token, window: 1 });
if (delta === null) {
return res.status(400).json({ error: 'Codice TOTP non valido' });
}
// Genera codici di recovery monouso
const recoveryCodes = generateRecoveryCodes(8);
const hashedCodes = await Promise.all(
recoveryCodes.map((code) => bcrypt.hash(code, 12))
);
// Attiva 2FA: salva secret definitivo e recovery codes hashati
await enable2FA(user.id, pendingSecret, hashedCodes);
await deletePending2FASecret(user.id);
// I recovery codes in chiaro vengono mostrati UNA SOLA VOLTA
res.json({ success: true, recoveryCodes });
});
// STEP 3: Login con 2FA - verifica secondo fattore
app.post('/auth/2fa/challenge', async (req, res) => {
const { token } = req.body;
// Verifica stato sessione: deve essere in pending MFA
if (!req.session.mfaPending || !req.session.pendingUserId) {
return res.status(401).json({ error: 'Invalid session state' });
}
const user = await getUserById(req.session.pendingUserId);
if (!user?.totpSecret) {
return res.status(400).json({ error: '2FA not configured' });
}
const totp = new OTPAuth.TOTP({
secret: OTPAuth.Secret.fromBase32(user.totpSecret),
algorithm: 'SHA1',
digits: 6,
period: 30,
});
const delta = totp.validate({ token, window: 1 });
if (delta === null) {
// Rate limiting: incrementa tentativi falliti per questo account
await incrementFailedMFAAttempts(user.id);
return res.status(401).json({ error: 'Codice 2FA non valido' });
}
// TOTP valido: promuovi a sessione completamente autenticata
await resetFailedMFAAttempts(user.id);
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.userId = user.id;
req.session.mfaPending = false;
req.session.pendingUserId = undefined;
res.json({ success: true });
});
});
// Genera recovery codes in formato leggibile (es. ABCD-EFGH-IJKL)
function generateRecoveryCodes(count: number): string[] {
return Array.from({ length: count }, () =>
Array.from({ length: 3 }, () =>
crypto.randomBytes(2).toString('hex').toUpperCase()
).join('-')
);
}
Autorizzazione: RBAC, ABAC e ReBAC
Autenticazione (chi sei?) e autorizzazione (cosa puoi fare?) sono concetti distinti ma spesso
implementati insieme nello stesso middleware, creando codice difficile da testare e mantenere.
Una volta verificata l'identità, il sistema deve decidere se l'utente ha il permesso per
l'operazione specifica richiesta. Esistono tre modelli principali:
RBAC (Role-Based Access Control) assegna permessi a ruoli predefiniti, e ruoli
agli utenti. E il modello più semplice da implementare e adatto alla maggior parte delle
applicazioni. Il limite e la rigidita: con molti tenant o risorse personali, il numero di ruoli
esplode.
ABAC (Attribute-Based Access Control) valuta attributi dell'utente
(ruolo, dipartimento, livello), della risorsa (proprietario, classificazione, tenant) e del
contesto (ora, IP, device). Più flessibile di RBAC ma più complesso da implementare e debuggare.
ReBAC (Relationship-Based Access Control) basa i permessi sulle relazioni
tra entità nel grafo dei dati (es. Google Zanzibar). Usato da Google Drive, GitHub, Notion.
Ideale per applicazioni con strutture gerarchiche di ownership.
// Middleware RBAC + ABAC ibrido - Node.js Express TypeScript
// Definizione dei permessi per ruolo
const ROLE_PERMISSIONS: Record<string, string[]> = {
admin: ['users:read', 'users:write', 'users:delete', 'posts:*'],
editor: ['posts:read', 'posts:write', 'posts:delete'],
viewer: ['posts:read', 'users:read'],
};
// RBAC Middleware: verifica permesso base del ruolo
function requirePermission(permission: string) {
return (req: Request, res: Response, next: NextFunction) => {
const userRole = req.user?.role;
if (!userRole) {
return res.status(401).json({ error: 'Not authenticated' });
}
const permissions = ROLE_PERMISSIONS[userRole] ?? [];
const hasPermission = permissions.some((p) => {
if (p.endsWith(':*')) {
return permission.startsWith(p.slice(0, -1));
}
return p === permission;
});
if (!hasPermission) {
return res.status(403).json({ error: 'Permessi insufficienti' });
}
next();
};
}
// ABAC: verifica proprietà della risorsa (ownership check)
function requireOwnership<T extends { authorId: string }>(
getResource: (id: string) => Promise<T | null>
) {
return async (req: Request, res: Response, next: NextFunction) => {
const resourceId = req.params.id;
const resource = await getResource(resourceId);
if (!resource) {
return res.status(404).json({ error: 'Risorsa non trovata' });
}
// Admin bypassa il check di proprietà per operazioni di supporto
if (req.user?.role === 'admin') {
req.resource = resource;
return next();
}
// Gli altri devono essere proprietari della risorsa
if (resource.authorId !== req.user?.id) {
return res.status(403).json({ error: 'Accesso negato' });
}
req.resource = resource;
next();
};
}
// Composizione: RBAC + ABAC applicati in sequenza sulla stessa route
app.put(
'/api/posts/:id',
authenticateJWT, // 1. Autenticazione: chi sei?
requirePermission('posts:write'), // 2. RBAC: il tuo ruolo può scrivere posts?
requireOwnership(getPostById), // 3. ABAC: sei il proprietario di questo post?
async (req: Request, res: Response) => {
// req.resource e il post verificato e disponibile
const updatedPost = await updatePost(req.params.id, req.body);
res.json(updatedPost);
}
);
// Esempio con Casbin per sistemi multi-tenant più complessi
// npm install casbin
import { newEnforcer } from 'casbin';
const enforcer = await newEnforcer(
'rbac_with_domains_model.conf',
'policy.csv'
);
// Verifica permesso con contesto di dominio/tenant
async function checkTenantPermission(
userId: string,
tenantId: string,
resource: string,
action: string
): Promise<boolean> {
return enforcer.enforce(userId, tenantId, resource, action);
}
Password Security: Hashing e NIST Guidelines 2025
Le linee guida NIST SP 800-63B (revisione 2024) hanno cambiato radicalmente l'approccio alle password.
Addio alla rotazione periodica obbligatoria (provoca in realtà password più deboli e prevedibili come
MyApp2024! che diventa MyApp2025!). Addio ai requisiti rigidi di caratteri
speciali (gli utenti usano pattern prevedibili come P@ssw0rd1!). Le nuove linee guida
si concentrano su: lunghezza minima 8 caratteri, lunghezza massima almeno 64 caratteri, confronto
con database di password compromesse, e nessun requisito di composizione obbligatorio.
bcrypt vs Argon2id: Quale Scegliere?
Argon2id (raccomandato per nuovi progetti): vincitore della Password Hashing Competition 2015. Resistente agli attacchi GPU (memory-hard) e side-channel. Parametri consigliati: memoryCost 64MB, timeCost 3, parallelism 4.
bcrypt (adatto per sistemi legacy): ancora sicuro con cost factor >= 12. Attenzione: tronca silenziosamente password più lunghe di 72 caratteri - usa sempre un pre-hash o un wrapper che gestisce questo limite.
scrypt: buona alternativa a Argon2, ma parametrizzazione più complessa e meno documentata per i developer.
MD5, SHA-1, SHA-256 senza salt: MAI usare per password. Sono funzioni di hash veloci, non funzioni di derivazione chiave. Un attaccante con una GPU moderna può verificare miliardi di hash al secondo.
// Password hashing sicuro con Argon2id + controllo HaveIBeenPwned
// npm install argon2
import argon2 from 'argon2';
const ARGON2_OPTIONS: argon2.Options = {
type: argon2.argon2id, // argon2id: bilanciamento ottimale sicurezza/performance
memoryCost: 64 * 1024, // 64 MB di RAM richiesta (difesa contro GPU farms)
timeCost: 3, // 3 iterazioni sequenziali
parallelism: 4, // 4 thread paralleli
saltLength: 32, // Salt da 32 byte (generato automaticamente da argon2)
};
// Hash password durante registrazione
async function hashPassword(password: string): Promise<string> {
if (password.length < 8) {
throw new Error('Password troppo corta (minimo 8 caratteri)');
}
if (password.length > 128) {
throw new Error('Password troppo lunga (massimo 128 caratteri)');
}
// Verifica contro HaveIBeenPwned prima di salvare
const isPwned = await checkHaveIBeenPwned(password);
if (isPwned) {
throw new Error(
'Questa password e stata trovata in un data breach. Scegli una password diversa.'
);
}
return argon2.hash(password, ARGON2_OPTIONS);
}
// Verifica password durante login (timing-safe grazie ad argon2.verify)
async function verifyPassword(
plainPassword: string,
hashedPassword: string
): Promise<boolean> {
try {
return await argon2.verify(hashedPassword, plainPassword);
} catch {
return false;
}
}
// Controllo HaveIBeenPwned con k-anonymity
// Non invia la password intera: solo i primi 5 caratteri dello SHA-1 hash
async function checkHaveIBeenPwned(password: string): Promise<boolean> {
const encoder = new TextEncoder();
const data = encoder.encode(password);
const hashBuffer = await crypto.subtle.digest('SHA-1', data);
const hashHex = Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
.toUpperCase();
// Invia solo i primi 5 caratteri dell'hash (k-anonymity model)
const prefix = hashHex.slice(0, 5);
const suffix = hashHex.slice(5);
const response = await fetch(
`https://api.pwnedpasswords.com/range/#123;prefix}`,
{ headers: { 'Add-Padding': 'true' } } // Padding contro traffic analysis
);
const text = await response.text();
// Verifica se il nostro suffisso e nel risultato (>0 occorrenze)
return text.split('\n').some((line) =>
line.split(':')[0].trim() === suffix
);
}
Auth Stack Completo: Rate Limiting e Audit Log
Un'implementazione production-ready dell'autenticazione deve includere rate limiting per prevenire
brute force, account lockout con notifica all'utente, e audit logging per conformità normativa
e incident response. Ecco il pattern completo che integra tutti i layer:
// Login completo con tutti i layer di sicurezza - Express.js
// npm install express-rate-limit rate-limit-redis ioredis
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
// Rate limiter per endpoint di login (brute force protection a livello IP)
export const loginRateLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // Finestra di 15 minuti
max: 5, // Max 5 tentativi per IP per finestra
standardHeaders: true,
legacyHeaders: false,
store: new RedisStore({
sendCommand: (...args: string[]) => (redis as any).call(...args),
}),
handler: (req, res) => {
res.status(429).json({
error: 'Troppi tentativi di login. Riprova tra 15 minuti.',
retryAfter: res.getHeader('Retry-After'),
});
},
});
// Account lockout (distinto dal rate limiting per IP: blocca l'account specifico)
async function checkAccountLockout(email: string): Promise<void> {
const key = `lockout:#123;email}`;
const attempts = await redis.get(key);
if (attempts && parseInt(attempts) >= 10) {
throw new Error('ACCOUNT_LOCKED');
}
}
async function recordFailedLogin(email: string): Promise<void> {
const key = `lockout:#123;email}`;
const pipeline = redis.pipeline();
pipeline.incr(key);
pipeline.expire(key, 30 * 60); // Reset dopo 30 minuti
await pipeline.exec();
}
// Audit log per conformità e incident response
async function logAuthEvent(event: {
userId?: string;
email: string;
action: 'LOGIN' | 'LOGOUT' | 'REGISTER' | 'PASSWORD_CHANGE' | 'MFA_VERIFY';
success: boolean;
ip: string;
userAgent: string;
}): Promise<void> {
await db.query(
`INSERT INTO auth_audit_log
(user_id, email, action, success, ip_address, user_agent, created_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW())`,
[event.userId, event.email, event.action, event.success, event.ip, event.userAgent]
);
}
// Endpoint di login: combina tutti i layer
app.post('/auth/login', loginRateLimiter, async (req: Request, res: Response) => {
const { email, password } = req.body;
const ip = req.ip ?? 'unknown';
const userAgent = req.get('User-Agent') ?? 'unknown';
if (!email || !password || typeof email !== 'string') {
return res.status(400).json({ error: 'Email e password richiesti' });
}
try {
await checkAccountLockout(email);
const user = await getUserByEmail(email.toLowerCase().trim());
// CRITICO: verifica la password SEMPRE, anche se l'utente non esiste
// Previene timing attack che rivelano se un utente e registrato o no
const passwordValid = user
? await verifyPassword(password, user.passwordHash)
: await verifyPassword(password, '$argon2id$v=19$m=65536,t=3,p=4$fake$fake');
if (!user || !passwordValid) {
await recordFailedLogin(email);
await logAuthEvent({ email, action: 'LOGIN', success: false, ip, userAgent });
// Risposta generica: non rivelare se l'utente esiste
return res.status(401).json({ error: 'Credenziali non valide' });
}
if (user.mfaEnabled) {
// Sessione parziale: blocca fino al completamento del secondo fattore
req.session.mfaPending = true;
req.session.pendingUserId = user.id;
return res.json({ mfaRequired: true });
}
// Login completo (senza MFA)
req.session.regenerate((err) => {
if (err) return res.status(500).json({ error: 'Session error' });
req.session.userId = user.id;
req.session.userRole = user.role;
logAuthEvent({ userId: user.id, email, action: 'LOGIN', success: true, ip, userAgent });
res.json({ success: true });
});
} catch (err) {
if ((err as Error).message === 'ACCOUNT_LOCKED') {
return res.status(429).json({
error: 'Account temporaneamente bloccato. Contatta il supporto o riprova tra 30 minuti.',
});
}
console.error('Login error:', err);
res.status(500).json({ error: 'Errore interno del server' });
}
});
Angular: Guard, Interceptor e Sicurezza Lato Client
Quando implementi l'autenticazione in un'applicazione Angular, la distinzione fondamentale e:
i route guard proteggono la navigazione UI, non la sicurezza dei dati.
Un utente malintenzionato può sempre aggirare Angular modificando l'URL o chiamando le API
direttamente da strumenti come curl o Postman. La sicurezza reale risiede sempre e solo nel backend.
Route Guards non Sono Sicurezza
I CanActivate guard di Angular proteggono la navigazione nell'UI, ma non impediscono
l'accesso diretto alle API. Ogni endpoint API deve autenticare e autorizzare in modo
completamente indipendente dal frontend. I guard servono solo per mostrare la pagina
di login all'utente invece della dashboard, non per proteggere i dati.
// auth.guard.ts - Guard per navigazione UI (non sicurezza API)
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';
import { map, take } from 'rxjs/operators';
export const authGuard: CanActivateFn = (route, state) => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.isAuthenticated$.pipe(
take(1),
map((isAuthenticated) => {
if (isAuthenticated) {
return true;
}
// Salva URL per redirect post-login
return router.createUrlTree(['/login'], {
queryParams: { returnUrl: state.url }
});
})
);
};
// auth.interceptor.ts - CSRF token e refresh automatico
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, switchMap, throwError } from 'rxjs';
import { AuthService } from './auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
// Aggiunge CSRF token per richieste mutative (POST/PUT/PATCH/DELETE)
const csrfToken = getCookie('XSRF-TOKEN');
const modifiedReq = csrfToken && isStatefulMethod(req.method)
? req.clone({
headers: req.headers.set('X-XSRF-TOKEN', csrfToken),
withCredentials: true, // Invia cookie di sessione con le richieste
})
: req.clone({ withCredentials: true });
return next(modifiedReq).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Prova refresh automatico del token
return auth.refreshToken().pipe(
switchMap(() => next(modifiedReq)),
catchError((refreshError) => {
// Refresh fallito: forza logout e redirect al login
auth.logout();
return throwError(() => refreshError);
})
);
}
return throwError(() => error);
})
);
};
function isStatefulMethod(method: string): boolean {
return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method.toUpperCase());
}
function getCookie(name: string): string | null {
const match = document.cookie.match(new RegExp(`(^| )#123;name}=([^;]+)`));
return match ? decodeURIComponent(match[2]) : null;
}
Checklist OWASP A07: Identification and Authentication Failures
OWASP A07:2021 (Identification and Authentication Failures) scala dalla quarta alla settima posizione
rispetto al 2017, grazie alla maggiore adozione di MFA e password manager. Tuttavia rimane critico:
la seguente checklist riassume i controlli minimi per essere conformi alle best practice OWASP 2025.
Ricorda: il 45% del codice AI-generated fallisce i security test sull'autenticazione,
verifica sempre il codice generato da strumenti come GitHub Copilot o ChatGPT.
Checklist Autenticazione Sicura (OWASP 2025)
Hashing password: Argon2id o bcrypt (cost factor >= 12), mai MD5/SHA semplici
Password policy: Lunghezza minima 8, massima almeno 64 caratteri, confronto HaveIBeenPwned
Session fixation: Rigenera session ID dopo ogni login con req.session.regenerate()
Cookie flags: HttpOnly + Secure + SameSite su tutti i cookie di sessione e auth
Logout: Distruggi la sessione lato server, non solo cancellare il cookie
Session timeout: Scadenza per inattivita (30-60 min per dati sensibili)
JWT algoritmo: Specifica whitelist esplicita, blocca "none" e algoritmi non attesi
JWT storage: Cookie HttpOnly, mai localStorage o sessionStorage
Brute force: Rate limiting per IP + account lockout con notifica utente
User enumeration: Risposta generica per credenziali errate, confronto timing-safe
MFA: TOTP disponibile per tutti, obbligatorio per ruoli admin/privilegiati
OAuth 2.1: PKCE obbligatorio, no implicit flow, no password grant
Passkeys: Considera come alternativa passwordless per nuovi progetti
Audit log: Log di tutti gli eventi auth con IP, user agent e timestamp
Angular guards: Solo per UX, sicurezza reale sempre e solo nel backend
CSRF: Token CSRF per SPA che usano cookie di sessione cross-origin
Conclusioni
L'autenticazione sicura nel 2025 richiede una strategia a più livelli: password hashing forte con
Argon2id, session management corretto con cookie HttpOnly+Secure+SameSite, rate limiting e account
lockout contro il brute force, MFA con TOTP per tutti gli utenti privilegiati, e OAuth 2.1 con PKCE
per l'autenticazione federata. Ogni layer mitiga una categoria specifica di attacchi; saltarne anche
uno solo apre finestre di vulnerabilità reali.
Il cambiamento più significativo per i prossimi anni e la transizione alle passkeys:
resistenti al phishing per design, senza password da ricordare o da hashare nel database, con UX
superiore grazie a Face ID e Touch ID. Se stai costruendo una nuova applicazione nel 2025, valuta
seriamente le passkeys come primo fattore di autenticazione.
Infine, una nota critica sul codice AI-generated: il 45% del codice di autenticazione generato da
strumenti AI fallisce i security test. I problemi più comuni sono sessioni senza regenerate dopo
il login, JWT memorizzati in localStorage, mancanza di timing-safe comparison, e cookie senza
i flag di sicurezza corretti. Usa sempre questa checklist OWASP come review finale del codice,
indipendentemente da chi (o cosa) lo ha scritto.