Please upgrade here. These earlier versions are no longer being updated and have security issues.
HackerOne users: Testing against this community violates our program's Terms of Service and will result in your bounty being denied.

SAML SSO

I've been looking to extend an SSO system we have (using SAML) to provide "login-free" referrals to our
Vanilla forum.

Looking at the contributed plugins (and the bundled jsconnect), it appears that all of these are not what
I would term SSO. They all appear to present a username/password dialogue and then use external APIs
to validate user authenitcation (rather than have other system assert their credentials into Vanilla). Is
there a way to have a plugin take in incoming request and (using code I've written in php) validate the
SAML assertion and authenticate a user using that token?

I've had a look around the documentation (which is a bit spare when it comes to the API) and a few of
the plugins, but cannot seem to fathom a way to do this. Answers on a postcard! :)

Best Answer

Answers

  • @dravster said:
    Looking at the contributed plugins (and the bundled jsconnect), it appears that all of these are not what
    I would term SSO. They all appear to present a username/password dialogue and then use external APIs
    to validate user authenitcation.

    I'm not sure I understand the highlighted statement. I use JsConnect, and it doesn't display any User/Password dialogue to the User. The same applies to Google SSO, or any OpenID. They present a dialogue the first time a User connects to the site, but it's just to collect additional information (such as a nickname or an email address) if they haven't been provided by the OpenID website.

  • dravsterdravster New
    edited March 2013

    The SSO's i've worked with before assert the user credentials forward to the provider, who validates the assertion and then redirects the user to the "authenticated" content.
    All of the openID, google ID redirect you to login boxes. JS connect connects off to some random code you have to write outside of the vanilla environment (and involved cookies and other things).

    What I'm asking is, given a posted data (which contains the assertion), what parts of the API do I use to put in my assertion verification code and place/redirect the user onto vanilla as "authenticated"?

    Hopefully that makes what I'm asking a little clearer! :)

  • The first dialogue has to appear, so that the User can sign in into his Google account. His token will then be shared with Vanilla, who will log him in automatically. Without such dialogue, there would be no session anywhere, and Vanilla would not be able to fetch it. In short, the User must log in somewhere, at least once.

    I'm not familiar with the second one, though. The OpenID URL is the one that will be used by Vanilla to communicate with an OpenID provider. I reckon that that is the dialogue which is displayed when such URL has not been configured.

  • That's the point though, the users HAVE already signed in (in our own website which has it's own authentication system), and I'm trying to provide a single sign on over to the Vanilla Forum. I'm trying to work out how I would achieve this.

  • edited March 2013

    @dravster said:
    That's the point though, the users HAVE already signed in (in our own website which has it's own authentication system), and I'm trying to provide a single sign on over to the Vanilla Forum. I'm trying to work out how I would achieve this.

    I think I get why it was confusing. As you wrote, Google, OpenID & co display a login form; however, this happens only if the user is not already authenticated with them. In your case, I understand that you want to use your custom site to act as an ID provider using SAML. As far as I know, such protocol is not currently supported by Vanilla, therefore you would need a custom plugin for it.

    The good news is that, based on what the SAML use case I found, it seems that its flow would similar to the one implemented by existing JsConnect plugin (more details about it below).

    How does JsConnect work
    The way SSO works is that the client website (in your case, Vanilla) asks the Identity Provider to vouch for a User as soon as he connects. The Provider will then reply with a "User logged in, here are the details", or a "No, User not logged in", and Vanilla will act accordingly.

    Here's a summary of JsConnect flow. It doesn't go into technical details, it's just meant to illustrate the logic from a User perspective:

    • User visits Vanilla.
    • A JavaScript runs on page load and contacts the Provider, to see if User is logged in there.
    • If User is logged in, a link called "Hello Mr Somebody, log in with Provider" is displayed. If not, link just states "Log in with Provider".

    When User clicks on the link, one of two things happens:

    • If User is not logged in, he gets redirected to the Provider to log in. The Provider will then have to send him back to Vanilla.
    • If User is logged in, its details are posted to Vanilla, which then authenticates him.

    An additional plugin, called JsAutoconnect, can automate the flow and trigger the whole procedure whenever a User visits Vanilla, thus removing the need to click on the "Log in with Provider" button.

    Differences with SAML
    Although the flow is similar, there are some key differences between JsConnect and SAML logic:

    JsConnect

    • Expects data in JSON format.
    • Response data simply contains User details, which will be used to create an internal User account.
    • Validation is done via a simple hash and a timestamp.

    SAML

    • Returns a SAMLAssertion, which is XML.
    • Assertion contains a lot more validation data and, I guess, also User details (I don't have a SAML Assertion to analyse).
    • Validation would require parsing the Assertion and performing the required checks.
  • JsConnect is a three step handshake. That is why it is approx 3x request time.

    grep is your friend.

  • As far as I know, such protocol is not currently supported by Vanilla, therefore you would need a custom plugin for it.

    Which is what I was asking how to do in the first instance. At least we're on the same page now.

    Several providers who we provide SSO for, only present us with a SAML assertion. There is no toing and froing as described above. They post an assertion which we procees, and either throw up an authentication failure page or redirect them to a authenticated session on our website. I'd like to code this in a plugin (as in please point me to parts of the API so I attempt to code this myself).

    I tried jsConnect and found it not to be of any use to the senario I've described previously. I've already (as described above) coded the SAML validation parts in php already, so just need to know how to slot this into a plugin.

  • Great, I did have a go at creating the "Hello World" plugin and got that to play. So I've at least go into the basics of a plugin (and took a look around the source code of the google login in and open id plugins). I'll take a look at your example and have a go at getting it all to work! Thanks for the time and the lack of clarity from my side of things!

  • @dravster said:
    Thanks for the time and the lack of clarity from my side of things!

    You're welcome. By the way, the lack of clarity is not your fault. Wherever XML and SOAP are involved, there must be lack of clarity. Sometimes I think they have been invented by a demon called "obfuscatiis". :D

  • @dravster: I've got a similar need (must avoid the js SSO options for same reasons as you) and will be going down this route in a few days. Have you got any code to share? Might save me a little time.

    cheers,
    mike

  • dravsterdravster New
    edited June 2013

    Apologies for the laggy response, but with many other work projects (including an office move!) I've been rather busy!

    In the end my solution ended up becoming an application (which may have been overkill - I'm not 100% sure the same solution could of been achieved with a plugin instead). The main reason I ended up going down this route was that the business had a requirement for the "Sign Out" button to be replaced with a "Back to Main Site" button. This essentially does a SAML assertion back to our parent website.

    This required some hefty hooks and additions not least of which was custom js in the forum's headers and a page to call inside the forum that returns JSON. It could of been argued these pages could of been done outside of the forum, but I wanted to keep it all in one place, so I could snap the changes in place with future upgrades of Vanilla. The JSON part seemed somewhat impossible to do (without it being wrapped in the forum's html layout). This may have been an incorrect assumption due to the fact that I first approached the problem using the CustomPages plugin - which by default wraps any pages in the website's layout.

    I achieved the above by adding some jquery registered with AddJsFile() via a Base_Render_Before() hook inside my applications settings/class.hooks.php file. This jquery adds a .click() bind to the sign out button. The .click() calls through a jquery AJAX call to my application's SSO controller controllers/class.jsoncontroller.php. The controller in turn builds the assertion and returns it as an JSON statement. The jquery then pulls the assertion from the JSON response, loads the result in to a hidden input on field on the page and submits it back to a cgi on the parent website. The html of the form/input field was placed into the website via a "template" in a custom Theme I added (inside the views/default.master.php file).

    The business had other requirements, they wanted user's names to be displayed as "Full Names" rather than a username (mostly achieved via the FirstLastNames plugin), for admins to be able to see what company the user was from (we run a service for many different customers) - which was added via a custom override in the Theme I created. The return site could also be one of several (so I had to store the website it came from) and also I needed to store a parent website "session identifier" so the users session could be identified on their return to the parent website. In the end I ended up adding several fields to the vanilla database via my applications settings/structure.php file to store these values per vanilla user.

    As for the SSO "inbound" to the forum, I had a separate controller in my application which was called by the referring parent site. The crux of which lived inside an Index() function. The controller checks $_POST for my base64 assertion, and if that is set, hands the POST variable to a custom written php library. The library base64 decodes this, validates the authenticity and if valid builds an array from the assertions AtrributeStatement which contains all the data mentioned above (First Name, Surname, Returning Site, Session ID, plus a unique user identifier used as the users username). The controller then uses Gdn::UserModel() to check if the user exists:

       $Where = array();
       $Where['Name'] = $Values['Name'];   /* $Values['Name'] is set to the unique username passed in the SAML */
       $TestData = $UserModel->GetWhere($Where);
    

    If they have a session already, clear it (or registration fails) via $Session = Gdn::Session();

       if($Session->IsValid()) Gdn::Authenticator()->AuthenticateWith('user')->DeAuthenticate();
    

    If the user does not exist, register them

       if ($TestData->NumRows() < 1) {
            $AuthUserID = $UserModel->Register($Values);
    .
    .
    

    error out if the registration failed:

       if (!$AuthUserID) {
    .
    .
            Redirect('/ssofailed');
    

    else select the newly created user

       $Match = $UserModel->GetWhere($Where)->FirstRow();
    

    build a Session:

        if (!Gdn::Session()->IsValid()) Gdn::Session()->Start($Match->UserID);
    

    If this creates a valid session, fire off triggers/events

    if (!Gdn::Session()->IsValid()) {
        $this->FireEvent('Authenticated');
        Gdn::Authenticator()->Trigger(Gdn_Authenticator::AUTH_SUCCESS);
    

    Throw them out if blocked:

        if (!Gdn::Session()->CheckPermission('Garden.SignIn.Allow')) {
            if(Gdn::Session()->IsValid()) Gdn::Authenticator()->AuthenticateWith('user')->DeAuthenticate();
            Gdn::Session()->End();
            Redirect('/accessdenied');
    

    Deal with time zones:

            if ($HourOffset != Gdn::Session()->User->HourOffset) {
                Gdn::UserModel()->SetProperty(Gdn::Session()->UserID, 'HourOffset', $HourOffset);
            }
    

    Update user details ($attributes being the array created by my custom SAML library):

            # update all of the passed attributes
            Gdn::SQL()->Update('User')
            ->Set('FirstName', $attributes['first_name'])
            ->Where('UserID', $Match->UserID)
            ->Put();
    

    (plus other db updates here)

    and redirect them to a "landing page"

    Redirect('/welcome');
    

    The three /ssofailed, /accessdenied and /welcome redirects are Dashboard entered Routes which route to pages created inside the CustomPages plugin (why re-invent the wheel? :). The forum is "SSO only" and as such I have another plugin which has a Base_Render_Before() call that checks if $_SERVER['REQUEST_URI'] is one of the above 3 redirects, the /entry page (so admins can log in), and the "this forum is closed" page it redirects to if it's not one of those urls (so users get a nice "This is for $Parent_Website users only" error page).

    For completion, I also had to rename "Discussions" to "Posts" (and a number of other cosmetic nomenclature) for our business requirements, which I achieved via a Locale(!).

    At some point I hope to strip out some of the more company sensitive parts of the code and publish it somewhere it can be downloaded (should any one need to attempt a similar method).

    Hope that helps!

  • Hey - did you ever publish this code? :)

Sign In or Register to comment.