Autenticazione JWT con backend centralizzato
Nei miei progetti ho spesso bisogno della stessa infrastruttura di autenticazione: registrazione, login, verifica email, reimpostazione della password, gestione dei token. Invece di reimplementarla ogni volta, ho costruito un backend di autenticazione centralizzato che serve tutte le mie applicazioni.
Il problema: l'autenticazione ridondante
Ogni progetto Symfony/React richiedeva:
- Un sistema di login/register
- La gestione dei token JWT
- La verifica delle email
- La reimpostazione delle password
- La protezione delle route
- Il refresh token
Risultato: codice duplicato, potenziali vulnerabilità e manutenzione complessa.
La soluzione: un backend centralizzato
Ho creato un'API di autenticazione unica che gestisce tutto. Tutte le mie applicazioni (portfolio, baccalaureat.sn, SyllVS) vi si connettono.
L'architettura lato backend
1. Il JWT Authenticator
Il cuore della sicurezza: un authenticator Symfony che valida ogni richiesta.
// src/Security/JwtAuthenticator.php
class JwtAuthenticator extends AbstractAuthenticator
{
public function __construct(
private readonly UserRepository $userRepository,
private readonly string $jwtSecret,
) {}
public function authenticate(Request $request): SelfValidatingPassport
{
$authHeader = $request->headers->get('Authorization');
if (!str_starts_with($authHeader, 'Bearer ')) {
throw new CustomUserMessageAuthenticationException('Missing token');
}
$jwt = trim(substr($authHeader, 7));
try {
$payload = JWT::decode($jwt, new Key($this->jwtSecret, 'HS256'));
} catch (Throwable $e) {
throw new CustomUserMessageAuthenticationException('Invalid JWT token');
}
if ($payload->exp < time()) {
throw new CustomUserMessageAuthenticationException('Token expired');
}
$user = $this->userRepository->findOneBy(['email' => $payload->email]);
if (!$user) {
throw new CustomUserMessageAuthenticationException('User not found');
}
if ($user->isBlocked()) {
throw new CustomUserMessageAuthenticationException('Account disabled');
}
return new SelfValidatingPassport(
new UserBadge($payload->email, fn () => $user)
);
}
}
2. Il TokenService
// src/Service/TokenService.php
class TokenService
{
public function generateToken(User $user, int $ttl = 3600): string
{
$payload = [
'email' => $user->getEmail(),
'roles' => $user->getRoles(),
'exp' => time() + $ttl,
'iat' => time(),
'jti' => bin2hex(random_bytes(16)),
];
return JWT::encode($payload, $this->jwtSecret, 'HS256');
}
public function getUserFromToken(Request $request): ?User
{
$authHeader = $request->headers->get('Authorization');
if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) {
return null;
}
$jwt = trim(substr($authHeader, 7));
try {
$payload = JWT::decode($jwt, new Key($this->jwtSecret, 'HS256'));
return $this->userRepository->findOneBy(['email' => $payload->email]);
} catch (Throwable $e) {
return null;
}
}
public function refreshToken(Request $request): ?string
{
$user = $this->getUserFromToken($request);
if (!$user) return null;
return $this->generateToken($user);
}
}
3. Il controller di autenticazione
// src/Controller/Security/AuthController.php
#[Route('/api/security', name: 'security_')]
class AuthController extends AbstractController
{
#[Route('/login', name: 'login', methods: ['POST'])]
public function login(
Request $request,
UserPasswordHasherInterface $hasher,
TokenService $tokenService
): JsonResponse {
$data = json_decode($request->getContent(), true);
$user = $this->userRepository->findOneBy(['email' => $data['email']]);
if (!$user || !$hasher->isPasswordValid($user, $data['password'])) {
return new JsonResponse([
'success' => false,
'error' => 'Invalid credentials'
], 401);
}
$token = $tokenService->generateToken($user);
return new JsonResponse([
'success' => true,
'token' => $token,
'token_type' => 'Bearer',
'expires_in' => 3600,
'user' => [
'id' => $user->getId(),
'email' => $user->getEmail(),
'firstname' => $user->getPrenom(),
'lastname' => $user->getNom(),
'roles' => $user->getRoles(),
'isVerified' => $user->isVerified()
]
]);
}
#[Route('/refresh-token', name: 'refresh_token', methods: ['POST'])]
public function refreshToken(Request $request, TokenService $tokenService): JsonResponse
{
try {
$newToken = $tokenService->refreshToken($request);
if (!$newToken) {
return new JsonResponse([
'success' => false,
'error' => 'Unable to refresh token'
], 401);
}
return new JsonResponse([
'success' => true,
'token' => $newToken,
'expires_in' => 3600
]);
} catch (Exception $e) {
return new JsonResponse([
'success' => false,
'error' => 'Token refresh failed'
], 401);
}
}
}
Lato frontend: l'integrazione Redux
L'interceptor Axios con refresh automatico
// store/slices/authSlice.js
let isRefreshing = false;
let failedQueue = [];
const processQueue = (error, token = null) => {
failedQueue.forEach(prom => {
if (error) prom.reject(error);
else prom.resolve(token);
});
failedQueue = [];
};
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
.then(token => {
originalRequest.headers.Authorization = `Bearer ${token}`;
return api(originalRequest);
})
.catch(err => Promise.reject(err));
}
originalRequest._retry = true;
isRefreshing = true;
try {
const token = localStorage.getItem('token');
if (!token) throw new Error('No token');
const response = await axios.post(`${API_URL}/user/refresh-token`, {}, {
headers: { Authorization: `Bearer ${token}` }
});
if (response.data.success) {
const newToken = response.data.token;
localStorage.setItem('token', newToken);
processQueue(null, newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return api(originalRequest);
}
} catch (refreshError) {
processQueue(refreshError, null);
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/auth';
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
I vantaggi di questa architettura
- ✅ Centralizzazione: un unico database utenti
- ✅ Sicurezza: un'unica chiave JWT da proteggere
- ✅ Manutenzione: gli aggiornamenti di sicurezza vengono fatti una sola volta
- ✅ Esperienza utente: l'utente è connesso ovunque
- ✅ Monitoring: i log di connessione sono centralizzati
- ✅ Scalabilità: il backend può servire migliaia di applicazioni
- ✅ Refresh automatico: l'interceptor gestisce tutto
Cosa ne ricavo
- Centralizzare l'autenticazione non significa centralizzare tutti i dati utenti
- Il refresh token con coda evita le chiamate multiple in parallelo
- Il blocco dopo 5 tentativi è efficace contro il brute force
- I log di connessione sono essenziali per il monitoring