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
-
businessdad MVP
I still can't visualise the flow of the authentication mechanism you describe (mainly who posts the assertion to Vanilla and how), but anyway:
- You can start with the Example plugin, which will provide you with a skeleton.
- Once you have created your plugin's endpoint (i.e. the URL where the SAML assertion will be posted), you can use your code to validate it.
- To redirect to a failure page, simply use
Redirect()
. - To start a new session, you can use
Gdn::Session()->Start()
.
You can see how a session is started with normal authentication in
EntryController::SignIn()
. Here's the snippet that does it:if ($PasswordHash->CheckPassword($this->Form->GetFormValue('Password'), GetValue('Password', $User), GetValue('HashMethod', $User))) { Gdn::Session()->Start(GetValue('UserID', $User), TRUE, (bool)$this->Form->GetFormValue('RememberMe')); if (!Gdn::Session()->CheckPermission('Garden.SignIn.Allow')) { $this->Form->AddError('ErrorPermission'); Gdn::Session()->End(); } else { if ($HourOffset != Gdn::Session()->User->HourOffset) { Gdn::UserModel()->SetProperty(Gdn::Session()->UserID, 'HourOffset', $HourOffset); } $this->_SetRedirect(); } } else { $this->Form->AddError('ErrorCredentials'); }
6
Answers
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.
My shop | About Me
as attached
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.
My shop | About Me
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:
When User clicks on the link, one of two things happens:
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
SAML
My shop | About Me
JsConnect is a three step handshake. That is why it is approx 3x request time.
grep is your friend.
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.
I still can't visualise the flow of the authentication mechanism you describe (mainly who posts the assertion to Vanilla and how), but anyway:
Redirect()
.Gdn::Session()->Start()
.You can see how a session is started with normal authentication in
EntryController::SignIn()
. Here's the snippet that does it:My shop | About Me
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!
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".
My shop | About Me
@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
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 aBase_Render_Before()
hook inside my applicationssettings/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 controllercontrollers/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 theviews/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 usesGdn::UserModel()
to check if the user exists:If they have a session already, clear it (or registration fails) via
$Session = Gdn::Session();
If the user does not exist, register them
error out if the registration failed:
else select the newly created user
build a Session:
If this creates a valid session, fire off triggers/events
Throw them out if blocked:
Deal with time zones:
Update user details ($attributes being the array created by my custom SAML library):
(plus other db updates here)
and redirect them to a "landing page"
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?