Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Changes:
* 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.
* The `0000-00-00 00:00:00` is added for clarity/consistency, as this is probably the default behaviour of your database already.
* Removed unused index `consent.deleted_at`. Delete this from your production database if it's there.
* Added a specific error page for unsolicited SAML responses (IdP-initiated SSO without a prior AuthnRequest).

## 7.1.0
[SBS](https://github.com/SURFscz/SBS) integration
Expand Down
2 changes: 2 additions & 0 deletions languages/messages.en.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@
'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.',
'error_session_not_started' => 'Error - No session found',
'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.',
'error_unsolicited_response' => 'Error - Sign-in could not be completed',
'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.',
'error_authorization_policy_violation' => 'Error - Access denied',
'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%.',
'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%.',
Expand Down
2 changes: 2 additions & 0 deletions languages/messages.nl.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@
'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.',
'error_session_not_started' => 'Fout - Geen sessie gevonden',
'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.',
'error_unsolicited_response' => 'Fout - Inloggen kon niet worden voltooid',
'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.',
'error_authorization_policy_violation' => 'Fout - Geen toegang',
'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%.',
'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%.',
Expand Down
2 changes: 2 additions & 0 deletions languages/messages.pt.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@
'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>',
'error_session_not_started' => 'Erro - a sua sessão não foi encontrada',
'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>',
'error_unsolicited_response' => 'Erro - Não foi possível concluir o acesso',
'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í.',
'error_authorization_policy_violation' => 'Erro - Sem acesso',
'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.',
'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.',
Expand Down
2 changes: 1 addition & 1 deletion library/EngineBlock/Corto/Module/Bindings.php
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ public function receiveResponse($serviceEntityId, $expectedDestination)
// Make sure it has a InResponseTo (Unsolicited is not supported) but don't actually check that what it's
// in response to is actually a message we sent quite yet.
if ($sspResponse->getInResponseTo() === null) {
throw new EngineBlock_Corto_Module_Bindings_Exception(
throw new EngineBlock_Corto_Module_Bindings_UnsolicitedAssertionException(
'Unsolicited assertion (no InResponseTo in message) not supported!'
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/**
* Copyright 2026 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.
*/

class EngineBlock_Corto_Module_Bindings_UnsolicitedAssertionException extends EngineBlock_Corto_Module_Bindings_Exception
{
}
13 changes: 13 additions & 0 deletions src/OpenConext/EngineBlockBundle/Controller/FeedbackController.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,19 @@ public function unableToReceiveMessageAction()
);
}

#[Route(
path: '/authentication/feedback/unsolicited-response',
name: 'authentication_feedback_unsolicited_response',
methods: ['GET']
)]
public function unsolicitedResponseAction(): Response
{
return new Response(
$this->twig->render('@theme/Authentication/View/Feedback/unsolicited-response.html.twig'),
400
);
}

#[Route(path: '/feedback/unknown-error', name: 'feedback_unknown_error', methods: ['GET'])]
public function unknownErrorAction()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use EngineBlock_Corto_Module_Bindings_ClockIssueException;
use EngineBlock_Corto_Module_Bindings_SignatureVerificationException;
use EngineBlock_Corto_Module_Bindings_UnableToReceiveMessageException;
use EngineBlock_Corto_Module_Bindings_UnsolicitedAssertionException;
use EngineBlock_Corto_Module_Bindings_UnsupportedAcsLocationSchemeException;
use EngineBlock_Corto_Module_Bindings_UnsupportedBindingException;
use EngineBlock_Corto_Module_Bindings_UnsupportedSignatureMethodException;
Expand Down Expand Up @@ -111,6 +112,9 @@ public function onKernelException(ExceptionEvent $event)
if ($exception instanceof EngineBlock_Corto_Module_Bindings_UnableToReceiveMessageException) {
$message = 'Unable to receive message';
$redirectToRoute = 'authentication_feedback_unable_to_receive_message';
} elseif ($exception instanceof EngineBlock_Corto_Module_Bindings_UnsolicitedAssertionException) {
$message = 'Unsolicited assertion (IdP-initiated SSO) not supported';
$redirectToRoute = 'authentication_feedback_unsolicited_response';
} elseif ($exception instanceof EngineBlock_Corto_Module_Services_SessionLostException) {
$message = 'Session lost';
$redirectToRoute = 'authentication_feedback_session_lost';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,11 @@ Feature:
Then no RelayState should be present
And I pass through EngineBlock
Then the url should match "functional-testing/Dummy%20SP/acs"

Scenario: EngineBlock rejects a SAMLResponse without InResponseTo (IdP-initiated SSO)
Given the IdP omits InResponseTo from its response
When I log in at "Dummy SP"
And I pass through EngineBlock
And I pass through the IdP
Then the url should match "authentication/feedback/unsolicited-response"
And I should see "Error - Sign-in could not be completed"
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,18 @@ public function theIdPIsConfiguredToNotSendAnAssertion()
$this->mockIdpRegistry->save();
}

/**
* @Given /^the IdP omits InResponseTo from its response$/
*/
public function theIdpOmitsInResponseToFromItsResponse(): void
{
$idp = $this->mockIdpRegistry->getOnly();

$idp->omitInResponseTo();

$this->mockIdpRegistry->save();
}

/**
* @Given /^the IdP "([^"]*)" sends attribute "([^"]*)" with value "([^"]*)"$/
* @param string $idpName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class MockIdentityProvider extends AbstractMockEntityRole

private $fromTheFuture = false;

private $omitInResponseTo = false;

public function singleSignOnLocation()
{
return $this->getSsoRole()->getSingleSignOnService()[0]->getLocation();
Expand Down Expand Up @@ -341,6 +343,18 @@ public function fromTheFuture()
return $this;
}

public function omitInResponseTo(): self
{
$this->omitInResponseTo = true;

return $this;
}

public function shouldOmitInResponseTo(): bool
{
return $this->omitInResponseTo;
}

public function shouldNotSendAssertions()
{
return $this->sendAssertions === false;
Expand Down Expand Up @@ -374,7 +388,7 @@ public function __sleep()
$role->setExtensions($extensions);
}

return ['name', 'descriptor', 'sendAssertions', 'turnBackTime', 'fromTheFuture'];
return ['name', 'descriptor', 'sendAssertions', 'turnBackTime', 'fromTheFuture', 'omitInResponseTo'];
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ public function createForEntityWithRequest(
// Note that we expect the Mock IdP to always have a 'template' Response.
$response = $mockIdp->getResponse();

$this->setResponseReferencesToRequest($request, $response);
if ($mockIdp->shouldOmitInResponseTo()) {
$response->setInResponseTo(null);
} else {
$this->setResponseReferencesToRequest($request, $response);
}

$this->setResponseStatus($mockIdp, $response);

Expand Down
Loading