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()); + } +}