Skip to content

Commit 14bb0db

Browse files
authored
unsolicited response specific error (#1965)
* feat: add specific error page for unsolicited SAML responses (IdP-initiated SSO) When an IdP sends an assertion without InResponseTo (e.g. via a portal tile, bookmark, or saved link), EngineBlock now shows a specific user-friendly error page instead of the generic unknown-error page. Fixes #1838 * docs: add CHANGELOG entry for unsolicited SAML response error page * test: add Behat coverage for unsolicited SAML response (IdP-initiated SSO) - Add omitInResponseTo flag to MockIdentityProvider (serialized in __sleep) - ResponseFactory: skip InResponseTo when flag set, explicitly null the template placeholder - MockIdpContext: new step 'the IdP omits InResponseTo from its response' - Bindings.feature: scenario asserting redirect to unsolicited-response error page * Add test coverage
1 parent 4282a3f commit 14bb0db

15 files changed

Lines changed: 563 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Changes:
2626
* Because `deleted_at` is part of the PK, no migration is provided. The database engine should not allow this to be null in the first place, so it is probably not nullable already on your db.
2727
* The `0000-00-00 00:00:00` is added for clarity/consistency, as this is probably the default behaviour of your database already.
2828
* Removed unused index `consent.deleted_at`. Delete this from your production database if it's there.
29+
* Added a specific error page for unsolicited SAML responses (IdP-initiated SSO without a prior AuthnRequest).
2930

3031
* Stabilized consent checks
3132
* In order to make the consent hashes more robust, a more consistent way of hashing the user attributes has been introduced

languages/messages.en.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@
167167
'error_session_lost_desc' => 'To continue to the service an active session is required. However, your session expired. Perhaps you waited too long with logging in? Please go back to the service and try again. If that doesn\'t work, close your browser first and then try again.',
168168
'error_session_not_started' => 'Error - No session found',
169169
'error_session_not_started_desc' => 'To continue to the service an active session is required. However, no session was found. Your browser must accept cookies. Alternatively, the link you used to get to the service might be wrong. Please go back to the service and try again. If that doesn\'t work, try a different browser.',
170+
'error_unsolicited_response' => 'Error - Sign-in could not be completed',
171+
'error_unsolicited_response_desc' => 'Your sign-in could not be completed because the login request was initiated in a way that is not supported. You were sent directly to this application by your identity provider (e.g. via a bookmark, portal tile, or saved link) without first starting a login from this application. This is not supported. Please start again from the service you were trying to access and log in from there.',
170172
'error_authorization_policy_violation' => 'Error - Access denied',
171173
'error_authorization_policy_violation_desc' => 'You cannot use %spName% because %idpName% limits access to it (the "Service Provider") with an authorization policy. Please contact the service desk of %idpName% if you think you should be allowed access to %spName%.',
172174
'error_authorization_policy_violation_desc_no_idp_name' => 'You cannot use %spName% because your %organisationNoun% limits access to it (the "Service Provider") with an authorization policy. Please contact the service desk of your %organisationNoun% if you think you should be allowed access to %spName%.',

languages/messages.nl.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@
167167
'error_session_lost_desc' => 'Om verder te gaan naar de dienst heb je een actieve sessie nodig, maar deze is verlopen. Heb je misschien te lang gewacht met inloggen? Ga terug naar de dienst en probeer het nog een keer. Als dat niet werkt, sluit je browser af en probeer nogmaals opnieuw in te loggen.',
168168
'error_session_not_started' => 'Fout - Geen sessie gevonden',
169169
'error_session_not_started_desc' => 'Om verder te gaan naar de dienst heb je een actieve sessie nodig, maar we kunnen deze niet vinden. Je browser moet cookies ondersteunen. Ook kan de link die je hebt gebruikt om bij de dienst te komen, verkeerd zijn. Ga terug naar de dienst en probeer het opnieuw. Als dat niet werkt, probeer een andere browser.',
170+
'error_unsolicited_response' => 'Fout - Inloggen kon niet worden voltooid',
171+
'error_unsolicited_response_desc' => 'Je inlogpoging kon niet worden voltooid omdat het inlogverzoek op een niet-ondersteunde manier is gestart. Je bent rechtstreeks naar deze toepassing gestuurd door je identiteitsprovider (bijv. via een bladwijzer, portaltegel of opgeslagen link) zonder eerst een login te starten vanuit de dienst zelf. Dit wordt niet ondersteund. Begin opnieuw vanuit de dienst die je wilt gebruiken en log in via die weg.',
170172
'error_authorization_policy_violation' => 'Fout - Geen toegang',
171173
'error_authorization_policy_violation_desc' => 'Neem contact op met de helpdesk van %idpName% als je toegang tot %spName% wilt. Vermeld daarbij dat je probeerde in te loggen op %spName% en dat je werd tegengehouden door een autorisatieregel van %suiteName%, geconfigureerd door %idpName%.',
172174
'error_authorization_policy_violation_desc_no_idp_name' => 'Neem contact op met de helpdesk van je eigen %organisationNoun% als je toegang tot %spName% wilt. Vermeld daarbij dat je probeerde in te loggen op %spName% en dat je werd tegengehouden door een autorisatieregel van %suiteName%, geconfigureerd door jouw eigen %organisationNoun%.',

languages/messages.pt.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@
165165
'error_session_lost_desc' => '<p>Esta ação requer uma sessão ativa, no entanto, não conseguimos encontrar a sessão. Está a aguardar há muito tempo? Feche o browser e tente novamente, ou tente um browser diferente.</p>',
166166
'error_session_not_started' => 'Erro - a sua sessão não foi encontrada',
167167
'error_session_not_started_desc' => '<p>Esta ação requer uma sessão ativa, no entanto, não recebemos nenhum cookie de sessão. O browser deve aceitar cookies. Não utilize endereços do marcador ou link. Feche o browser e tente novamente, ou tente um browser diferente.</p>',
168+
'error_unsolicited_response' => 'Erro - Não foi possível concluir o acesso',
169+
'error_unsolicited_response_desc' => 'O seu acesso não pôde ser concluído porque o pedido de autenticação foi iniciado de uma forma não suportada. Foi enviado diretamente para esta aplicação pelo seu fornecedor de identidade (por exemplo, através de um marcador, mosaico do portal ou ligação guardada) sem primeiro iniciar uma sessão a partir desta aplicação. Isso não é suportado. Por favor, comece novamente a partir do serviço ao qual estava a tentar aceder e inicie sessão a partir daí.',
168170
'error_authorization_policy_violation' => 'Erro - Sem acesso',
169171
'error_authorization_policy_violation_desc' => 'Você autenticu-se com sucesso na %idpName%, mas infelizmente você não pode utilizar %spName% (o "Fornecedor de Serviço") porque não tem acesso. A %idpName% limita o acesso a %spName% com uma política de autorização. Entre em contacto com o suporte da %idpName% se acha que deve ser-lhe concedido acesso ao serviço.',
170172
'error_authorization_policy_violation_desc_no_idp_name' => 'Você autenticu-se com sucesso na sua %organisationNoun%, mas infelizmente você não pode utilizar %spName% (o "Fornecedor de Serviço") porque não tem acesso. A sua %organisationNoun% limita o acesso a %spName% com uma política de autorização. Entre em contacto com o suporte da sua %organisationNoun% se acha que deve ser-lhe concedido acesso ao serviço.',

library/EngineBlock/Corto/Module/Bindings.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ public function receiveResponse($serviceEntityId, $expectedDestination)
335335
// Make sure it has a InResponseTo (Unsolicited is not supported) but don't actually check that what it's
336336
// in response to is actually a message we sent quite yet.
337337
if ($sspResponse->getInResponseTo() === null) {
338-
throw new EngineBlock_Corto_Module_Bindings_Exception(
338+
throw new EngineBlock_Corto_Module_Bindings_UnsolicitedAssertionException(
339339
'Unsolicited assertion (no InResponseTo in message) not supported!'
340340
);
341341
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/**
4+
* Copyright 2026 SURFnet B.V.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
class EngineBlock_Corto_Module_Bindings_UnsolicitedAssertionException extends EngineBlock_Corto_Module_Bindings_Exception
20+
{
21+
}

src/OpenConext/EngineBlockBundle/Controller/FeedbackController.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,19 @@ public function unableToReceiveMessageAction()
7878
);
7979
}
8080

81+
#[Route(
82+
path: '/authentication/feedback/unsolicited-response',
83+
name: 'authentication_feedback_unsolicited_response',
84+
methods: ['GET']
85+
)]
86+
public function unsolicitedResponseAction(): Response
87+
{
88+
return new Response(
89+
$this->twig->render('@theme/Authentication/View/Feedback/unsolicited-response.html.twig'),
90+
400
91+
);
92+
}
93+
8194
#[Route(path: '/feedback/unknown-error', name: 'feedback_unknown_error', methods: ['GET'])]
8295
public function unknownErrorAction()
8396
{

src/OpenConext/EngineBlockBundle/EventListener/RedirectToFeedbackPageExceptionListener.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use EngineBlock_Corto_Module_Bindings_ClockIssueException;
3636
use EngineBlock_Corto_Module_Bindings_SignatureVerificationException;
3737
use EngineBlock_Corto_Module_Bindings_UnableToReceiveMessageException;
38+
use EngineBlock_Corto_Module_Bindings_UnsolicitedAssertionException;
3839
use EngineBlock_Corto_Module_Bindings_UnsupportedAcsLocationSchemeException;
3940
use EngineBlock_Corto_Module_Bindings_UnsupportedBindingException;
4041
use EngineBlock_Corto_Module_Bindings_UnsupportedSignatureMethodException;
@@ -111,6 +112,9 @@ public function onKernelException(ExceptionEvent $event)
111112
if ($exception instanceof EngineBlock_Corto_Module_Bindings_UnableToReceiveMessageException) {
112113
$message = 'Unable to receive message';
113114
$redirectToRoute = 'authentication_feedback_unable_to_receive_message';
115+
} elseif ($exception instanceof EngineBlock_Corto_Module_Bindings_UnsolicitedAssertionException) {
116+
$message = 'Unsolicited assertion (IdP-initiated SSO) not supported';
117+
$redirectToRoute = 'authentication_feedback_unsolicited_response';
114118
} elseif ($exception instanceof EngineBlock_Corto_Module_Services_SessionLostException) {
115119
$message = 'Session lost';
116120
$redirectToRoute = 'authentication_feedback_session_lost';

src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Bindings.feature

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,11 @@ Feature:
110110
Then no RelayState should be present
111111
And I pass through EngineBlock
112112
Then the url should match "functional-testing/Dummy%20SP/acs"
113+
114+
Scenario: EngineBlock rejects a SAMLResponse without InResponseTo (IdP-initiated SSO)
115+
Given the IdP omits InResponseTo from its response
116+
When I log in at "Dummy SP"
117+
And I pass through EngineBlock
118+
And I pass through the IdP
119+
Then the url should match "authentication/feedback/unsolicited-response"
120+
And I should see "Error - Sign-in could not be completed"

src/OpenConext/EngineBlockFunctionalTestingBundle/Features/Context/MockIdpContext.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,18 @@ public function theIdPIsConfiguredToNotSendAnAssertion()
301301
$this->mockIdpRegistry->save();
302302
}
303303

304+
/**
305+
* @Given /^the IdP omits InResponseTo from its response$/
306+
*/
307+
public function theIdpOmitsInResponseToFromItsResponse(): void
308+
{
309+
$idp = $this->mockIdpRegistry->getOnly();
310+
311+
$idp->omitInResponseTo();
312+
313+
$this->mockIdpRegistry->save();
314+
}
315+
304316
/**
305317
* @Given /^the IdP "([^"]*)" sends attribute "([^"]*)" with value "([^"]*)"$/
306318
* @param string $idpName

0 commit comments

Comments
 (0)