PRayno / CasAuthBundle

Basic CAS (SSO) authenticator for Symfony (version 3,4 and 5)
MIT License
16 stars 19 forks source link

Add an event listener for Single Logout (SLO) #16

Open junowilderness opened 6 years ago

junowilderness commented 6 years ago

It should be possible to handle single logout signals from CAS servers as documented in the spec. This could perhaps be something that could be activated in the service container optionally.

junowilderness commented 6 years ago

Here is an example in PHP from Drupal: https://cgit.drupalcode.org/cas/tree/cas.module?h=7.x-1.x#n1111 Drupal stores sessions in a database by default, but the principle would be the same.

junowilderness commented 6 years ago

deleted

junowilderness commented 6 years ago

Here is a working example for the basic case where sessions are stored on the filesystem. The hosting I use has a shared filesystem so there is only one file to delete.

There isn't a one-size-fits-all way to code the session deletion, but we could type hint an interface for doing so and implementations could do whatever is needed to delete a session, such as a database query, Redis, or whatever.

<?php

namespace App\Security\Cas;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

class SingleLogoutEventSubscriber implements EventSubscriberInterface
{
    /**
     * @var GetResponseEvent
     */
    protected $event;

    public static function getSubscribedEvents()
    {
        return [
            KernelEvents::REQUEST => ['singleLogout']
        ];
    }

    public function singleLogout(GetResponseEvent $event)
    {
        if ($event->getRequest()->get("logoutRequest", false)) {
            $this->event = $event;
            $this->processLogoutRequest();
        }
    }

    protected function processLogoutRequest(): void
    {
        $xmlString = utf8_encode(urldecode($this->event->getRequest()->get("logoutRequest")));
        $xmlData = new \SimpleXMLElement($xmlString);
        if (is_object($xmlData)) {
            $this->processXmlData($xmlData);
        }
    }

    protected function processXmlData($xmlData): void
    {
        $sessions = $this->extractSessions($xmlData);
        if ($sessions) {
            $this->terminateSessions($sessions);
        }
    }

    protected function extractSessions($xmlData)
    {
        $namespaces = $xmlData->getNameSpaces();
        if (isset($namespaces['samlp'])) {
            $sessions = $xmlData->children($namespaces['samlp'])->SessionIndex;
        } else {
            $sessions = $xmlData->xpath('SessionIndex');
        }

        return $sessions;
    }

    protected function terminateSessions($sessions): void
    {
        $sessionId = (string) $sessions[0];
        $file = session_save_path().'/sess_'.trim($sessionId);
        if (file_exists($file)) {
            unlink($file);
        }
        $this->event->setResponse(new Response());
    }
}