Skip to content

Commit

Permalink
Add in flight authentication limit
Browse files Browse the repository at this point in the history
  • Loading branch information
pablothedude committed Feb 7, 2025
1 parent 9d09b65 commit fbf3585
Show file tree
Hide file tree
Showing 15 changed files with 137 additions and 22 deletions.
1 change: 1 addition & 0 deletions app/config/parameters.yml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions languages/messages.en.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
2 changes: 2 additions & 0 deletions languages/messages.nl.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
2 changes: 2 additions & 0 deletions languages/messages.pt.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' => '<p>Não pode autenticar-se porque a %idpName% enviou um valor para AuthnContextClassRef que não é permitido.</p>',
'error_authn_context_class_ref_blacklisted_desc_no_idp_name' => '<p>Não pode autenticar-se porque a sua %organisationNoun% enviou um valor para AuthnContextClassRef que não é permitido.</p>',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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));

Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/**
* Copyright 2010 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.
* 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 OpenConext\EngineBlockBundle\Exception;

use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;

final class AuthenticationProceduresLimitExceededException extends TooManyRequestsHttpException
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ authentication_feedback_stuck_in_authentication_loop:
methods: [GET]
defaults: { _controller: engineblock.controller.authentication.feedback:stuckInAuthenticationLoopAction }

authentication_feedback_authentication_limit_exceeded:
path: '/authentication/feedback/authentication-limit-exceeded'
methods: [ GET ]
defaults: { _controller: engineblock.controller.authentication.feedback:authenticationLimitExceededAction }

authentication_feedback_no_authentication_request_received:
path: '/authentication/feedback/invalid-request-method-on-sso'
methods: [GET]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ services:
arguments:
- "%maximum_authentication_procedures_allowed%"
- "%time_frame_for_authentication_loop_in_seconds%"
- "%maximum_authentication_procedures_allowed_all%"

engineblock.language_support_provider:
class: OpenConext\EngineBlockBundle\Localization\LanguageSupportProvider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,16 @@ public function __construct(
/**
* @param int $maximumAuthenticationProceduresAllowed
* @param int $timeFrameForAuthenticationLoopInSeconds
* @param int $maximumAuthenticationProceduresAllowedAll
*/
public function saveAuthenticationLoopGuardConfiguration(
$maximumAuthenticationProceduresAllowed,
$timeFrameForAuthenticationLoopInSeconds
int $maximumAuthenticationProceduresAllowed,
int $timeFrameForAuthenticationLoopInSeconds,
int $maximumAuthenticationProceduresAllowedAll
) {
$this->authenticationGuardFixture['maximumAuthenticationProceduresAllowed'] = $maximumAuthenticationProceduresAllowed;
$this->authenticationGuardFixture['timeFrameForAuthenticationLoopInSeconds']
= $timeFrameForAuthenticationLoopInSeconds;
$this->authenticationGuardFixture['timeFrameForAuthenticationLoopInSeconds'] = $timeFrameForAuthenticationLoopInSeconds;
$this->authenticationGuardFixture['maximumAuthenticationProceduresAllowedAll'] = $maximumAuthenticationProceduresAllowedAll;

$this->dataStore->save($this->authenticationGuardFixture);
}
Expand All @@ -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']
);
}

Expand All @@ -96,8 +99,16 @@ public function detectsAuthenticationLoop(
)
);
}

return false;
}

public function detectsAuthenticationLimit(AuthenticationProcedureMap $pastAuthenticationProcedures): bool
{
return false;
}


public function cleanUp()
{
$this->authenticationGuardFixture = [];
Expand Down
Original file line number Diff line number Diff line change
@@ -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 %}

0 comments on commit fbf3585

Please sign in to comment.