From ff9742aeb8a7aaa5fc03c10964ee56f8561d1c06 Mon Sep 17 00:00:00 2001 From: Martijn Harte Date: Thu, 12 Sep 2024 08:59:30 +0200 Subject: [PATCH] Log every time a session is created Prior to this change there we weren't able to keep track of sessions that got lost. This change allows us to see every time a session is created and distinguish them by their correlation id. Log an error on a route that requires an active session when there is none Prior to this change all routes were able to called, even though the user might not have had an active session This change will start logging errors when the session wasn't found, or is in an unexpected state Listen to all routes and log the state of the session Prior to this change session information got lost. We had no way of tracking down what happened to user sessions in the logs. This change logs whether a session existed and if it's in a valid state. Log information is enriched with a correlation id to be able to distinguish them. Enable session requirement check for enrollment Inject session name into the session check services That way we always follow the configured session name set in the framework.yaml Inject the correlation salt That way we do not hard code a security measure in the code base. And allow for manual setting of that SALT Enable Session constraint testing on Authn routes --- config/openconext/parameters.yaml.dist | 5 + config/packages/framework.yaml | 5 +- config/services.yaml | 2 + src/Attribute/RequiresActiveSession.php | 28 +++ .../AuthenticationNotificationController.php | 2 + src/Controller/AuthenticationQrController.php | 2 + .../AuthenticationStatusController.php | 3 +- src/Controller/RegistrationController.php | 5 +- ...RequiresActiveSessionAttributeListener.php | 93 +++++++ src/EventSubscriber/SessionStateListener.php | 87 +++++++ src/Service/SessionCorrelationIdService.php | 57 +++++ src/Session/LoggingSessionFactory.php | 70 ++++++ ...iresActiveSessionAttributeListenerTest.php | 227 ++++++++++++++++++ .../SessionStateListenerTest.php | 166 +++++++++++++ .../SessionCorrelationIdServiceTest.php | 48 ++++ .../Session/LoggingSessionFactoryTest.php | 53 ++++ 16 files changed, 848 insertions(+), 5 deletions(-) create mode 100644 src/Attribute/RequiresActiveSession.php create mode 100644 src/EventSubscriber/RequiresActiveSessionAttributeListener.php create mode 100644 src/EventSubscriber/SessionStateListener.php create mode 100644 src/Service/SessionCorrelationIdService.php create mode 100644 src/Session/LoggingSessionFactory.php create mode 100644 tests/Unit/EventSubscriber/RequiresActiveSessionAttributeListenerTest.php create mode 100644 tests/Unit/EventSubscriber/SessionStateListenerTest.php create mode 100644 tests/Unit/Service/SessionCorrelationIdServiceTest.php create mode 100644 tests/Unit/Session/LoggingSessionFactoryTest.php diff --git a/config/openconext/parameters.yaml.dist b/config/openconext/parameters.yaml.dist index 5705b9f8..f3d53eaf 100644 --- a/config/openconext/parameters.yaml.dist +++ b/config/openconext/parameters.yaml.dist @@ -41,6 +41,11 @@ parameters: # PCRE as accepted by preg_match (http://php.net/preg_match). mobile_app_user_agent_pattern: "/^.*$/" + # When logging authentication related messages, having a reference to the session id of the user + # makes auditing the logs much easier. We do not want to log the session_id for obvious reasons, that's why + # a salt is used to hash a part of the session id. Ensuring we do not disclose sensitive data in the logs. + session_correlation_salt: 'Mr6LpJYtuWRDdVR2_7VgTChFhzQ' + # Options for the tiqr library tiqr_library_options: general: diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 16aae45d..6bf555e3 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -14,8 +14,9 @@ framework: # Remove or comment this section to explicitly disable session support. session: handler_id: null - cookie_secure: auto - cookie_samesite: none + name: sess_tiqr + cookie_httponly: true + cookie_secure: true router: strict_requirements: null utf8: true diff --git a/config/services.yaml b/config/services.yaml index d1b7ac86..ffcdc91c 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -19,6 +19,8 @@ services: $locales: '%locales%' $tiqrConfiguration: '%tiqr_library_options%' $appSecret: '%app_secret%' + $sessionOptions: '%session.storage.options%' + $sessionCorrelationSalt: '%session_correlation_salt%' # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name diff --git a/src/Attribute/RequiresActiveSession.php b/src/Attribute/RequiresActiveSession.php new file mode 100644 index 00000000..98d87bfb --- /dev/null +++ b/src/Attribute/RequiresActiveSession.php @@ -0,0 +1,28 @@ +authenticationService->getNameId(); diff --git a/src/Controller/AuthenticationQrController.php b/src/Controller/AuthenticationQrController.php index ed3bd456..b7260e03 100644 --- a/src/Controller/AuthenticationQrController.php +++ b/src/Controller/AuthenticationQrController.php @@ -24,6 +24,7 @@ use Psr\Log\LoggerInterface; use Surfnet\GsspBundle\Service\AuthenticationService; use Surfnet\GsspBundle\Service\StateHandlerInterface; +use Surfnet\Tiqr\Attribute\RequiresActiveSession; use Surfnet\Tiqr\Tiqr\TiqrServiceInterface; use Surfnet\Tiqr\WithContextLogger; use Symfony\Component\HttpFoundation\Response; @@ -44,6 +45,7 @@ public function __construct( * @throws InvalidArgumentException */ #[Route(path: '/authentication/qr', name: 'app_identity_authentication_qr', methods: ['GET'])] + #[RequiresActiveSession] public function __invoke(): Response { $nameId = $this->authenticationService->getNameId(); diff --git a/src/Controller/AuthenticationStatusController.php b/src/Controller/AuthenticationStatusController.php index ce5e71c3..5a130c9c 100644 --- a/src/Controller/AuthenticationStatusController.php +++ b/src/Controller/AuthenticationStatusController.php @@ -25,6 +25,7 @@ use Psr\Log\LoggerInterface; use Surfnet\GsspBundle\Service\AuthenticationService; use Surfnet\GsspBundle\Service\StateHandlerInterface; +use Surfnet\Tiqr\Attribute\RequiresActiveSession; use Surfnet\Tiqr\Tiqr\TiqrServiceInterface; use Surfnet\Tiqr\WithContextLogger; use Symfony\Component\HttpFoundation\JsonResponse; @@ -44,6 +45,7 @@ public function __construct( * @throws InvalidArgumentException */ #[Route(path: '/authentication/status', name: 'app_identity_authentication_status', methods: ['GET'])] + #[RequiresActiveSession] public function __invoke(): JsonResponse { try { @@ -57,7 +59,6 @@ public function __invoke(): JsonResponse return $this->refreshAuthenticationPage(); } - $isAuthenticated = $this->tiqrService->isAuthenticated(); if ($isAuthenticated) { diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php index 5ba1c942..67f0a3ba 100644 --- a/src/Controller/RegistrationController.php +++ b/src/Controller/RegistrationController.php @@ -23,6 +23,7 @@ use Psr\Log\LoggerInterface; use Surfnet\GsspBundle\Service\RegistrationService; use Surfnet\GsspBundle\Service\StateHandlerInterface; +use Surfnet\Tiqr\Attribute\RequiresActiveSession; use Surfnet\Tiqr\Exception\NoActiveAuthenrequestException; use Surfnet\Tiqr\Tiqr\Legacy\TiqrService; use Surfnet\Tiqr\Tiqr\TiqrServiceInterface; @@ -90,9 +91,8 @@ public function registration(Request $request): Response * * * @throws \InvalidArgumentException - * - * Requires session cookie to be set to a valid session. */ + #[RequiresActiveSession] #[Route(path: '/registration/status', name: 'app_identity_registration_status', methods: ['GET'])] public function registrationStatus() : Response { @@ -123,6 +123,7 @@ public function registrationStatus() : Response * * @throws \InvalidArgumentException */ + #[RequiresActiveSession] #[Route(path: '/registration/qr/{enrollmentKey}', name: 'app_identity_registration_qr', methods: ['GET'])] public function registrationQr(Request $request, string $enrollmentKey): Response { diff --git a/src/EventSubscriber/RequiresActiveSessionAttributeListener.php b/src/EventSubscriber/RequiresActiveSessionAttributeListener.php new file mode 100644 index 00000000..beb7a5a3 --- /dev/null +++ b/src/EventSubscriber/RequiresActiveSessionAttributeListener.php @@ -0,0 +1,93 @@ +sessionOptions)) { + throw new RuntimeException( + 'The session name (PHP session cookie identifier) could not be found in the session configuration.' + ); + } + $this->sessionName = $this->sessionOptions['name']; + } + + public function onKernelControllerArguments(ControllerArgumentsEvent $event): void + { + if (!is_array($event->getAttributes()[RequiresActiveSession::class] ?? null)) { + return; + } + + $logger = WithContextLogger::from($this->logger, [ + 'correlationId' => $this->sessionCorrelationIdService->generateCorrelationId() ?? '', + 'route' => $event->getRequest()->getRequestUri(), + ]); + + try { + $sessionId = $event->getRequest()->getSession()->getId(); + $sessionCookieId = $event->getRequest()->cookies->get($this->sessionName); + + if (!$sessionCookieId) { + $logger->error('Route requires active session. Active session wasn\'t found. No session cookie was set.'); + + throw new AccessDeniedException(); + } + + if ($sessionId !== $sessionCookieId) { + $logger->error('Route requires active session. Session does not match session cookie.'); + + throw new AccessDeniedException(); + } + } catch (SessionNotFoundException) { + $logger->error('Route requires active session. Active session wasn\'t found.'); + + throw new AccessDeniedException(); + } + } + + public static function getSubscribedEvents(): array + { + return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 20]]; + } +} diff --git a/src/EventSubscriber/SessionStateListener.php b/src/EventSubscriber/SessionStateListener.php new file mode 100644 index 00000000..a03a774b --- /dev/null +++ b/src/EventSubscriber/SessionStateListener.php @@ -0,0 +1,87 @@ +sessionOptions)) { + throw new RuntimeException( + 'The session name (PHP session cookie identifier) could not be found in the session configuration.' + ); + } + $this->sessionName = $this->sessionOptions['name']; + } + + public function onKernelRequest(RequestEvent $event): void + { + $logger = WithContextLogger::from($this->logger, [ + 'correlationId' => $this->sessionCorrelationIdService->generateCorrelationId() ?? '', + 'route' => $event->getRequest()->getRequestUri(), + ]); + + $sessionCookieId = $event->getRequest()->cookies->get($this->sessionName); + if ($sessionCookieId === null) { + $logger->info('User made a request without a session cookie.'); + return; + } + + $logger->info('User made a request with a session cookie.'); + + try { + $sessionId = $event->getRequest()->getSession()->getId(); + $logger->info('User has a session.'); + + if ($sessionId !== $sessionCookieId) { + $logger->error('The session cookie does not match the session id.'); + return; + } + } catch (SessionNotFoundException) { + $logger->info('Session not found.'); + return; + } + + $logger->info('User session matches the session cookie.'); + } + + public static function getSubscribedEvents(): array + { + return [KernelEvents::REQUEST => ['onKernelRequest', 20]]; + } +} diff --git a/src/Service/SessionCorrelationIdService.php b/src/Service/SessionCorrelationIdService.php new file mode 100644 index 00000000..76e72698 --- /dev/null +++ b/src/Service/SessionCorrelationIdService.php @@ -0,0 +1,57 @@ +sessionOptions)) { + throw new RuntimeException( + 'The session name (PHP session cookie identifier) could not be found in the session configuration.' + ); + } + if (empty($this->sessionCorrelationSalt)) { + throw new RuntimeException('Please configure a non empty session correlation salt.'); + } + + $this->sessionName = $this->sessionOptions['name']; + } + + public function generateCorrelationId(): ?string + { + $sessionCookie = $this->requestStack->getMainRequest()?->cookies->get($this->sessionName); + + if ($sessionCookie === null) { + return null; + } + + return hash('sha256', $this->sessionCorrelationSalt . substr($sessionCookie, 0, 10)); + } +} diff --git a/src/Session/LoggingSessionFactory.php b/src/Session/LoggingSessionFactory.php new file mode 100644 index 00000000..8491197f --- /dev/null +++ b/src/Session/LoggingSessionFactory.php @@ -0,0 +1,70 @@ +logger = WithContextLogger::from( + $monologLogger, + ['correlationId' => $sessionCorrelationIdService->generateCorrelationId() ?? ''], + ); + + parent::__construct($requestStack, $storageFactory, $usageReporter); + } + + public function createSession(): SessionInterface + { + $this->logger->info('Created new session.'); + + return parent::createSession(); + } +} diff --git a/tests/Unit/EventSubscriber/RequiresActiveSessionAttributeListenerTest.php b/tests/Unit/EventSubscriber/RequiresActiveSessionAttributeListenerTest.php new file mode 100644 index 00000000..0f310f0b --- /dev/null +++ b/tests/Unit/EventSubscriber/RequiresActiveSessionAttributeListenerTest.php @@ -0,0 +1,227 @@ +expectNotToPerformAssertions(); + + self::bootKernel(); + + $request = new Request(server: ['REQUEST_URI' => '/route']); + + $requestStack = new RequestStack(); + $requestStack->push($request); + + $stubControllerFactory = fn() => new class extends AbstractController { + }; + + $event = new ControllerArgumentsEvent( + self::$kernel, + $stubControllerFactory, + [], $request, + HttpKernelInterface::MAIN_REQUEST + ); + + $dispatcher = new EventDispatcher(); + + $mockLogger = Mockery::mock(LoggerInterface::class); + $mockLogger->shouldNotReceive('log'); + + $listener = new RequiresActiveSessionAttributeListener($mockLogger, new SessionCorrelationIdService($requestStack)); + + $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'onKernelControllerArguments']); + $dispatcher->dispatch($event, KernelEvents::REQUEST); + } + + public function testItDeniesAccessWhenThereIsNoActiveSession(): void + { + $this->expectException(AccessDeniedException::class); + $this->expectExceptionMessage('Access Denied.'); + + self::bootKernel(); + + $request = new Request(server: ['REQUEST_URI' => '/route']); + + $requestStack = new RequestStack(); + $requestStack->push($request); + + $stubControllerFactory = fn() => new class extends AbstractController { + }; + + $requestType = HttpKernelInterface::MAIN_REQUEST; + $controllerEvent = new ControllerEvent(self::$kernel, $stubControllerFactory, $request, $requestType); + $controllerEvent->setController($stubControllerFactory, [RequiresActiveSession::class => [null]]); + $event = new ControllerArgumentsEvent(self::$kernel, $controllerEvent, [], $request, $requestType); + + $dispatcher = new EventDispatcher(); + + $mockLogger = Mockery::mock(LoggerInterface::class); + $mockLogger->shouldReceive('log') + ->once() + ->with( + LogLevel::ERROR, + 'Route requires active session. Active session wasn\'t found.', + ['correlationId' => '', 'route' => '/route'] + ); + + $listener = new RequiresActiveSessionAttributeListener($mockLogger, new SessionCorrelationIdService($requestStack)); + + $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'onKernelControllerArguments']); + $dispatcher->dispatch($event, KernelEvents::REQUEST); + } + + public function testItDeniesAccessWhenThereIsNoSessionCookie(): void + { + $this->expectException(AccessDeniedException::class); + $this->expectExceptionMessage('Access Denied.'); + + self::bootKernel(); + + $session = new Session(new MockArraySessionStorage()); + $session->setId(self::SESSION_ID); + + $request = new Request(server: ['REQUEST_URI' => '/route']); + $request->setSession($session); + + $requestStack = new RequestStack(); + $requestStack->push($request); + + $stubControllerFactory = fn() => new class extends AbstractController { + }; + + $requestType = HttpKernelInterface::MAIN_REQUEST; + $controllerEvent = new ControllerEvent(self::$kernel, $stubControllerFactory, $request, $requestType); + $controllerEvent->setController($stubControllerFactory, [RequiresActiveSession::class => [null]]); + $event = new ControllerArgumentsEvent(self::$kernel, $controllerEvent, [], $request, $requestType); + + $dispatcher = new EventDispatcher(); + + $mockLogger = Mockery::mock(LoggerInterface::class); + $mockLogger->shouldReceive('log') + ->once() + ->with( + LogLevel::ERROR, + 'Route requires active session. Active session wasn\'t found. No session cookie was set.', + ['correlationId' => '', 'route' => '/route'] + ); + + $listener = new RequiresActiveSessionAttributeListener($mockLogger, new SessionCorrelationIdService($requestStack)); + + $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'onKernelControllerArguments']); + $dispatcher->dispatch($event, KernelEvents::REQUEST); + } + + public function testItDeniesAccessWhenTheActiveSessionDoesNotMatchTheSessionCookie(): void + { + $this->expectException(AccessDeniedException::class); + $this->expectExceptionMessage('Access Denied.'); + + self::bootKernel(); + + $session = new Session(new MockArraySessionStorage()); + $session->setId('erroneous-session-id'); + + $request = new Request(server: ['REQUEST_URI' => '/route'], cookies: ['PHPSESSID' => self::SESSION_ID]); + $request->setSession($session); + + $requestStack = new RequestStack(); + $requestStack->push($request); + + $stubControllerFactory = fn() => new class extends AbstractController { + }; + + $requestType = HttpKernelInterface::MAIN_REQUEST; + $controllerEvent = new ControllerEvent(self::$kernel, $stubControllerFactory, $request, $requestType); + $controllerEvent->setController($stubControllerFactory, [RequiresActiveSession::class => [null]]); + $event = new ControllerArgumentsEvent(self::$kernel, $controllerEvent, [], $request, $requestType); + + $dispatcher = new EventDispatcher(); + + $mockLogger = Mockery::mock(LoggerInterface::class); + $mockLogger->shouldReceive('log') + ->once() + ->with( + LogLevel::ERROR, + 'Route requires active session. Session does not match session cookie.', + ['correlationId' => 'f6e7cfb6f0861f577c48f171e27542236b1184f7a599dde82aca1640d86da961', 'route' => '/route'] + ); + + $listener = new RequiresActiveSessionAttributeListener($mockLogger, new SessionCorrelationIdService($requestStack)); + + $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'onKernelControllerArguments']); + $dispatcher->dispatch($event, KernelEvents::REQUEST); + } + + public function testItDoesNotThrowWhenTheActiveSessionMatchesTheSessionCookie(): void + { + $this->expectNotToPerformAssertions(); + + self::bootKernel(); + + $session = new Session(new MockArraySessionStorage()); + $session->setId(self::SESSION_ID); + + $request = new Request(server: ['REQUEST_URI' => '/route'], cookies: ['PHPSESSID' => self::SESSION_ID]); + $request->setSession($session); + + $requestStack = new RequestStack(); + $requestStack->push($request); + + $stubControllerFactory = fn() => new class extends AbstractController { + }; + + $requestType = HttpKernelInterface::MAIN_REQUEST; + $controllerEvent = new ControllerEvent(self::$kernel, $stubControllerFactory, $request, $requestType); + $controllerEvent->setController($stubControllerFactory, [RequiresActiveSession::class => [null]]); + $event = new ControllerArgumentsEvent(self::$kernel, $controllerEvent, [], $request, $requestType); + + $dispatcher = new EventDispatcher(); + + $mockLogger = Mockery::mock(LoggerInterface::class); + $mockLogger->shouldNotReceive('log'); + + $listener = new RequiresActiveSessionAttributeListener($mockLogger, new SessionCorrelationIdService($requestStack)); + + $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'onKernelControllerArguments']); + $dispatcher->dispatch($event, KernelEvents::REQUEST); + } +} diff --git a/tests/Unit/EventSubscriber/SessionStateListenerTest.php b/tests/Unit/EventSubscriber/SessionStateListenerTest.php new file mode 100644 index 00000000..580bddbc --- /dev/null +++ b/tests/Unit/EventSubscriber/SessionStateListenerTest.php @@ -0,0 +1,166 @@ +expectNotToPerformAssertions(); + + self::bootKernel(); + + $request = new Request(server: ['REQUEST_URI' => '/route']); + + $requestStack = new RequestStack(); + $requestStack->push($request); + + $event = new RequestEvent(self::$kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $dispatcher = new EventDispatcher(); + + $mockLogger = Mockery::mock(LoggerInterface::class); + $mockLogger->shouldReceive('log') + ->once() + ->with(LogLevel::INFO, 'User made a request without a session cookie.', ['correlationId' => '', 'route' => '/route']); + + $listener = new SessionStateListener($mockLogger, new SessionCorrelationIdService($requestStack)); + + $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'onKernelRequest']); + $dispatcher->dispatch($event, KernelEvents::REQUEST); + } + + public function testItLogsWhenUserHasNoSession(): void + { + $this->expectNotToPerformAssertions(); + + self::bootKernel(); + + $request = new Request(server: ['REQUEST_URI' => '/route'], cookies: ['PHPSESSID' => self::SESSION_ID]); + + $requestStack = new RequestStack(); + $requestStack->push($request); + + $event = new RequestEvent(self::$kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $dispatcher = new EventDispatcher(); + + $mockLogger = Mockery::mock(LoggerInterface::class); + $mockLogger->shouldReceive('log') + ->once() + ->with(LogLevel::INFO, 'User made a request with a session cookie.', ['correlationId' => 'f6e7cfb6f0861f577c48f171e27542236b1184f7a599dde82aca1640d86da961', 'route' => '/route']); + $mockLogger->shouldReceive('log') + ->once() + ->with(LogLevel::INFO, 'Session not found.', ['correlationId' => 'f6e7cfb6f0861f577c48f171e27542236b1184f7a599dde82aca1640d86da961', 'route' => '/route']); + + $listener = new SessionStateListener($mockLogger, new SessionCorrelationIdService($requestStack)); + + $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'onKernelRequest']); + $dispatcher->dispatch($event, KernelEvents::REQUEST); + } + + public function testItLogsAnErrorWhenTheSessionIdDoesNotMatchTheSessionCookie(): void + { + $this->expectNotToPerformAssertions(); + + self::bootKernel(); + + $session = new Session(new MockArraySessionStorage()); + $session->setId('erroneous-session-id'); + + $request = new Request(server: ['REQUEST_URI' => '/route'], cookies: ['PHPSESSID' => self::SESSION_ID]); + $request->setSession($session); + + $requestStack = new RequestStack(); + $requestStack->push($request); + + $event = new RequestEvent(self::$kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $dispatcher = new EventDispatcher(); + + $mockLogger = Mockery::mock(LoggerInterface::class); + $mockLogger->shouldReceive('log') + ->once() + ->with(LogLevel::INFO, 'User made a request with a session cookie.', ['correlationId' => 'f6e7cfb6f0861f577c48f171e27542236b1184f7a599dde82aca1640d86da961', 'route' => '/route']); + $mockLogger->shouldReceive('log') + ->once() + ->with(LogLevel::INFO, 'User has a session.', ['correlationId' => 'f6e7cfb6f0861f577c48f171e27542236b1184f7a599dde82aca1640d86da961', 'route' => '/route']); + $mockLogger->shouldReceive('log') + ->once() + ->with(LogLevel::ERROR, 'The session cookie does not match the session id.', ['correlationId' => 'f6e7cfb6f0861f577c48f171e27542236b1184f7a599dde82aca1640d86da961', 'route' => '/route']); + + $listener = new SessionStateListener($mockLogger, new SessionCorrelationIdService($requestStack)); + + $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'onKernelRequest']); + $dispatcher->dispatch($event, KernelEvents::REQUEST); + } + + public function testTheUserSessionMatchesTheSessionCookie(): void + { + $this->expectNotToPerformAssertions(); + + self::bootKernel(); + + $session = new Session(new MockArraySessionStorage()); + $session->setId(self::SESSION_ID); + + $request = new Request(server: ['REQUEST_URI' => '/route'], cookies: ['PHPSESSID' => self::SESSION_ID]); + $request->setSession($session); + + $requestStack = new RequestStack(); + $requestStack->push($request); + + + $event = new RequestEvent(self::$kernel, $request, HttpKernelInterface::MAIN_REQUEST); + + $dispatcher = new EventDispatcher(); + + $mockLogger = Mockery::mock(LoggerInterface::class); + $mockLogger->shouldReceive('log') + ->once() + ->with(LogLevel::INFO, 'User made a request with a session cookie.', ['correlationId' => 'f6e7cfb6f0861f577c48f171e27542236b1184f7a599dde82aca1640d86da961', 'route' => '/route']); + $mockLogger->shouldReceive('log') + ->once() + ->with(LogLevel::INFO, 'User has a session.', ['correlationId' => 'f6e7cfb6f0861f577c48f171e27542236b1184f7a599dde82aca1640d86da961', 'route' => '/route']); + $mockLogger->shouldReceive('log') + ->once() + ->with(LogLevel::INFO, 'User session matches the session cookie.', ['correlationId' => 'f6e7cfb6f0861f577c48f171e27542236b1184f7a599dde82aca1640d86da961', 'route' => '/route']); + + $listener = new SessionStateListener($mockLogger, new SessionCorrelationIdService($requestStack)); + + $dispatcher->addListener(KernelEvents::REQUEST, [$listener, 'onKernelRequest']); + $dispatcher->dispatch($event, KernelEvents::REQUEST); + } +} diff --git a/tests/Unit/Service/SessionCorrelationIdServiceTest.php b/tests/Unit/Service/SessionCorrelationIdServiceTest.php new file mode 100644 index 00000000..8ff8c178 --- /dev/null +++ b/tests/Unit/Service/SessionCorrelationIdServiceTest.php @@ -0,0 +1,48 @@ +push($request); + + $service = new SessionCorrelationIdService($requestStack); + + $this->assertNull($service->generateCorrelationId()); + } + + public function testItGeneratesACorrelationIdBasedOnTheSessionCookie(): void + { + $request = new Request(cookies: ['PHPSESSID' => 'session-id']); + $requestStack = new RequestStack(); + $requestStack->push($request); + + $service = new SessionCorrelationIdService($requestStack); + + $this->assertSame('f6e7cfb6f0861f577c48f171e27542236b1184f7a599dde82aca1640d86da961', $service->generateCorrelationId()); + } +} diff --git a/tests/Unit/Session/LoggingSessionFactoryTest.php b/tests/Unit/Session/LoggingSessionFactoryTest.php new file mode 100644 index 00000000..43d625f5 --- /dev/null +++ b/tests/Unit/Session/LoggingSessionFactoryTest.php @@ -0,0 +1,53 @@ + 'session-id']); + $requestStack = new RequestStack(); + $requestStack->push($request); + + $mockLogger = Mockery::mock(LoggerInterface::class); + $mockLogger->shouldReceive('log') + ->once() + ->with(LogLevel::INFO, 'Created new session.', ['correlationId' => 'f6e7cfb6f0861f577c48f171e27542236b1184f7a599dde82aca1640d86da961']); + + $sessionFactory = new LoggingSessionFactory( + $requestStack, + $this->createStub(SessionStorageFactoryInterface::class), + $mockLogger, + new SessionCorrelationIdService($requestStack), + ); + + $this->assertInstanceOf(SessionInterface::class, $sessionFactory->createSession()); + } +}