From a90a61e0c0c8aeb1524e9986fe91e0cb46f3fa75 Mon Sep 17 00:00:00 2001 From: Paul Rijke Date: Wed, 17 Jul 2024 15:51:45 +0200 Subject: [PATCH 1/5] Restore deleted state objects --- .../AuthenticatedSessionStateHandler.php | 89 ++++++++++++ .../Session/SessionLifetimeGuard.php | 93 ++++++++++++ .../Authentication/Session/SessionStorage.php | 133 ++++++++++++++++++ .../DashboardSamlBundle/Value/TimeFrame.php | 74 ++++++++++ 4 files changed, 389 insertions(+) create mode 100644 src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardSamlBundle/Security/Authentication/AuthenticatedSessionStateHandler.php create mode 100644 src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardSamlBundle/Security/Authentication/Session/SessionLifetimeGuard.php create mode 100644 src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardSamlBundle/Security/Authentication/Session/SessionStorage.php create mode 100644 src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardSamlBundle/Value/TimeFrame.php diff --git a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardSamlBundle/Security/Authentication/AuthenticatedSessionStateHandler.php b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardSamlBundle/Security/Authentication/AuthenticatedSessionStateHandler.php new file mode 100644 index 000000000..92085dff4 --- /dev/null +++ b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardSamlBundle/Security/Authentication/AuthenticatedSessionStateHandler.php @@ -0,0 +1,89 @@ +absoluteTimeoutLimit = $absoluteTimeoutLimit; + $this->relativeTimeoutLimit = $relativeTimeoutLimit; + } + + /** + * @param AuthenticatedSessionStateHandler $sessionStateHandler + * @return bool + */ + public function sessionLifetimeWithinLimits(AuthenticatedSessionStateHandler $sessionStateHandler) + { + return $this->sessionLifetimeWithinAbsoluteLimit($sessionStateHandler) + && $this->sessionLifetimeWithinRelativeLimit($sessionStateHandler); + } + + /** + * @param AuthenticatedSessionStateHandler $sessionStateHandler + * @return bool + */ + public function sessionLifetimeWithinAbsoluteLimit(AuthenticatedSessionStateHandler $sessionStateHandler) + { + if (!$sessionStateHandler->isAuthenticationMomentLogged()) { + return true; + } + + $authenticationMoment = $sessionStateHandler->getAuthenticationMoment(); + $sessionTimeoutMoment = $this->absoluteTimeoutLimit->getEndWhenStartingAt($authenticationMoment); + $now = DateTime::now(); + + if ($now->comesBeforeOrIsEqual($sessionTimeoutMoment)) { + return true; + } + + return false; + } + + /** + * @param AuthenticatedSessionStateHandler $sessionStateHandler + * @return bool + */ + public function sessionLifetimeWithinRelativeLimit(AuthenticatedSessionStateHandler $sessionStateHandler) + { + if (!$sessionStateHandler->hasSeenInteraction()) { + return true; + } + + $lastInteractionMoment = $sessionStateHandler->getLastInteractionMoment(); + $sessionTimeoutMoment = $this->relativeTimeoutLimit->getEndWhenStartingAt($lastInteractionMoment); + $now = DateTime::now(); + + if ($now->comesBeforeOrIsEqual($sessionTimeoutMoment)) { + return true; + } + + return false; + } +} diff --git a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardSamlBundle/Security/Authentication/Session/SessionStorage.php b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardSamlBundle/Security/Authentication/Session/SessionStorage.php new file mode 100644 index 000000000..eefedac6b --- /dev/null +++ b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardSamlBundle/Security/Authentication/Session/SessionStorage.php @@ -0,0 +1,133 @@ +session = $session; + } + + public function logAuthenticationMoment() + { + if ($this->isAuthenticationMomentLogged()) { + throw new LogicException('Cannot log authentication moment as an authentication moment is already logged'); + } + + $this->session->set(self::AUTH_SESSION_KEY . 'authenticated_at', DateTime::now()->format(DateTime::FORMAT)); + $this->updateLastInteractionMoment(); + } + + public function isAuthenticationMomentLogged() + { + return $this->session->get(self::AUTH_SESSION_KEY . 'authenticated_at', null) !== null; + } + + public function getAuthenticationMoment() + { + if (!$this->isAuthenticationMomentLogged()) { + throw new LogicException('Cannot get last authentication moment as no authentication has been set'); + } + + return DateTime::fromString($this->session->get(self::AUTH_SESSION_KEY . 'authenticated_at')); + } + + public function updateLastInteractionMoment() + { + $this->session->set(self::AUTH_SESSION_KEY . 'last_interaction', DateTime::now()->format(DateTime::FORMAT)); + } + + public function hasSeenInteraction() + { + return $this->session->get(self::AUTH_SESSION_KEY . 'last_interaction', null) !== null; + } + + public function getLastInteractionMoment() + { + if (!$this->hasSeenInteraction()) { + throw new LogicException('Cannot get last interaction moment as we have not seen any interaction'); + } + + return DateTime::fromString($this->session->get(self::AUTH_SESSION_KEY . 'last_interaction')); + } + + public function setCurrentRequestUri($uri) + { + $this->session->set(self::AUTH_SESSION_KEY . 'current_uri', $uri); + } + + public function getCurrentRequestUri() + { + $uri = $this->session->get(self::AUTH_SESSION_KEY . 'current_uri'); + $this->session->remove(self::AUTH_SESSION_KEY . 'current_uri'); + + return $uri; + } + + public function getRequestId() + { + return $this->session->get(self::SAML_SESSION_KEY . 'request_id'); + } + + public function setRequestId($requestId) + { + $this->session->set(self::SAML_SESSION_KEY . 'request_id', $requestId); + } + + public function hasRequestId() + { + return $this->session->has(self::SAML_SESSION_KEY. 'request_id'); + } + + public function clearRequestId() + { + $this->session->remove(self::SAML_SESSION_KEY . 'request_id'); + } + + public function invalidate() + { + $this->session->invalidate(); + } + + public function migrate() + { + $this->session->migrate(); + } +} diff --git a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardSamlBundle/Value/TimeFrame.php b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardSamlBundle/Value/TimeFrame.php new file mode 100644 index 000000000..3e879e90f --- /dev/null +++ b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardSamlBundle/Value/TimeFrame.php @@ -0,0 +1,74 @@ +timeFrame = $timeFrame; + } + + /** + * @param int $seconds + * @return TimeFrame + */ + public static function ofSeconds($seconds) + { + if (!is_int($seconds) || $seconds < 1) { + throw InvalidArgumentException::invalidType('positive integer', 'seconds', $seconds); + } + + return new TimeFrame(new DateInterval('PT' . $seconds . 'S')); + } + + /** + * @param DateTime $dateTime + * @return DateTime + */ + public function getEndWhenStartingAt(DateTime $dateTime) + { + return $dateTime->add($this->timeFrame); + } + + /** + * @param TimeFrame $other + * @return bool + */ + public function equals(TimeFrame $other) + { + return $this->timeFrame->s === $other->timeFrame->s; + } + + public function __toString() + { + return $this->timeFrame->format('%S'); + } +} From 9fc4294b1a526d8b6cee9eeaae04c0aa40a21772 Mon Sep 17 00:00:00 2001 From: Paul Rijke Date: Wed, 17 Jul 2024 15:52:00 +0200 Subject: [PATCH 2/5] Add listeners --- .../AuthenticatedUserListener.php | 63 +++++++++++ .../ExplicitSessionTimeoutListener.php | 102 ++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/AuthenticatedUserListener.php create mode 100644 src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/ExplicitSessionTimeoutListener.php diff --git a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/AuthenticatedUserListener.php b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/AuthenticatedUserListener.php new file mode 100644 index 000000000..87219091d --- /dev/null +++ b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/AuthenticatedUserListener.php @@ -0,0 +1,63 @@ + ['updateLastInteractionMoment', 6], + ]; + } + + public function updateLastInteractionMoment(RequestEvent $event): void + { + $token = $this->tokenStorage->getToken(); + + if ($token === null || !$this->sessionLifetimeGuard->sessionLifetimeWithinLimits($this->sessionStateHandler)) { + return; + } + $this->logger->notice('Logged in user with a session within time limits detected, updating session state'); + + // see ExplicitSessionTimeoutHandler for the rationale + if ($event->getRequest()->getMethod() === 'GET') { + $this->sessionStateHandler->setCurrentRequestUri($event->getRequest()->getRequestUri()); + } + $this->sessionStateHandler->updateLastInteractionMoment(); + } +} diff --git a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/ExplicitSessionTimeoutListener.php b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/ExplicitSessionTimeoutListener.php new file mode 100644 index 000000000..c3d19bd26 --- /dev/null +++ b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/ExplicitSessionTimeoutListener.php @@ -0,0 +1,102 @@ + ['checkSessionTimeout', 5], + ]; + } + + public function checkSessionTimeout(RequestEvent $event): void + { + $token = $this->tokenStorage->getToken(); + + if ($token === null || $this->sessionLifetimeGuard->sessionLifetimeWithinLimits($this->authenticatedSession)) { + return; + } + + $invalidatedBy = []; + if (!$this->sessionLifetimeGuard->sessionLifetimeWithinAbsoluteLimit($this->authenticatedSession)) { + $invalidatedBy[] = 'absolute'; + } + + if (!$this->sessionLifetimeGuard->sessionLifetimeWithinRelativeLimit($this->authenticatedSession)) { + $invalidatedBy[] = 'relative'; + } + + $this->logger->notice(sprintf( + 'Authenticated user found, but session was determined to be outside of the "%s" time limit. User will ' + . 'be logged out and redirected to session-expired page to attempt new login.', + implode(' and ', $invalidatedBy), + )); + + $request = $event->getRequest(); + + // if the current request was not a GET request we cannot safely redirect to that page after login as it + // may require a form resubmit for instance. Therefor, we redirect to the last GET request (either current + // or previous). + $afterLoginRedirectTo = $this->authenticatedSession->getCurrentRequestUri(); + + if ($event->getRequest()->getMethod() === 'GET') { + $afterLoginRedirectTo = $event->getRequest()->getRequestUri(); + } + + // log the user out using Symfony methodology, see the LogoutListener + $event->setResponse(new RedirectResponse($this->router->generate('selfservice_security_session_expired'))); + + // something to clear cookies + $this->eventDispatcher->dispatch(new LogoutEvent($request, $token)); + $this->tokenStorage->setToken(null); + + // the session is restarted after invalidation during the logout, so we can (re)store the last GET request + $this->authenticatedSession->setCurrentRequestUri($afterLoginRedirectTo); + } +} From 982fabacc534807e40ecbb3e3cc7b107b62359e7 Mon Sep 17 00:00:00 2001 From: Paul Rijke Date: Wed, 17 Jul 2024 16:11:44 +0200 Subject: [PATCH 3/5] Configure service --- config/services.yaml | 2 ++ .../EventListener/ExplicitSessionTimeoutListener.php | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/config/services.yaml b/config/services.yaml index dd7e784a9..fb365761c 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -28,3 +28,5 @@ services: - '@twig' - [] - '@logger' + + Surfnet\ServiceProviderDashboard\Infrastructure\DashboardBundle\Security\Authentication\Session\SessionLifetimeGuard: diff --git a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/ExplicitSessionTimeoutListener.php b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/ExplicitSessionTimeoutListener.php index c3d19bd26..3493d7f86 100644 --- a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/ExplicitSessionTimeoutListener.php +++ b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/ExplicitSessionTimeoutListener.php @@ -21,8 +21,8 @@ namespace Surfnet\ServiceProviderDashboard\Infrastructure\DashboardBundle\EventListener; use Psr\Log\LoggerInterface; -use Surfnet\StepupSelfService\SelfServiceBundle\Security\Authentication\AuthenticatedSessionStateHandler; -use Surfnet\StepupSelfService\SelfServiceBundle\Security\Authentication\Session\SessionLifetimeGuard; +use Surfnet\ServiceProviderDashboard\Infrastructure\DashboardSamlBundle\Security\Authentication\AuthenticatedSessionStateHandler; +use Surfnet\ServiceProviderDashboard\Infrastructure\DashboardSamlBundle\Security\Authentication\Session\SessionLifetimeGuard; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -38,8 +38,8 @@ public function __construct( private TokenStorageInterface $tokenStorage, private AuthenticatedSessionStateHandler $authenticatedSession, - #[Autowire(service: 'self_service.security.authentication.session.session_lifetime_guard')] - private SessionLifetimeGuard $sessionLifetimeGuard, + #[Autowire(service: SessionLifetimeGuard::class)] + private SessionLifetimeGuard $sessionLifetimeGuard, private RouterInterface $router, private LoggerInterface $logger, private EventDispatcherInterface $eventDispatcher, From f69373d2188ad40dd665101718e4569256d97452 Mon Sep 17 00:00:00 2001 From: Paul Rijke Date: Wed, 17 Jul 2024 16:20:52 +0200 Subject: [PATCH 4/5] Correct docheader --- .../DashboardBundle/EventListener/AuthenticatedUserListener.php | 2 +- .../EventListener/ExplicitSessionTimeoutListener.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/AuthenticatedUserListener.php b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/AuthenticatedUserListener.php index 87219091d..9e6b0092b 100644 --- a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/AuthenticatedUserListener.php +++ b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/AuthenticatedUserListener.php @@ -3,7 +3,7 @@ declare(strict_types = 1); /** - * Copyright 2016 SURFnet bv + * Copyright 2024 SURFnet B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/ExplicitSessionTimeoutListener.php b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/ExplicitSessionTimeoutListener.php index 3493d7f86..1c2b97bd5 100644 --- a/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/ExplicitSessionTimeoutListener.php +++ b/src/Surfnet/ServiceProviderDashboard/Infrastructure/DashboardBundle/EventListener/ExplicitSessionTimeoutListener.php @@ -3,7 +3,7 @@ declare(strict_types = 1); /** - * Copyright 2024 SURFnet bv + * Copyright 2024 SURFnet B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 519c42ca45349b98fa925fb7d948c64bc0839bcf Mon Sep 17 00:00:00 2001 From: Paul Rijke Date: Wed, 17 Jul 2024 16:25:20 +0200 Subject: [PATCH 5/5] Add listeners as services --- config/services.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/services.yaml b/config/services.yaml index fb365761c..09fcdd722 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -29,4 +29,8 @@ services: - [] - '@logger' + Surfnet\ServiceProviderDashboard\Infrastructure\DashboardBundle\Security\Authentication\Listener\ExplicitSessionTimeoutListener: + + Surfnet\ServiceProviderDashboard\Infrastructure\DashboardBundle\Security\Authentication\Listener\AuthenticatedUserListener: + Surfnet\ServiceProviderDashboard\Infrastructure\DashboardBundle\Security\Authentication\Session\SessionLifetimeGuard: