Skip to content

Instantly share code, notes, and snippets.

@ntoniazzi
Created May 4, 2016 12:13
Show Gist options
  • Save ntoniazzi/cc9a5f020f9bbde0d9dea7d53ce2e6e1 to your computer and use it in GitHub Desktop.
Save ntoniazzi/cc9a5f020f9bbde0d9dea7d53ce2e6e1 to your computer and use it in GitHub Desktop.
Single Logout with LightSaml
# routing.yml, in the main project or a dedicated bundle
lightsaml_sp:
resource: "@LightSamlSpBundle/Resources/config/routing.yml"
prefix: saml
lightsaml_sp.logout:
path: /saml/logout
lightsaml_sp.denied:
path: /saml/error
defaults:
# simple action that throws an AccessDeniedException
_controller: AcmeBundle:Routing:accessDenied
<?php
/* SamlLogoutHandler.php, in the main project or a dedicated bundle */
namespace AcmeBundle\Lib;
use Symfony\Component\Security\Http\Logout\LogoutSuccessHandlerInterface;
use Exception;
use LightSaml\Binding\AbstractBinding;
use LightSaml\Binding\BindingFactory;
use LightSaml\Context\Profile\MessageContext;
use LightSaml\Model\Assertion\Issuer;
use LightSaml\Model\Assertion\NameID;
use LightSaml\Model\Metadata\EntityDescriptor;
use LightSaml\Model\Metadata\SingleLogoutService;
use LightSaml\Model\Protocol\LogoutRequest;
use LightSaml\Model\Protocol\LogoutResponse;
use LightSaml\Model\Protocol\SamlMessage;
use LightSaml\Model\Protocol\Status;
use LightSaml\Model\Protocol\StatusCode;
use LightSaml\SamlConstants;
use LightSaml\State\Sso\SsoSessionState;
use LightSaml\SymfonyBridgeBundle\Bridge\Container\BuildContainer;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class SamlLogoutHandler implements LogoutSuccessHandlerInterface
{
private $container;
public function setContainer($container)
{
$this->container = $container;
return $this;
}
/**
* {@inheritdoc}
*/
public function onLogoutSuccess(Request $request)
{
$bindingFactory = new BindingFactory();
$bindingType = $bindingFactory->detectBindingType($request);
if (null === $bindingType) {
// no SAML request: initiate logout
return $this->sendLogoutRequest();
}
$messageContext = new MessageContext();
$binding = $bindingFactory->create($bindingType);
/* @var $binding AbstractBinding */
$binding->receive($request, $messageContext);
$samlRequest = $messageContext->getMessage();
if ($samlRequest instanceof LogoutResponse) {
// back from IdP after all other SP have been disconnected
$status = $samlRequest->getStatus();
$code = $status->getStatusCode() ? $status->getStatusCode()->getValue() : null;
if ($code === SamlConstants::STATUS_PARTIAL_LOGOUT || $code === SamlConstants::STATUS_SUCCESS) {
// OK, logout
$session = $request->getSession();
$session->invalidate();
// redirect to wherever you want
return new \Symfony\Component\HttpFoundation\RedirectResponse(
$this->container->get('router')->generate('default')
);
}
// TODO: handle errors from IdP
} elseif ($samlRequest instanceof LogoutRequest) {
// logout request from IdP, initiated by another SP
$response = $this->sendLogoutResponse($samlRequest);
// clean session
$session = $request->getSession();
$session->invalidate();
return $response;
}
throw new Exception('request not handled');
}
/**
* Send a logout request to the IdP
*
* @return Response
*/
private function sendLogoutRequest()
{
// <LogoutRequest
// xmlns="urn:oasis:names:tc:SAML:2.0:protocol"
// ID="_6210989d671b429f1c82467626ffd0be990ded60bd"
// Version="2.0"
// IssueInstant="2013-11-07T16:07:25Z"
// Destination="https://b1.bead.loc/adfs/ls/"
// NotOnOrAfter="2013-11-07T16:07:25Z"
// >
// <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
// https://mt.evo.team/simplesaml/module.php/saml/sp/metadata.php/default-sp
// </saml:Issuer>
// <saml:NameID
// xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
// Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
// >
// user
// </saml:NameID>
// <SessionIndex>_677952a2-7fb3-4e7a-b439-326366e677db</SessionIndex>
// </LogoutRequest>
$builder = $this->container->get('lightsaml.container.build');
/* @var $builder BuildContainer */
$sessions = $builder->getStoreContainer()->getSsoStateStore()->get()->getSsoSessions();
$session = $sessions[count($sessions) - 1];
/* @var $session SsoSessionState */
$idp = $builder->getPartyContainer()->getIdpEntityDescriptorStore()->get(0);
/* @var $idp EntityDescriptor */
$slo = $idp->getFirstIdpSsoDescriptor()->getFirstSingleLogoutService();
/* @var $slo SingleLogoutService */
$logoutRequest = new LogoutRequest();
$logoutRequest
->setSessionIndex($session->getSessionIndex())
->setNameID(new NameID(
$session->getNameId(), $session->getNameIdFormat()
))
->setDestination($slo->getLocation())
->setID(\LightSaml\Helper::generateID())
->setIssueInstant(new \DateTime())
/* here, the SP entity id is a container parameter, change it as you wish */
->setIssuer(new Issuer($this->container->getParameter('saml.entity_id')))
;
$context = new MessageContext();
$context->setBindingType($slo->getBinding());
$context->setMessage($logoutRequest);
$bindingFactory = $this->container->get('lightsaml.service.binding_factory');
/* @var $bindingFactory BindingFactory */
$binding = $bindingFactory->create($slo->getBinding());
/* @var $binding AbstractBinding */
$response = $binding->send($context);
return $response;
}
/**
* Send a Success response to a logout request from the IdP
*
* @param SamlMessage $samlRequest
*
* @return Response
*/
private function sendLogoutResponse(SamlMessage $samlRequest)
{
// <LogoutResponse
// xmlns="urn:oasis:names:tc:SAML:2.0:protocol"
// xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
// ID="_6c3737282f007720e736f0f4028feed8cb9b40291c"
// Version="2.0"
// IssueInstant="2014-07-18T01:13:06Z"
// Destination="http://sp.example.com/demo1/index.php?acs"
// InResponseTo="ONELOGIN_21df91a89767879fc0f7df6a1490c6000c81644d"
// >
// <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
// <samlp:Status>
// <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
// </samlp:Status>
// </samlp:LogoutResponse>
$builder = $this->container->get('lightsaml.container.build');
/* @var $builder BuildContainer */
$idp = $builder->getPartyContainer()->getIdpEntityDescriptorStore()->get(0);
/* @var $idp EntityDescriptor */
$slo = $idp->getFirstIdpSsoDescriptor()->getFirstSingleLogoutService();
/* @var $slo SingleLogoutService */
$message = new LogoutResponse();
$message
->setRelayState($samlRequest->getRelayState())
->setStatus(new Status(
new StatusCode(SamlConstants::STATUS_SUCCESS)
))
->setDestination($slo->getLocation())
->setInResponseTo($samlRequest->getID())
->setID(\LightSaml\Helper::generateID())
->setIssueInstant(new \DateTime())
/* here, the SP entity id is a container parameter, change it as you wish */
->setIssuer(new Issuer($this->container->getParameter('saml.entity_id')))
;
$context = new MessageContext();
$context->setBindingType($slo->getBinding());
$context->setMessage($message);
$bindingFactory = $this->container->get('lightsaml.service.binding_factory');
/* @var $bindingFactory BindingFactory */
$binding = $bindingFactory->create($slo->getBinding());
/* @var $binding AbstractBinding */
$response = $binding->send($context);
return $response;
}
}
# main security.yml
security:
providers:
default:
id: acme.user_provider
firewalls:
main:
light_saml_sp:
provider: default
login_path: /saml/login
check_path: /saml/login_check
failure_path: security_error
attribute_mapper: acme.attribute_mapper
logout:
path: lightsaml_sp.logout
target: default
invalidate_session: false
success_handler: acme.logout_handler
anonymous: ~
access_control:
- { path: ^/saml/, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
- { path: ^/, roles: ROLE_USER, requires_channel: https }
# services.yml, in the main project or a dedicated bundle
services:
acme.attribute_mapper:
# […]
acme.logout_handler:
class: AcmeBundle\Lib\SamlLogoutHandler
calls:
- [setContainer, [@service_container]]
@marnixavans
Copy link

marnixavans commented Feb 23, 2017

Dealing with an IndexOutOfBounds exception when calling /logout when there are no sessions active - line 119
https://gist.github.com/marnixavans/fdb4d144092f6c157ab52e0f71ab68e0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment