src/EventListener/JWTAuthenticationSuccessListener.php line 21

Open in your IDE?
  1. <?php
  2. namespace App\EventListener;
  3. use App\Application\Abonnent\DeviceService;
  4. use App\Entity\Abonnent\Abonnent;
  5. use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent;
  6. use Psr\Log\LoggerInterface;
  7. use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
  8. use Symfony\Component\HttpFoundation\RequestStack;
  9. #[AsEventListener(event: 'lexik_jwt_authentication.on_authentication_success', method: 'onAuthenticationSuccess')]
  10. class JWTAuthenticationSuccessListener
  11. {
  12. public function __construct(
  13. private RequestStack $requestStack,
  14. private LoggerInterface $logger,
  15. private DeviceService $deviceService
  16. ) {}
  17. public function onAuthenticationSuccess(AuthenticationSuccessEvent $event): void
  18. {
  19. $request = $this->requestStack->getCurrentRequest();
  20. if (!$request) {
  21. return;
  22. }
  23. $data = $event->getData();
  24. $user = $event->getUser();
  25. if (!$user instanceof Abonnent) {
  26. return;
  27. }
  28. $path = $request->getPathInfo() ?? '';
  29. $isLoginPath = str_starts_with($path, '/api/login');
  30. $isRefreshPath = str_starts_with($path, '/api/token/refresh');
  31. $this->logger->info('JWT Authentication Success triggered', [
  32. 'path' => $path,
  33. 'isLoginPath' => $isLoginPath,
  34. 'isRefreshPath' => $isRefreshPath,
  35. 'user' => $user->getUserIdentifier()
  36. ]);
  37. if ($isLoginPath) {
  38. $this->handleLogin($request, $event, $data, $user);
  39. } elseif ($isRefreshPath) {
  40. $this->handleRefresh($request, $event, $data, $user);
  41. }
  42. }
  43. private function handleLogin($request, $event, $data, $user): void
  44. {
  45. // Add info about OTHER device being logged out (existing functionality enhanced)
  46. $loggedOutDeviceInfo = $request->attributes->get('logged_out_device');
  47. if ($loggedOutDeviceInfo) {
  48. $data['logout_info'] = $loggedOutDeviceInfo;
  49. }
  50. // Create device-specific refresh token
  51. try {
  52. $deviceId = $this->deviceService->getDeviceIdFromRequest($request);
  53. // Create a new device-specific refresh token
  54. $deviceRefreshToken = $this->deviceService->createDeviceSpecificRefreshToken($user, $deviceId);
  55. // Replace the default refresh token with our device-specific one
  56. $data['refresh_token'] = $deviceRefreshToken;
  57. $this->logger->info('Created device-specific refresh token for login', [
  58. 'user' => $user->getUserIdentifier(),
  59. 'deviceId' => substr($deviceId, 0, 8) . '...',
  60. 'token_preview' => substr($deviceRefreshToken, 0, 8) . '...'
  61. ]);
  62. } catch (\Exception $e) {
  63. $this->logger->error('Failed to create device-specific refresh token', [
  64. 'user' => $user->getUserIdentifier(),
  65. 'error' => $e->getMessage()
  66. ]);
  67. }
  68. $event->setData($data);
  69. }
  70. private function handleRefresh($request, $event, $data, $user): void
  71. {
  72. $this->logger->info('handleRefresh called', [
  73. 'user' => $user->getUserIdentifier(),
  74. 'data_keys' => array_keys($data)
  75. ]);
  76. try {
  77. // Get the old refresh token from the request
  78. $oldRefreshToken = $this->getRefreshTokenFromRequest($request);
  79. $this->logger->info('Refresh token details', [
  80. 'user' => $user->getUserIdentifier(),
  81. 'oldRefreshToken' => $oldRefreshToken ? substr($oldRefreshToken, 0, 8) . '...' : 'null',
  82. 'has_new_token_in_data' => isset($data['refresh_token']),
  83. 'new_token_preview' => isset($data['refresh_token']) ? substr($data['refresh_token'], 0, 8) . '...' : 'null'
  84. ]);
  85. if ($oldRefreshToken && isset($data['refresh_token'])) {
  86. // Find which device this belongs to and update it with the new token
  87. $device = $this->deviceService->findDeviceByRefreshToken($user, $oldRefreshToken);
  88. if ($device) {
  89. $this->logger->info('Found device for refresh token', [
  90. 'user' => $user->getUserIdentifier(),
  91. 'deviceId' => substr($device->getDeviceId(), 0, 8) . '...'
  92. ]);
  93. // Update device with the new refresh token (Gesdinet already rotated it)
  94. $device->setRefreshToken($data['refresh_token']);
  95. $device->setLastActivity(new \DateTime());
  96. // Persist the change
  97. $this->deviceService->persistDeviceUpdate($device);
  98. $this->logger->info('Updated device with new refresh token', [
  99. 'user' => $user->getUserIdentifier(),
  100. 'deviceId' => substr($device->getDeviceId(), 0, 8) . '...'
  101. ]);
  102. } else {
  103. $this->logger->warning('No device found for refresh token', [
  104. 'user' => $user->getUserIdentifier(),
  105. 'oldRefreshToken' => substr($oldRefreshToken, 0, 8) . '...'
  106. ]);
  107. }
  108. } else {
  109. $this->logger->warning('Missing refresh token data', [
  110. 'user' => $user->getUserIdentifier(),
  111. 'has_old_token' => $oldRefreshToken !== null,
  112. 'has_new_token' => isset($data['refresh_token'])
  113. ]);
  114. }
  115. } catch (\Exception $e) {
  116. $this->logger->error('Failed to handle refresh token rotation', [
  117. 'user' => $user->getUserIdentifier(),
  118. 'error' => $e->getMessage(),
  119. 'trace' => $e->getTraceAsString()
  120. ]);
  121. }
  122. $event->setData($data);
  123. }
  124. private function getRefreshTokenFromRequest($request): ?string
  125. {
  126. $content = $request->getContent();
  127. if ($content) {
  128. $data = json_decode($content, true);
  129. if (isset($data['refresh_token'])) {
  130. return $data['refresh_token'];
  131. }
  132. }
  133. return $request->request->get('refresh_token') ?? $request->query->get('refresh_token');
  134. }
  135. }