src/EventSubscriber/SecurityEventSubscriber.php line 236

Open in your IDE?
  1. <?php
  2. namespace MedBrief\MSR\EventSubscriber;
  3. use DateTime;
  4. use DeviceDetector\DeviceDetector;
  5. use Doctrine\ORM\EntityManagerInterface;
  6. use GeoIp2\Database\Reader;
  7. use MedBrief\MSR\Entity\AuditRecord\User as UserAuditRecord;
  8. use MedBrief\MSR\Entity\Firm;
  9. use MedBrief\MSR\Entity\User;
  10. use MedBrief\MSR\Entity\UserLogin;
  11. use MedBrief\MSR\Event\AuditRecordEvent;
  12. use MedBrief\MSR\Factory\AuditRecordEventFactory;
  13. use MedBrief\MSR\Factory\AuditRecordFactory;
  14. use MedBrief\MSR\Security\Exception\GenericOAuthException;
  15. use MedBrief\MSR\Service\EntityHelper\UserHelper;
  16. use MedBrief\MSR\Service\Notification\Generators\UserLoginNotification;
  17. use Override;
  18. use Ramsey\Uuid\Uuid;
  19. use function Sentry\captureException;
  20. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  21. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  22. use Symfony\Component\HttpFoundation\Cookie;
  23. use Symfony\Component\HttpFoundation\RequestStack;
  24. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  25. use Symfony\Component\HttpKernel\KernelEvents;
  26. use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
  27. use Symfony\Component\Security\Core\AuthenticationEvents;
  28. use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent;
  29. use Symfony\Component\Security\Core\User\UserInterface;
  30. use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
  31. use Symfony\Component\Security\Http\Event\SwitchUserEvent;
  32. use Symfony\Component\Security\Http\SecurityEvents;
  33. use Throwable;
  34. class SecurityEventSubscriber implements EventSubscriberInterface
  35. {
  36. public const LOGIN_NEW_IP = 'ip';
  37. public const LOGIN_NEW_DEVICE = 'device';
  38. public function __construct(protected EntityManagerInterface $entityManager, protected Reader $reader, protected UserLoginNotification $loginNotificationGenerator, protected UserHelper $userHelper, protected RequestStack $request, protected EventDispatcherInterface $eventDispatcher)
  39. {
  40. }
  41. /**
  42. * @inheritDoc
  43. */
  44. #[Override]
  45. public static function getSubscribedEvents(): array
  46. {
  47. return [
  48. LoginSuccessEvent::class => [
  49. ['logUserLogin', 0],
  50. ],
  51. SecurityEvents::SWITCH_USER => [
  52. ['logUserSimulation', 0],
  53. ],
  54. AuthenticationEvents::AUTHENTICATION_FAILURE => [
  55. ['logFailedUserLogin', 0],
  56. ],
  57. KernelEvents::RESPONSE => [
  58. ['setDeviceId', 50],
  59. ],
  60. ];
  61. }
  62. /**
  63. *
  64. *
  65. *
  66. * @param AuthenticationFailureEvent $event
  67. *
  68. * @throws \Exception
  69. */
  70. public function logFailedUserLogin(AuthenticationFailureEvent $event): void
  71. {
  72. // Because OAuth exceptions cannot be traced to a specific user, skip them
  73. if (!$event->getAuthenticationException() instanceof GenericOAuthException) {
  74. // Grab the user account that the failure was logged for
  75. $email = $this->request->getMasterRequest()->get('username');
  76. $user = $this->entityManager->getRepository(User::class)
  77. ->findOneBy(['email' => $email])
  78. ;
  79. // If we actually have a user
  80. if ($user instanceof UserInterface) {
  81. // Grab the user's IP address
  82. $ipAddress = $this->request->getMasterRequest()->getClientIp();
  83. // Grab the user's browser
  84. $dd = new DeviceDetector($this->request->getMasterRequest()->headers->get('User-Agent'));
  85. $dd->parse();
  86. $browser = $dd->getClient();
  87. $os = $dd->getOs();
  88. // Get the user's location
  89. try {
  90. $record = $this->reader->city($ipAddress);
  91. // City, County/State, Country
  92. $location = sprintf('%s, %s, %s', $record->city->name, $record->mostSpecificSubdivision->name, $record->country->name);
  93. } catch (Throwable $exception) {
  94. captureException($exception);
  95. $location = 'Unknown Location';
  96. }
  97. // Create a UserLogin object
  98. $login = new UserLogin($user, $ipAddress, $browser, $os, $location, false);
  99. $this->entityManager->persist($login);
  100. $this->entityManager->flush();
  101. }
  102. }
  103. }
  104. /**
  105. * Logs a user's login (login success event) and persists the details to the database to ensure an auditable track
  106. * of IP addresses they accessed from as well as what platform and browser, including emailing users when a new
  107. * login is detected.
  108. *
  109. *
  110. *
  111. * @todo: Correctly log DocSorter logins rather than bypassing them
  112. *
  113. * @param LoginSuccessEvent $loginEvent
  114. *
  115. * @throws \Exception
  116. */
  117. public function logUserLogin(LoginSuccessEvent $loginEvent): void
  118. {
  119. // Grab the path and make sure we are not doing this check on anything but login events
  120. $path = $loginEvent->getRequest()->getRequestUri();
  121. // Putting this logic back for now, until we have time to prevent the interactive login happening on every DocSorter request.
  122. if (preg_match("/^\/api\/sort\/(?!login).*$/", $path)) {
  123. return;
  124. }
  125. // Grab the User
  126. /** @var User $user */
  127. $user = $loginEvent->getUser();
  128. if ($user instanceof Firm) {
  129. // Firms do not get logged here (at the moment)
  130. return;
  131. }
  132. // Grab the device identifier cookie, if it exists
  133. $deviceId = $loginEvent->getRequest()->cookies->get('msr_dev_id');
  134. if ($deviceId !== null) {
  135. $deviceId = Uuid::fromString($deviceId);
  136. }
  137. // Grab the user's IP address
  138. $ipAddress = $loginEvent->getRequest()->getClientIp();
  139. // Grab the user's browser
  140. $dd = new DeviceDetector($loginEvent->getRequest()->headers->get('User-Agent'));
  141. $dd->parse();
  142. $browser = $dd->getClient();
  143. $os = $dd->getOs();
  144. // Get the user's location
  145. try {
  146. $record = $this->reader->city($ipAddress);
  147. // City, County/State, Country
  148. $location = sprintf('%s, %s, %s', $record->city->name, $record->mostSpecificSubdivision->name, $record->country->name);
  149. } catch (Throwable $exception) {
  150. captureException($exception);
  151. $location = 'Unknown Location';
  152. }
  153. // Create a UserLogin object
  154. $login = new UserLogin($user, $ipAddress, $browser, $os, $location, true, $deviceId);
  155. // Set the device ID in the attributes to persist it to cookies
  156. $loginEvent->getRequest()->attributes->set('msr_dev_id', $login->getDevice()->toString());
  157. // First time user has logged in. Set firstLoginDate
  158. if ($user->getFirstLoginDate() === null) {
  159. $user->setFirstLoginDate(new DateTime('NOW'));
  160. }
  161. // Set the last login date for the user as well
  162. $user->setLastLogin(new DateTime('NOW'));
  163. // This user has never logged in before or we can't match so...
  164. // Register this login
  165. $this->entityManager->persist($user);
  166. $this->entityManager->persist($login);
  167. $this->entityManager->flush();
  168. // ... and email the user
  169. // @TODO: This caused issues with clients who were spammed.
  170. // $this->sendAlertNotification($login, self::LOGIN_NEW_DEVICE);
  171. }
  172. /**
  173. * Logs the simulation of users
  174. *
  175. * @param SwitchUserEvent $event
  176. */
  177. public function logUserSimulation(SwitchUserEvent $event): void
  178. {
  179. $token = $event->getToken();
  180. if ($token instanceof SwitchUserToken) {
  181. $user = $event->getToken()->getOriginalToken()->getUser();
  182. $targetUser = $event->getTargetUser();
  183. $verb = UserAuditRecord::VERB_SIMULATE;
  184. } else {
  185. $user = $event->getToken()->getUser();
  186. $targetUser = null;
  187. $verb = UserAuditRecord::VERB_SIMULATE_EXIT;
  188. }
  189. $auditRecord = AuditRecordFactory::create(
  190. UserAuditRecord::class,
  191. $user,
  192. $targetUser,
  193. $verb
  194. );
  195. $this->eventDispatcher->dispatch(
  196. AuditRecordEventFactory::create($auditRecord),
  197. AuditRecordEvent::AUDIT
  198. );
  199. }
  200. /**
  201. * Sets a device ID cookie during the kernel.response
  202. *
  203. * @param ResponseEvent $event
  204. */
  205. public function setDeviceId(ResponseEvent $event): void
  206. {
  207. $attr = $event->getRequest()->attributes->get('msr_dev_id');
  208. if ($attr !== null) {
  209. $event->getResponse()->headers->setCookie(Cookie::create('msr_dev_id', $attr, new DateTime('+2 year')));
  210. }
  211. }
  212. /**
  213. * Sends a login alert notification to the user
  214. *
  215. * @param $type
  216. * @param UserLogin $login
  217. */
  218. protected function sendAlertNotification(UserLogin $login, $type)
  219. {
  220. // Disable notification temporarily, until there is a way to unsubscribe
  221. // $this->loginNotificationGenerator->generate($login, $type);
  222. }
  223. }