codegreencreative/laravel-samlidp

[QUESTION] Initiate SAML Single Sign On from Identity Provider

Closed this issue · 22 comments

In a nutshell, is it possible to initiate the SAML SSO flow from the IdP?

Currently it looks as if the SP has to make a request to your Laravel application for the request parameters to be filled out?

Is there a way that a logged in user at the IdP end could press a button and initiate the SAML SSO flow with the SP?

@blorange2 simply go to a protected route at the SP. If you are not logged in, the SP ask for authorization from the IdP. We do this all the time.

@upwebdesign so in this case I wouldn't need to send an assertion in the first instance?

@blorange2 right, simply attempt to access the SP.

@upwebdesign without sounding pretentious, does this work just as well as sending an assertion? I'm assuming you'd hit the SP and it would redirect to the login route of the identity provider which eventually would trigger the SSO flow?

@blorange2 that is correct. I found this flow to make more sense. I did not want to keep track of SP's to generate assertions.

@upwebdesign final question - as the package requires light saml anyway is it possible to just have a controller whose only function is to pass this assertion to the SP? Essentially as the package is doing 95% of the work is it weird to just add in an extra part away from the package?

@blorange2 if you would create a pull request with this functionality and it works as it should, I would be happy to make it part of this package! Thank you for the deep thought on this, I just never needed this type of initiation.

@upwebdesign okay, I will try to implement the intended behaviour and create a pull request. On a side note, I have another point. My stupid hosting provider has enabled PHP short tags as in <? which is very annoying. This is because it breaks the metadata blade.

If its not implemented already do you think it would make sense to echo out the XML if possible?

@blorange2 I would prefer not to modify the code to handle php short tags. It's such bad practice I really don't want to include it in this package. Are you able to provide your own php.ini file in your public folder with your hosting provider? Some allow you to modify the php ini file within your project. You should consider moving to a hosting provider that allows you full control over your server.

@upwebdesign that's fair enough. As Laravel is served from the public folder I believe, I'll try to create a php.ini in the public folder. I know this wasn't strictly related so thanks for answering. Hopefully I have some time on the weekend or next week to start my actual pull request for my original issue.

@upwebdesign I now see the difficulty as IdP initiated SSO is literally identical in nature to that of your Sso job.


<?php

namespace App\Http\Controllers\Saml;

use App\Http\Controllers\Controller;
use CodeGreenCreative\SamlIdp\Traits\PerformsSingleSignOn;

class LoginController extends Controller
{
    use PerformsSingleSignOn;

    /**
     * Initialise a new constructotr instance.
     */
    public function __construct()
    {
        $this->init();
    }

    /**
     * Display the login form for accessing SAML
     *
     * @return void
     */
    public function showLoginForm()
    {
        return view('user.custodian');
    }

    /**
     * Attempt to log into a given Service Provider.
     *
     * @return void
     */
    public function login()
    {
        $this->destination = 'https://services-uat.investrandx.com/SingleSignOn/ServiceProvider';

        return $this->samlResponse();
    }

    /**
     * Send a SAML message to the intended Service Provider with security assertions and other bearer assertions.
     * In this case we're explicitly sending email and name id
     *
     * @link https://www.lightsaml.com/LightSAML-Core/Cookbook/How-to-make-Response/
     *
     * @return void
     */
    public function samlResponse()
    {
        // Create a new SAML Response instance
        $this->response = new \LightSaml\Model\Protocol\Response();

        // Create a response object that we'll send later.
        $this->response
            ->addAssertion($assertion = new \LightSaml\Model\Assertion\Assertion())
            ->setStatus(
                new \LightSaml\Model\Protocol\Status(
                    new \LightSaml\Model\Protocol\StatusCode(
                        \LightSaml\SamlConstants::STATUS_SUCCESS
                    )
                )
            )
            ->setID(\LightSaml\Helper::generateID())
            ->setIssueInstant(new \DateTime())
            ->setDestination($this->destination)
            ->setIssuer(new \LightSaml\Model\Assertion\Issuer($this->issuer));

        // Build out our Assertion instance
        $assertion
            ->setId(\LightSaml\Helper::generateID())
            ->setIssueInstant(new \DateTime())
            ->setIssuer(new \LightSaml\Model\Assertion\Issuer($this->issuer))
            ->setSubject(
                (new \LightSaml\Model\Assertion\Subject())
                    ->setNameID(new \LightSaml\Model\Assertion\NameID(
                        auth()->user()->email,
                        \LightSaml\SamlConstants::NAME_ID_FORMAT_EMAIL
                    ))
                    ->addSubjectConfirmation(
                        (new \LightSaml\Model\Assertion\SubjectConfirmation())
                            ->setMethod(\LightSaml\SamlConstants::CONFIRMATION_METHOD_BEARER)
                            ->setSubjectConfirmationData(
                                (new \LightSaml\Model\Assertion\SubjectConfirmationData())
                                    ->setNotOnOrAfter(new \DateTime('+1 MINUTE'))
                                    ->setRecipient($this->destination)
                            )
                    )
            )
            ->setConditions(
                (new \LightSaml\Model\Assertion\Conditions())
                    ->setNotBefore(new \DateTime())
                    ->setNotOnOrAfter(new \DateTime('+1 MINUTE'))
                    ->addItem(
                        new \LightSaml\Model\Assertion\AudienceRestriction([$this->destination])
                    )
            )
            ->addItem(
                (new \LightSaml\Model\Assertion\AttributeStatement())
                    ->addAttribute(new \LightSaml\Model\Assertion\Attribute(
                        \LightSaml\ClaimTypes::EMAIL_ADDRESS,
                        auth()->user()->email,
                    ))
                    ->addAttribute(new \LightSaml\Model\Assertion\Attribute(
                        \LightSaml\ClaimTypes::NAME_ID,
                        auth()->user()->id,
                    ))
            )
            ->addItem(
                (new \LightSaml\Model\Assertion\AuthnStatement())
                    ->setAuthnInstant(new \DateTime('-10 MINUTE'))
                    ->setSessionIndex(\LightSaml\Helper::generateID())
                    ->setAuthnContext(
                        (new \LightSaml\Model\Assertion\AuthnContext())
                            ->setAuthnContextClassRef(\LightSaml\SamlConstants::AUTHN_CONTEXT_PASSWORD_PROTECTED_TRANSPORT)
                    )
            );

        return $this->sendSAMLResponse();
    }

    /**
     * Send a SAML response to the Service Provider and display the end result to the user.
     * If this is successful it should log the user into the SP.
     *
     * @param \LightSaml\Model\Protocol\Response $response
     *
     * @return void
     */
    private function sendSAMLResponse()
    {
        $bindingFactory = new \LightSaml\Binding\BindingFactory();
        $postBinding = $bindingFactory->create(\LightSaml\SamlConstants::BINDING_SAML2_HTTP_POST);
        $messageContext = new \LightSaml\Context\Profile\MessageContext();
        $messageContext->setMessage($this->response)->asResponse();
        $httpResponse = $postBinding->send($messageContext);

        return $httpResponse->getContent();
    }
}


The only difference being that we're not have to deal with an incoming request from the SP.

@upwebdesign I played with this when I had a moment and one thing I evidently forgot was to include the certificates. As I'm importing the SingleSignOn trait anyway, you can just include it as you did in the original job class.


<?php

namespace App\Http\Controllers\Saml;

use App\Http\Controllers\Controller;
use CodeGreenCreative\SamlIdp\Traits\PerformsSingleSignOn;

class LoginController extends Controller
{
    use PerformsSingleSignOn;

    /**
     * Initialise a new constructotr instance.
     */
    public function __construct()
    {
        $this->init();
    }

    /**
     * Display the login form for accessing SAML
     *
     * @return void
     */
    public function showLoginForm()
    {
        return view('user.custodian');
    }

    /**
     * Attempt to log into a given Service Provider.
     *
     * @return void
     */
    public function login()
    {
        $this->destination = 'https://services-uk.sungarddx.com/SingleSignOn/ServiceProvider';

        return $this->samlResponse();
    }

    /**
     * Send a SAML message to the intended Service Provider with security assertions and other bearer assertions.
     * In this case we're explicitly sending email and name id
     *
     * @link https://www.lightsaml.com/LightSAML-Core/Cookbook/How-to-make-Response/
     *
     * @return void
     */
    public function samlResponse()
    {
        // Create a new SAML Response instance
        $this->response = new \LightSaml\Model\Protocol\Response();

        // Create a response object that we'll send later.
        $this->response
            ->addAssertion($assertion = new \LightSaml\Model\Assertion\Assertion())
            ->setStatus(
                new \LightSaml\Model\Protocol\Status(
                    new \LightSaml\Model\Protocol\StatusCode(
                        \LightSaml\SamlConstants::STATUS_SUCCESS
                    )
                )
            )
            ->setID(\LightSaml\Helper::generateID())
            ->setIssueInstant(new \DateTime())
            ->setDestination($this->destination)
            ->setIssuer(new \LightSaml\Model\Assertion\Issuer($this->issuer));

        // Build out our Assertion instance
        $assertion
            ->setId(\LightSaml\Helper::generateID())
            ->setIssueInstant(new \DateTime())
            ->setIssuer(new \LightSaml\Model\Assertion\Issuer($this->issuer))
            ->setSignature(new \LightSaml\Model\XmlDSig\SignatureWriter($this->certificate, $this->private_key))
            ->setSubject(
                (new \LightSaml\Model\Assertion\Subject())
                    ->setNameID(new \LightSaml\Model\Assertion\NameID(
                        auth()->user()->email,
                        \LightSaml\SamlConstants::NAME_ID_FORMAT_EMAIL
                    ))
                    ->addSubjectConfirmation(
                        (new \LightSaml\Model\Assertion\SubjectConfirmation())
                            ->setMethod(\LightSaml\SamlConstants::CONFIRMATION_METHOD_BEARER)
                            ->setSubjectConfirmationData(
                                (new \LightSaml\Model\Assertion\SubjectConfirmationData())
                                    ->setNotOnOrAfter(new \DateTime('+1 MINUTE'))
                                    ->setRecipient($this->destination)
                            )
                    )
            )
            ->setConditions(
                (new \LightSaml\Model\Assertion\Conditions())
                    ->setNotBefore(new \DateTime())
                    ->setNotOnOrAfter(new \DateTime('+1 MINUTE'))
                    ->addItem(
                        new \LightSaml\Model\Assertion\AudienceRestriction([$this->destination])
                    )
            )
            ->addItem(
                (new \LightSaml\Model\Assertion\AttributeStatement())
                    ->addAttribute(new \LightSaml\Model\Assertion\Attribute(
                        \LightSaml\ClaimTypes::EMAIL_ADDRESS,
                        auth()->user()->email,
                    ))
                    ->addAttribute(new \LightSaml\Model\Assertion\Attribute(
                        \LightSaml\ClaimTypes::NAME_ID,
                        auth()->user()->id,
                    ))
            )
            ->addItem(
                (new \LightSaml\Model\Assertion\AuthnStatement())
                    ->setAuthnInstant(new \DateTime('-10 MINUTE'))
                    ->setSessionIndex(\LightSaml\Helper::generateID())
                    ->setAuthnContext(
                        (new \LightSaml\Model\Assertion\AuthnContext())
                            ->setAuthnContextClassRef(\LightSaml\SamlConstants::AUTHN_CONTEXT_WINDOWS)
                    )
            );

        return $this->sendSAMLResponse();
    }

    /**
     * Send a SAML response to the Service Provider and display the end result to the user.
     * If this is successful it should log the user into the SP.
     *
     * @param \LightSaml\Model\Protocol\Response $response
     *
     * @return void
     */
    private function sendSAMLResponse()
    {
        $bindingFactory = new \LightSaml\Binding\BindingFactory();
        $postBinding = $bindingFactory->create(\LightSaml\SamlConstants::BINDING_SAML2_HTTP_POST);
        $messageContext = new \LightSaml\Context\Profile\MessageContext();
        $messageContext->setMessage($this->response)->asResponse();
        $httpResponse = $postBinding->send($messageContext);

        return $httpResponse->getContent();
    }
}

Having thought about this some more, what I could do in principle is create another job like you have already, something like IdentityProviderSingleSignOn which a user could just call within any controller method.

What do you think about this?

As either way I then intend to use the Single Sign Out job when leaving the SP.

Currently I feel like I'm just hijacking the package so I hope this can be made useful.

@upwebdesign Adding to the above, did you have to remove login and logout from being checked in the VerifyCsrfToken middleware to avoid hitting a token mismatch exception?

@blorange2 is this issue resolved? Let me know and I will close this issue.

@upwebdesign technically yes, I've been trying to think of ways to implement the above in the package but here's another caveat, my SP doesn't gzip the contents of their authn requests. Face palm moment.

Yes, we can close this as it seems to again be SP related.

@blorange2 I'm new to SAML. Do you mind clarifying something for me? Let's say you have apps A and B acting as SP's, and Laravel with this package as the IdP.

If you log into app A, you'll actually be sent to Laravel to authenticate, so both app A and Laravel will be authenticated right? But app B will not be auth'd?

Am I right thinking this GitHub issue means that you cannot log into Laravel and automatically also be authenticated immediately in app A and app B?

@jeff-h essentially what is happening with app A is it is realizing that they are not logged in. App A will then create a SAML request to the IdP for authentication. If the user is logged in at the IdP, a SAML response is provided back to app A with the authenticated users data for app A to log in. Nothing happens with app B until the user attempts to access app B. SAML logout will log the user out of all SP's.

Ah, is there any efficient way for the IdP to "push a user's login" to one or more SP's?

BTW, thanks @upwebdesign — educating random strangers on how SAML works is definitely going above & beyond, but greatly appreciated :)

@jeff-h Hi, in answer to your original question, my issue was related to my Service Provider having a massively botched setup which included a work around for not implementing Single Sign Out. If your SP is set up correctly however, this package does everything I think you would need?

@jeff-h is there any particular reason why you would want to have the user logged into multiple SP's when they are simply trying to access one of them? I would recommend not disrupting the natural flow a user has on their journey, ie, log them into the services they are trying to access.

We have an old Drupal site which we're slowly migrating away from. It is themed exactly the same as the Laravel site and the intention is for users to be logged into both, so they won't even realise the site is powered by two backends. Some pages will be served by one, some by the other.

They're actually running in Docker containers on the exact same host, so I am wondering if SAML is actually overkill for this, given there are probably numerous secure ways the two backends could communicate. I do have a working bespoke proof-of-concept but there always seem to be so many edge-cases to cater for hence me looking into SAML.

Thank you for this package. You did a great job.
I am concerned by the IDP initiated SAML, as my SP request a click on "Connect with SSO" to generate the request. I want to avoid this and improve the user experience, as a lot of people have no idea what SSO is, and don't click to continue.
It's quite a long time, but @blorange2, did you manage to finalize your class, to include it in the package ? or maybe you have the final version of your class ?
Thank you guys for your help.