Skip to content

Commit

Permalink
Restore cookie invalidation when logged in
Browse files Browse the repository at this point in the history
  • Loading branch information
parijke committed May 23, 2024
1 parent 236c43b commit f9e0242
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 9 deletions.
5 changes: 0 additions & 5 deletions ci/qa/phpstan-baseline.php
Original file line number Diff line number Diff line change
Expand Up @@ -296,11 +296,6 @@
'count' => 1,
'path' => __DIR__ . '/../../src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/EntryPointController.php',
];
$ignoreErrors[] = [
'message' => '#^Parameter \\#1 \\$uri of method Surfnet\\\\StepupSelfService\\\\SelfServiceBundle\\\\Service\\\\ActivationFlowService\\:\\:process\\(\\) expects string, string\\|null given\\.$#',
'count' => 1,
'path' => __DIR__ . '/../../src/Surfnet/StepupSelfService/SelfServiceBundle/Controller/EntryPointController.php',
];
$ignoreErrors[] = [
'message' => '#^Method Surfnet\\\\StepupSelfService\\\\SelfServiceBundle\\\\Controller\\\\ExceptionController\\:\\:getPageTitleAndDescription\\(\\) return type has no value type specified in iterable type array\\.$#',
'count' => 1,
Expand Down
2 changes: 2 additions & 0 deletions config/openconext/parameters.yaml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ parameters:
recovery_method_sms_enabled: true
recovery_method_safe_store_code_enabled: true

authentication_context_class_ref: ~

when@test:
parameters:
app_secret: $ecretf0rt3st
Expand Down
5 changes: 3 additions & 2 deletions config/packages/framework.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ framework:
cookie_httponly: true
cookie_secure: true
cookie_samesite: none
cookie_lifetime: '%session_max_absolute_lifetime%'
gc_maxlifetime: '%session_max_relative_lifetime%'
cookie_lifetime: 0
gc_maxlifetime: 0


fragments: false
error_controller: Surfnet\StepupSelfService\SelfServiceBundle\Controller\ExceptionController::show
Expand Down
8 changes: 8 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,11 @@ services:
arguments:
- '@security.helper'
- "%logout_redirect_url%"


Surfnet\StepupSelfService\SelfServiceBundle\EventListener\AuthenticatedUserListener:
Surfnet\StepupSelfService\SelfServiceBundle\EventListener\ExplicitSessionTimeoutListener:


Surfnet\StepupSelfService\SelfServiceBundle\Security\Authentication\Session\SessionLifetimeGuard:
alias: self_service.security.authentication.session.session_lifetime_guard
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

declare(strict_types = 1);

/**
* Copyright 2016 SURFnet bv
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Surfnet\StepupSelfService\SelfServiceBundle\EventListener;

use Psr\Log\LoggerInterface;
use Surfnet\StepupSelfService\SelfServiceBundle\Security\Authentication\AuthenticatedSessionStateHandler;
use Surfnet\StepupSelfService\SelfServiceBundle\Security\Authentication\Session\SessionLifetimeGuard;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class AuthenticatedUserListener implements EventSubscriberInterface
{
public function __construct(
private readonly TokenStorageInterface $tokenStorage,
private readonly SessionLifetimeGuard $sessionLifetimeGuard,
private readonly AuthenticatedSessionStateHandler $sessionStateHandler,
private readonly LoggerInterface $logger,
) {
}

public static function getSubscribedEvents(): array
{
return [
// The firewall, which makes the token available, listens at P8
// We must jump in after the firewall, forcing us to overwrite the translator locale.
KernelEvents::REQUEST => ['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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types = 1);

/**
* Copyright 2024 SURFnet bv
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Surfnet\StepupSelfService\SelfServiceBundle\EventListener;

use Psr\Log\LoggerInterface;
use Surfnet\StepupSelfService\SelfServiceBundle\Security\Authentication\AuthenticatedSessionStateHandler;
use Surfnet\StepupSelfService\SelfServiceBundle\Security\Authentication\Session\SessionLifetimeGuard;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

final readonly class ExplicitSessionTimeoutListener implements EventSubscriberInterface
{
public function __construct(
private TokenStorageInterface $tokenStorage,
private AuthenticatedSessionStateHandler $authenticatedSession,
#[Autowire(service: 'self_service.security.authentication.session.session_lifetime_guard')]
private SessionLifetimeGuard $sessionLifetimeGuard,
private RouterInterface $router,
private LoggerInterface $logger,
private EventDispatcherInterface $eventDispatcher,
) {
}

public static function getSubscribedEvents(): array
{
return [
// The firewall, which makes the token available, listens at P8
// We must jump in after the firewall, forcing us to overwrite the translator locale.
KernelEvents::REQUEST => ['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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public function hasSeenInteraction(): bool;

public function setCurrentRequestUri(string $uri): void;

public function getCurrentRequestUri(): ?string;
public function getCurrentRequestUri(): string;

/**
* Migrates the current session to a new session id while maintaining all
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ public function sessionLifetimeWithinLimits(AuthenticatedSessionStateHandler $se
&& $this->sessionLifetimeWithinRelativeLimit($sessionStateHandler);
}


public function sessionLifetimeWithinAbsoluteLimit(AuthenticatedSessionStateHandler $sessionStateHandler): bool
{
if (!$sessionStateHandler->isAuthenticationMomentLogged()) {
Expand Down

0 comments on commit f9e0242

Please sign in to comment.