diff --git a/app/config/parameters.yml.dist b/app/config/parameters.yml.dist index 2687881fc1..56559b3ef3 100644 --- a/app/config/parameters.yml.dist +++ b/app/config/parameters.yml.dist @@ -193,6 +193,7 @@ parameters: ## Settings for detecting whether the user is stuck in a authentication loop within his session time_frame_for_authentication_loop_in_seconds: 60 maximum_authentication_procedures_allowed: 5 + maximum_authentication_procedures_allowed_all: 5 ## Store attributes with their values, meaning that if an Idp suddenly ## sends a new value (like a new e-mail address) consent has to be diff --git a/languages/messages.en.php b/languages/messages.en.php index 5b335cb160..0961cb487a 100644 --- a/languages/messages.en.php +++ b/languages/messages.en.php @@ -222,6 +222,8 @@ 'error_stuck_in_authentication_loop_desc_no_idp_name' => 'You\'ve successfully authenticated at your %organisationNoun% but %spName% sends you back again to %suiteName%. Because you are already logged in, %suiteName% then sends you back to %spName%, which results in an infinite black hole. Likely, this is caused by an error at %spName%.', 'error_stuck_in_authentication_loop_desc_no_sp_name' => 'You\'ve successfully authenticated at %idpName% but the service you are trying to access sends you back again to %suiteName%. Because you are already logged in, %suiteName% then sends you back to the service, which results in an infinite black hole. Likely, this is caused by an error at the Service Provider.', 'error_stuck_in_authentication_loop_desc_no_name' => 'You\'ve successfully authenticated at your %organisationNoun% but the service you are trying to access sends you back again to %suiteName%. Because you are already logged in, %suiteName% then sends you back to the service, which results in an infinite black hole. Likely, this is caused by an error at the Service Provider.', + 'error_authentication_limit_exceeded' => 'Error - too many authentications in progress', + 'error_authentication_limit_exceeded_desc' => 'Too many authentications in progress', 'error_no_authentication_request_received' => 'Error - No authentication request received.', 'error_authn_context_class_ref_blacklisted' => 'Error - AuthnContextClassRef value is not allowed', 'error_authn_context_class_ref_blacklisted_desc' => 'You cannot login because %idpName% sent a value for AuthnContextClassRef that is not allowed. Please contact the service desk of %idpName% to solve this.', diff --git a/languages/messages.nl.php b/languages/messages.nl.php index aa29bbf6d0..d7e59c386a 100644 --- a/languages/messages.nl.php +++ b/languages/messages.nl.php @@ -219,6 +219,8 @@ 'error_stuck_in_authentication_loop_desc_no_idp_name' => 'Je bent succesvol ingelogd bij je %organisationNoun% maar %spName% stuurt je weer terug naar %suiteName%. Omdat je succesvol bent ingelogd, stuurt %suiteName% je weer naar %spName%, wat resulteert in een oneindig zwart gat. Dit komt waarschijnlijk door een foutje aan de kant van %spName%.', 'error_stuck_in_authentication_loop_desc_no_sp_name' => 'Je bent succesvol ingelogd bij %idpName% maar de dienst waar je naartoe wilt stuurt je weer terug naar %suiteName%. Omdat je succesvol bent ingelogd, stuurt %suiteName% je weer naar de dienst, wat resulteert in een oneindig zwart gat. Dit komt waarschijnlijk door een foutje aan de kant van de dienst.', 'error_stuck_in_authentication_loop_desc_no_name' => 'Je bent succesvol ingelogd bij je %organisationNoun% maar de dienst waar je naartoe wilt stuurt je weer terug naar %suiteName%. Omdat je succesvol bent ingelogd, stuurt %suiteName% je weer naar de dienst, wat resulteert in een oneindig zwart gat. Dit komt waarschijnlijk door een foutje aan de kant van de dienst.', + 'error_authentication_limit_exceeded' => 'Fout - teveeel authenticaties bezig', + 'error_authentication_limit_exceeded_desc' => 'Teveel authenticaties bezig', 'error_no_authentication_request_received' => 'Fout - Geen authenticatie-aanvraag ontvangen.', 'error_authn_context_class_ref_blacklisted' => 'Fout - Waarde van AuthnContextClassRef is niet toegestaan', 'error_authn_context_class_ref_blacklisted_desc' => 'Je kunt niet inloggen omdat %idpName% een waarde stuurde voor AuthnContextClassRef die niet is toegestaan. Neem contact op met de helpdesk van %idpName% om dit op te lossen.', diff --git a/languages/messages.pt.php b/languages/messages.pt.php index f3f68ab93c..cc7c40ee0d 100644 --- a/languages/messages.pt.php +++ b/languages/messages.pt.php @@ -216,6 +216,8 @@ 'error_stuck_in_authentication_loop_desc_no_idp_name' => 'Autenticou-se com sucesso no seu Fornecedor de Identidade, mas o %spName% reencaminhou-o de volta para %suiteName%. Como já está autenticado, o %suiteName% o reencaminha de volta para o %spName%, o que resulta num ciclo infinito. Muito provavelmente, isto é provocado por um erro no %spName%.', 'error_stuck_in_authentication_loop_desc_no_sp_name' => 'Autenticou-se com sucesso no seu %idpName%, mas o serviço reencaminhou-o de volta para %suiteName%. Como já está autenticado, o %suiteName% o reencaminha de volta para o serviço, o que resulta num ciclo infinito. Muito provavelmente, isto é provocado por um erro no Fornecedor de Serviço.', 'error_stuck_in_authentication_loop_desc_no_name' => 'Autenticou-se com sucesso no seu Fornecedor de Identidade, mas o serviço ao qual está a tentar aceder reencaminhou-o de volta para %suiteName%. Como já está autenticado, o %suiteName% o reencaminha de volta para o serviço, o que resulta num ciclo infinito. Muito provavelmente, isto é provocado por um erro no Fornecedor de Serviço.', + 'error_authentication_limit_exceeded' => 'Error - too many authentications in progress', + 'error_authentication_limit_exceeded_desc' => 'Too many authentications in progress', 'error_authn_context_class_ref_blacklisted' => 'Erro - O valor para AuthnContextClassRef não é permitido', 'error_authn_context_class_ref_blacklisted_desc' => '

Não pode autenticar-se porque a %idpName% enviou um valor para AuthnContextClassRef que não é permitido.

', 'error_authn_context_class_ref_blacklisted_desc_no_idp_name' => '

Não pode autenticar-se porque a sua %organisationNoun% enviou um valor para AuthnContextClassRef que não é permitido.

', diff --git a/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationLoopGuard.php b/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationLoopGuard.php index f5292dd5ff..6cc70c1aab 100644 --- a/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationLoopGuard.php +++ b/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationLoopGuard.php @@ -34,9 +34,15 @@ final class AuthenticationLoopGuard implements AuthenticationLoopGuardInterface */ private $timeFrameForAuthenticationLoopInSeconds; + /** + * @var int + */ + private $maximumAuthenticationProceduresAllowedAll; + public function __construct( $maximumAuthenticationProceduresAllowed, - $timeFrameForAuthenticationLoopInSeconds + $timeFrameForAuthenticationLoopInSeconds, + $maximumAuthenticationProceduresAllowedAll ) { Assertion::integer( $maximumAuthenticationProceduresAllowed, @@ -46,20 +52,20 @@ public function __construct( $timeFrameForAuthenticationLoopInSeconds, 'Expected time frame for determining authentication loop in seconds to be an integer, got "%s"' ); + Assertion::integer( + $maximumAuthenticationProceduresAllowedAll, + 'Expected maximum authentication procedures allowed all to be an integer, got "%s"' + ); $this->maximumAuthenticationProceduresAllowed = $maximumAuthenticationProceduresAllowed; $this->timeFrameForAuthenticationLoopInSeconds = $timeFrameForAuthenticationLoopInSeconds; + $this->maximumAuthenticationProceduresAllowedAll = $maximumAuthenticationProceduresAllowedAll; } - /** - * @param Entity $serviceProvider - * @param AuthenticationProcedureMap $pastAuthenticationProcedures - * @return bool - */ public function detectsAuthenticationLoop( Entity $serviceProvider, AuthenticationProcedureMap $pastAuthenticationProcedures - ) { + ): bool { $now = new DateTimeImmutable; $startDate = $now->modify(sprintf('-%d seconds', $this->timeFrameForAuthenticationLoopInSeconds)); @@ -70,4 +76,19 @@ public function detectsAuthenticationLoop( return $processedLoginProcedures >= $this->maximumAuthenticationProceduresAllowed; } + + + /** + * @param AuthenticationProcedureMap $pastAuthenticationProcedures + * @return bool + */ + public function detectsAuthenticationLimit( + AuthenticationProcedureMap $pastAuthenticationProcedures + ): bool { + $processedLoginProcedures = $pastAuthenticationProcedures + ->filterProceduresNotCompleted() + ->count(); + + return $processedLoginProcedures >= $this->maximumAuthenticationProceduresAllowedAll; + } } diff --git a/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationLoopGuardInterface.php b/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationLoopGuardInterface.php index e7ea6cb38a..503247f175 100644 --- a/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationLoopGuardInterface.php +++ b/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationLoopGuardInterface.php @@ -22,13 +22,12 @@ interface AuthenticationLoopGuardInterface { - /** - * @param Entity $serviceProvider - * @param AuthenticationProcedureMap $pastAuthenticationProcedures - * @return - */ public function detectsAuthenticationLoop( Entity $serviceProvider, AuthenticationProcedureMap $pastAuthenticationProcedures - ); + ): bool; + + public function detectsAuthenticationLimit( + AuthenticationProcedureMap $pastAuthenticationProcedures + ): bool; } diff --git a/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationProcedureMap.php b/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationProcedureMap.php index c13f2867a3..63ce696e73 100644 --- a/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationProcedureMap.php +++ b/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationProcedureMap.php @@ -91,6 +91,15 @@ public function filterProceduresCompletedAfter(DateTimeInterface $startDate): Au return new self(array_filter($this->authenticationProcedures, $filterMethod)); } + public function filterProceduresNotCompleted(): AuthenticationProcedureMap + { + $filterMethod = function (AuthenticationProcedure $authenticationProcedure) { + return !$authenticationProcedure->isCompleted(); + }; + + return new self(array_filter($this->authenticationProcedures, $filterMethod)); + } + /** * @param AuthenticationProcedure $other * @return bool diff --git a/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationState.php b/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationState.php index eb5aba1a98..516dbf8a11 100644 --- a/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationState.php +++ b/src/OpenConext/EngineBlockBundle/Authentication/AuthenticationState.php @@ -21,6 +21,7 @@ use Assert\AssertionFailedException; use DateTimeImmutable; use OpenConext\EngineBlock\Assert\Assertion; +use OpenConext\EngineBlockBundle\Exception\AuthenticationProceduresLimitExceededException; use OpenConext\EngineBlockBundle\Exception\LogicException; use OpenConext\EngineBlockBundle\Exception\StuckInAuthenticationLoopException; use OpenConext\Value\Saml\Entity; @@ -54,6 +55,22 @@ public function startAuthenticationOnBehalfOf(string $requestId, Entity $service Assertion::string($requestId, 'The requestId must be a string (XML ID) value'); $currentAuthenticationProcedure = AuthenticationProcedure::onBehalfOf($serviceProvider); + // total + $authenticationLimitExceeded = $this->authenticationLoopGuard->detectsAuthenticationLimit( + $this->authenticationProcedures + ); + + if ($authenticationLimitExceeded) { + session_destroy(); + + throw new AuthenticationProceduresLimitExceededException( + 'More than the configured maximum authentication procedures for the current user' + . ' the user seems to be stuck in an authentication loop. ' + . ' Aborting the current authentication procedure.' + ); + } + + // per SP $inAuthenticationLoop = $this->authenticationLoopGuard->detectsAuthenticationLoop( $serviceProvider, $this->authenticationProcedures diff --git a/src/OpenConext/EngineBlockBundle/Controller/FeedbackController.php b/src/OpenConext/EngineBlockBundle/Controller/FeedbackController.php index a41437ff95..d23687a2e9 100644 --- a/src/OpenConext/EngineBlockBundle/Controller/FeedbackController.php +++ b/src/OpenConext/EngineBlockBundle/Controller/FeedbackController.php @@ -454,6 +454,14 @@ public function stuckInAuthenticationLoopAction() ); } + public function authenticationLimitExceededAction() + { + return new Response( + $this->twig->render('@theme/Authentication/View/Feedback/authentication-limit-exceeded.html.twig'), + 429 + ); + } + /** * @return Response */ diff --git a/src/OpenConext/EngineBlockBundle/EventListener/RedirectToFeedbackPageExceptionListener.php b/src/OpenConext/EngineBlockBundle/EventListener/RedirectToFeedbackPageExceptionListener.php index aa17af6f18..b388151b2e 100644 --- a/src/OpenConext/EngineBlockBundle/EventListener/RedirectToFeedbackPageExceptionListener.php +++ b/src/OpenConext/EngineBlockBundle/EventListener/RedirectToFeedbackPageExceptionListener.php @@ -47,6 +47,7 @@ use OpenConext\EngineBlock\Exception\InvalidRequestMethodException; use OpenConext\EngineBlock\Exception\MissingParameterException; use OpenConext\EngineBlockBridge\ErrorReporter; +use OpenConext\EngineBlockBundle\Exception\AuthenticationProceduresLimitExceededException; use OpenConext\EngineBlockBundle\Exception\EntityCanNotBeFoundException; use OpenConext\EngineBlockBundle\Exception\StuckInAuthenticationLoopException; use OpenConext\EngineBlockBundle\Exception\UnknownKeyIdException; @@ -192,6 +193,9 @@ public function onKernelException(GetResponseForExceptionEvent $event) } elseif ($exception instanceof StuckInAuthenticationLoopException) { $message = 'Stuck in authentication loop'; $redirectToRoute = 'authentication_feedback_stuck_in_authentication_loop'; + } elseif ($exception instanceof AuthenticationProceduresLimitExceededException) { + $message = 'Authentication procedure limit exceeded'; + $redirectToRoute = 'authentication_feedback_authentication_limit_exceeded'; } elseif ($exception instanceof InvalidRequestMethodException || $exception instanceof InvalidBindingException || $exception instanceof MissingParameterException @@ -205,13 +209,13 @@ public function onKernelException(GetResponseForExceptionEvent $event) } elseif ($exception instanceof EngineBlock_Corto_Exception_UserCancelledStepupCallout) { $message = $exception->getMessage(); $redirectToRoute = 'authentication_feedback_stepup_callout_user_cancelled'; - } else if ($exception instanceof EngineBlock_Corto_Exception_InvalidStepupLoaLevel) { + } elseif ($exception instanceof EngineBlock_Corto_Exception_InvalidStepupLoaLevel) { $message = $exception->getMessage(); $redirectToRoute = 'authentication_feedback_stepup_callout_unmet_loa'; - } else if ($exception instanceof EngineBlock_Corto_Exception_InvalidStepupCalloutResponse) { + } elseif ($exception instanceof EngineBlock_Corto_Exception_InvalidStepupCalloutResponse) { $message = $exception->getMessage(); $redirectToRoute = 'authentication_feedback_stepup_callout_unknown'; - } else if ($exception instanceof EntityCanNotBeFoundException) { + } elseif ($exception instanceof EntityCanNotBeFoundException) { $event->getRequest()->getSession()->set('feedback_custom', $exception->getMessage()); $redirectToRoute = 'authentication_feedback_metadata_entity_not_found'; } else { diff --git a/src/OpenConext/EngineBlockBundle/Exception/AuthenticationProceduresLimitExceededException.php b/src/OpenConext/EngineBlockBundle/Exception/AuthenticationProceduresLimitExceededException.php new file mode 100644 index 0000000000..df2468b51b --- /dev/null +++ b/src/OpenConext/EngineBlockBundle/Exception/AuthenticationProceduresLimitExceededException.php @@ -0,0 +1,25 @@ +authenticationGuardFixture['maximumAuthenticationProceduresAllowed'] = $maximumAuthenticationProceduresAllowed; - $this->authenticationGuardFixture['timeFrameForAuthenticationLoopInSeconds'] - = $timeFrameForAuthenticationLoopInSeconds; + $this->authenticationGuardFixture['timeFrameForAuthenticationLoopInSeconds'] = $timeFrameForAuthenticationLoopInSeconds; + $this->authenticationGuardFixture['maximumAuthenticationProceduresAllowedAll'] = $maximumAuthenticationProceduresAllowedAll; $this->dataStore->save($this->authenticationGuardFixture); } @@ -75,13 +77,14 @@ public function saveAuthenticationLoopGuardConfiguration( public function detectsAuthenticationLoop( Entity $serviceProvider, AuthenticationProcedureMap $pastAuthenticationProcedures - ) { + ): bool { if ($this->authenticationGuardFixture === false) { $authenticationLoopGuard = $this->authenticationLoopGuard; } else { $authenticationLoopGuard = new AuthenticationLoopGuard( $this->authenticationGuardFixture['maximumAuthenticationProceduresAllowed'], - $this->authenticationGuardFixture['timeFrameForAuthenticationLoopInSeconds'] + $this->authenticationGuardFixture['timeFrameForAuthenticationLoopInSeconds'], + $this->authenticationGuardFixture['timeFrameForAuthenticationLoopInSecondsAll'] ); } @@ -96,8 +99,16 @@ public function detectsAuthenticationLoop( ) ); } + + return false; + } + + public function detectsAuthenticationLimit(AuthenticationProcedureMap $pastAuthenticationProcedures): bool + { + return false; } + public function cleanUp() { $this->authenticationGuardFixture = []; diff --git a/theme/base/templates/modules/Authentication/View/Feedback/authentication-limit-exceeded.html.twig b/theme/base/templates/modules/Authentication/View/Feedback/authentication-limit-exceeded.html.twig new file mode 100644 index 0000000000..cbb1c57e93 --- /dev/null +++ b/theme/base/templates/modules/Authentication/View/Feedback/authentication-limit-exceeded.html.twig @@ -0,0 +1,8 @@ +{% extends '@theme/Default/View/Error/error.html.twig' %} + +{% set pageTitle = 'error_authentication_limit_exceeded'|trans %} +{% block pageTitle %}{{ pageTitle }}{% endblock %} +{% block title %}{{ parent() }}{% endblock %} +{% block pageHeading %}{{ pageTitle }}{% endblock %} + +{% block errorMessage %}{{ 'error_authentication_limit_exceeded_desc'|trans }}{% endblock %}