<?php
namespace App\EventListener;
use App\Application\Abonnent\DeviceService;
use App\Entity\Abonnent\Abonnent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\RequestStack;
#[AsEventListener(event: 'lexik_jwt_authentication.on_authentication_success', method: 'onAuthenticationSuccess')]
class JWTAuthenticationSuccessListener
{
public function __construct(
private RequestStack $requestStack,
private LoggerInterface $logger,
private DeviceService $deviceService
) {}
public function onAuthenticationSuccess(AuthenticationSuccessEvent $event): void
{
$request = $this->requestStack->getCurrentRequest();
if (!$request) {
return;
}
$data = $event->getData();
$user = $event->getUser();
if (!$user instanceof Abonnent) {
return;
}
$path = $request->getPathInfo() ?? '';
$isLoginPath = str_starts_with($path, '/api/login');
$isRefreshPath = str_starts_with($path, '/api/token/refresh');
$this->logger->info('JWT Authentication Success triggered', [
'path' => $path,
'isLoginPath' => $isLoginPath,
'isRefreshPath' => $isRefreshPath,
'user' => $user->getUserIdentifier()
]);
if ($isLoginPath) {
$this->handleLogin($request, $event, $data, $user);
} elseif ($isRefreshPath) {
$this->handleRefresh($request, $event, $data, $user);
}
}
private function handleLogin($request, $event, $data, $user): void
{
// Add info about OTHER device being logged out (existing functionality enhanced)
$loggedOutDeviceInfo = $request->attributes->get('logged_out_device');
if ($loggedOutDeviceInfo) {
$data['logout_info'] = $loggedOutDeviceInfo;
}
// Create device-specific refresh token
try {
$deviceId = $this->deviceService->getDeviceIdFromRequest($request);
// Create a new device-specific refresh token
$deviceRefreshToken = $this->deviceService->createDeviceSpecificRefreshToken($user, $deviceId);
// Replace the default refresh token with our device-specific one
$data['refresh_token'] = $deviceRefreshToken;
$this->logger->info('Created device-specific refresh token for login', [
'user' => $user->getUserIdentifier(),
'deviceId' => substr($deviceId, 0, 8) . '...',
'token_preview' => substr($deviceRefreshToken, 0, 8) . '...'
]);
} catch (\Exception $e) {
$this->logger->error('Failed to create device-specific refresh token', [
'user' => $user->getUserIdentifier(),
'error' => $e->getMessage()
]);
}
$event->setData($data);
}
private function handleRefresh($request, $event, $data, $user): void
{
$this->logger->info('handleRefresh called', [
'user' => $user->getUserIdentifier(),
'data_keys' => array_keys($data)
]);
try {
// Get the old refresh token from the request
$oldRefreshToken = $this->getRefreshTokenFromRequest($request);
$this->logger->info('Refresh token details', [
'user' => $user->getUserIdentifier(),
'oldRefreshToken' => $oldRefreshToken ? substr($oldRefreshToken, 0, 8) . '...' : 'null',
'has_new_token_in_data' => isset($data['refresh_token']),
'new_token_preview' => isset($data['refresh_token']) ? substr($data['refresh_token'], 0, 8) . '...' : 'null'
]);
if ($oldRefreshToken && isset($data['refresh_token'])) {
// Find which device this belongs to and update it with the new token
$device = $this->deviceService->findDeviceByRefreshToken($user, $oldRefreshToken);
if ($device) {
$this->logger->info('Found device for refresh token', [
'user' => $user->getUserIdentifier(),
'deviceId' => substr($device->getDeviceId(), 0, 8) . '...'
]);
// Update device with the new refresh token (Gesdinet already rotated it)
$device->setRefreshToken($data['refresh_token']);
$device->setLastActivity(new \DateTime());
// Persist the change
$this->deviceService->persistDeviceUpdate($device);
$this->logger->info('Updated device with new refresh token', [
'user' => $user->getUserIdentifier(),
'deviceId' => substr($device->getDeviceId(), 0, 8) . '...'
]);
} else {
$this->logger->warning('No device found for refresh token', [
'user' => $user->getUserIdentifier(),
'oldRefreshToken' => substr($oldRefreshToken, 0, 8) . '...'
]);
}
} else {
$this->logger->warning('Missing refresh token data', [
'user' => $user->getUserIdentifier(),
'has_old_token' => $oldRefreshToken !== null,
'has_new_token' => isset($data['refresh_token'])
]);
}
} catch (\Exception $e) {
$this->logger->error('Failed to handle refresh token rotation', [
'user' => $user->getUserIdentifier(),
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
$event->setData($data);
}
private function getRefreshTokenFromRequest($request): ?string
{
$content = $request->getContent();
if ($content) {
$data = json_decode($content, true);
if (isset($data['refresh_token'])) {
return $data['refresh_token'];
}
}
return $request->request->get('refresh_token') ?? $request->query->get('refresh_token');
}
}