Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add in flight authentication limit #1803

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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_authentications_per_session: 20

## 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: 1 addition & 1 deletion ci/qa/behat.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ chown -R www-data app/cache/
chmod -R 0777 /tmp/eb-fixtures

echo -e "\nRun the Behat tests\n"
./vendor/bin/behat -c ./tests/behat-ci.yml --suite default -vv --format progress --strict
./vendor/bin/behat -c ./tests/behat-ci.yml --suite default -vv --format progress --strict $@

#echo -e "\nBehat tests (with selenium and headless Chrome)\n"
#./vendor/bin/behat -c ./tests/behat-ci.yml --suite selenium -vv --format progress --strict
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 $maximumAuthenticationsPerSession;

public function __construct(
$maximumAuthenticationProceduresAllowed,
$timeFrameForAuthenticationLoopInSeconds
$timeFrameForAuthenticationLoopInSeconds,
$maximumAuthenticationsPerSession
) {
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(
$maximumAuthenticationsPerSession,
'Expected maximum authentication per session to be an integer, got "%s"'
);

$this->maximumAuthenticationProceduresAllowed = $maximumAuthenticationProceduresAllowed;
$this->timeFrameForAuthenticationLoopInSeconds = $timeFrameForAuthenticationLoopInSeconds;
$this->maximumAuthenticationsPerSession = $maximumAuthenticationsPerSession;
}

/**
* @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,18 @@ public function detectsAuthenticationLoop(

return $processedLoginProcedures >= $this->maximumAuthenticationProceduresAllowed;
}


/**
* @param AuthenticationProcedureMap $pastAuthenticationProcedures
* @return bool
*/
public function detectsAuthenticationLimit(
AuthenticationProcedureMap $pastAuthenticationProcedures
): bool {
$processedLoginProcedures = $pastAuthenticationProcedures
->count();

return $processedLoginProcedures >= $this->maximumAuthenticationsPerSession;
}
}
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 @@ -21,6 +21,7 @@
use Assert\AssertionFailedException;
use DateTimeImmutable;
use OpenConext\EngineBlock\Assert\Assertion;
use OpenConext\EngineBlockBundle\Exception\AuthenticationSessionLimitExceededException;
use OpenConext\EngineBlockBundle\Exception\LogicException;
use OpenConext\EngineBlockBundle\Exception\StuckInAuthenticationLoopException;
use OpenConext\Value\Saml\Entity;
Expand Down Expand Up @@ -54,6 +55,23 @@ public function startAuthenticationOnBehalfOf(string $requestId, Entity $service
Assertion::string($requestId, 'The requestId must be a string (XML ID) value');
$currentAuthenticationProcedure = AuthenticationProcedure::onBehalfOf($serviceProvider);

// Validate if the processed authentications this session do not exceed the configured maximum of authentications
$authenticationLimitExceeded = $this->authenticationLoopGuard->detectsAuthenticationLimit(
$this->authenticationProcedures
);

if ($authenticationLimitExceeded) {
session_destroy();

throw new AuthenticationSessionLimitExceededException(
'More than the configured maximum authentication procedures for this session'
. ' the user seems to have started too much authentications this session. '
. ' Resetting the session.'
);
}

// Validate if the processed authentications for the service provider for this session do not exceed
// the configured maximum authentications in a configured time frame.
$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\AuthenticationSessionLimitExceededException;
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 AuthenticationSessionLimitExceededException) {
$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 AuthenticationSessionLimitExceededException 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_authentications_per_session%"

engineblock.language_support_provider:
class: OpenConext\EngineBlockBundle\Localization\LanguageSupportProvider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,58 @@ Feature:

Background:
Given an EngineBlock instance on "dev.openconext.local"
And EngineBlock is configured to allow a maximum of 2 authentication procedures within a time frame of 6000 seconds
And EngineBlock is configured to allow a maximum of 2 per SP within a timeframe of 6000 seconds and with a total of 3 authentication procedures
And no registered SPs
And no registered Idps
And an Identity Provider named "Dummy Idp"
And a Service Provider named "Dummy SP"
And a Service Provider named "Dummy SP 1"
And a Service Provider named "Dummy SP 2"

Scenario: an authentication loop is detected
When I log in at "Dummy SP"
When I log in at "Dummy SP 1"
And I pass through EngineBlock
And I pass through the IdP
And I give my consent
And I pass through EngineBlock
And I log in at "Dummy SP"
And I log in at "Dummy SP 1"
And I pass through EngineBlock
And I pass through the IdP
And I give my consent
And I pass through EngineBlock
And I log in at "Dummy SP"
And I log in at "Dummy SP 1"
Then I should see "Black hole"

Scenario: an authentication loop is detected when doing an unsolicited single sign on
When An IdP initiated Single Sign on for SP "Dummy SP" is triggered by IdP "Dummy Idp"
When An IdP initiated Single Sign on for SP "Dummy SP 1" is triggered by IdP "Dummy Idp"
And I pass through EngineBlock
And I pass through the IdP
And I give my consent
And I pass through EngineBlock
And An IdP initiated Single Sign on for SP "Dummy SP" is triggered by IdP "Dummy Idp"
And An IdP initiated Single Sign on for SP "Dummy SP 1" is triggered by IdP "Dummy Idp"
And I pass through EngineBlock
And I pass through the IdP
And I give my consent
And I pass through EngineBlock
And An IdP initiated Single Sign on for SP "Dummy SP" is triggered by IdP "Dummy Idp"
And An IdP initiated Single Sign on for SP "Dummy SP 1" is triggered by IdP "Dummy Idp"
Then I should see "Black hole"

Scenario: an authentication limit is detected
When I log in at "Dummy SP 1"
And I pass through EngineBlock
And I pass through the IdP
And I give my consent
And I pass through EngineBlock
And I log in at "Dummy SP 1"
And I pass through EngineBlock
And I pass through the IdP
And I give my consent
And I pass through EngineBlock
And I log in at "Dummy SP 2"
And I pass through EngineBlock
And I pass through the IdP
And I give my consent
And I pass through EngineBlock
And I log in at "Dummy SP 2"
Then I should see "Too many authentications in progress"


Original file line number Diff line number Diff line change
Expand Up @@ -525,17 +525,20 @@ public function pdpGivesANotApplicableResponse()
}

/**
* @Given /^EngineBlock is configured to allow a maximum of (\d+) authentication procedures within a time frame of (\d+) seconds$/
* @param int $timeFrameForAuthenticationLoopInSeconds
* @Given /^EngineBlock is configured to allow a maximum of (\d+) per SP within a timeframe of (\d+) seconds and with a total of (\d+) authentication procedures$/
* @param int $maximumAuthenticationProceduresAllowed
* @param int $timeFrameForAuthenticationLoopInSeconds
* @param int $maxiumumAuth
*/
public function engineblockIsConfiguredToAllowAMaximumOfAuthenticationProceduresWithinATimeFrameOfSeconds(
public function engineblockIsConfiguredToAllowAMaximumOfAuthenticationProcedures(
$maximumAuthenticationProceduresAllowed,
$timeFrameForAuthenticationLoopInSeconds
$timeFrameForAuthenticationLoopInSeconds,
$maximumAuthenticationsPerSession
) {
$this->authenticationLoopGuard->saveAuthenticationLoopGuardConfiguration(
(int) $maximumAuthenticationProceduresAllowed,
(int) $timeFrameForAuthenticationLoopInSeconds
(int) $timeFrameForAuthenticationLoopInSeconds,
(int) $maximumAuthenticationsPerSession
);
$this->usingAuthenticationLoopGuard = true;
}
Expand Down
Loading
Loading