Created
May 4, 2016 12:13
-
-
Save ntoniazzi/cc9a5f020f9bbde0d9dea7d53ce2e6e1 to your computer and use it in GitHub Desktop.
Single Logout with LightSaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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]] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Dealing with an IndexOutOfBounds exception when calling /logout when there are no sessions active - line 119
https://gist.github.com/marnixavans/fdb4d144092f6c157ab52e0f71ab68e0