Wednesday, 15 May 2013

Implementing a Simple SSO solution for TeamMentor (based on long MD5 shared key)

For one of the 3rd party apps we are integrating TM with (lets call it app XYZ),  there is a requirement to have the users from those websites to be able to automatically login into TeamMentor (TM).

Note that the solution I implemented is based on a variation of that 3rd party application SSO solution, which allows the login into their application using this worklow:
  • There is a SharedKey between both services (TM and XYZ)
  • TM redirects to XYZ with a special token which is made of MD5 of 'SharedKey +email' and the actual email. This is a GET request that looks like: /sso?requestToken=[md5({SharedKey}+{email})]&email={email}
  • XYZ app checks that the email value received matches the MD5 provided in the requestToken value, and if it does, and it is a valid user in their system, XYZ will redirect to TM with an unique token called responseToken
  • the TM server should reply back to XYZ server with an MD5 of the  responseToken + SharedKey. This is a GET request that looks lke: /sso?confirmToken=[md5(responseToken +SharedKey)] 
  • if confirmToken is good, then the user will be logged in into XYZ system

The problem is that this is authenticating TM users in XYZ, and what we need is to authenticate XYZ users in TM (there are also a number of other security problems with the SSO solution described above, can you spot them?)

Here are the  proposed plan/requirements to order to allow XYZ users to login in TM:
  • There is a SharedKey between both services (TM and XYZ)
  • XYZ will add a link to its website that will point to[md5({SharedKey}+{email})]&email={email} (i.e. the 1st step of the TM to XYZ SSO sequence described above
  • TM will check that the MD5 matches and:
    • If the user exists in TM logs the user in (which will set the user’s TM SessionID cookie)
    • If the user doesn’t exist, create a new user with that email and log the user in (also setting user’s TM SessionID cookie)
  • Once the user is logged in into TeamMentor he should not be automatically logged out on browser close
  • In this first implementation (for a shared client), there should be no changes to the main TeamMentor codebase (i.e. the currently released version)
And yes there are also a number of potential security issues/weakness in this SSO solution (can you spot them too?)

The next part of this post shows how the first working version of this solution was implemented in TM.

From 3.3, TM supports the customization of its deployed version via the 'site specific UserData repository', which can have extra Html/Js/Aspx/Razor files that can be added to a deployed TM instance (i.e. those files and copied ‘on top of the ‘git pulled web root’ files)

This means that if open the current test TM site User_Data folder


and add a folder called WebRoot_Files (with a test file in it)


and use Tbot’s to reload the TM cache:


That test file will be copied to the web root:


and available via the browser (btw, note how git detected the new file in the git managed folder)


Now, it is not a good practice to put customization files on the web root, so usually the recommendation is that all extra code is placed on a _Customizations folder


and if the new file is an *.aspx:


we can run c# code in there:


Note: there is already C# Razor support in TM (which is what Tbot uses) , BUT at the moment TBot will demand admin privileges, so it can’t be used anonymously. There are also other ways to hook the TM request pipeline, but I think in this case a simple ASPX page will do the trick.

From this ASPX page (renamed to SSO.aspx), it is quite easy to access the main TM objects:


which looks like this:


Tip: For the cases where VisualStudio is being used to create aspx pages like this one, it will make my life easier if to automate a bit the testing workflow.

So I opened up the O2’s VisualStudio C# REPL and wrote this script (which created a VS native window with a web browser control)


which can then be used side-by-side with the script under development:


Ok, back to the script,

Let’s start with a simple case of creating users based on a url parameter


which looks like this when executed:


and if I provide an value that currently doesn’t exist in TM (note that the user search is done  username, not email)


a new user will be created that user in memory and in the file system:


All the 'user check and creation' action happens in these two lines (where there is an attempt to resolve a user based on the provided value, and if that value doesn’t exists, the user is created)


Note that there are a bunch of other methods that could be used to create a new user. The one used (with only the first value provided) will create a user with random data on all fields except the UserName (see below)


Now that we can create new users based on the a provided value, the next step is to log that user in.

Which can be done with a couple extra lines of code:


And now, every call to sso.asp will either create a new user, or login into an existing user.

For example http://local.:3187/_Customizations/sso.aspx?userName=test :


will login into the current user test:


and http://local.:3187/_Customizations/sso.aspx?userName=XYZ_User


will create and login the XYZ_User user (for that browser session):


Note: to make that SSO page look a bit better, we can just add a reference to bootstrap css


so it now looks like this:


OK, next we need to add the MD5 check and redirect back to the main page if all is good:


To test this we will need a better environment than just a browser.

So back in the VisualStudio C# REPL environment, lets create an GUI that has a browser and a code editor (connected to that browser)


The script above created this GUI, which when executed will fail the SSO (because we didn’t provide the correct values)


here is a better script (with the correct amount of values, but the wrong ssoKey)


and finally, here is the solution working :)

Login-in as an existing user (with url http://local:3187/_Customizations/sso.aspx?userName=asd@asdasd&requestToken=3854dfd50b3db0de8e408382b07f4383 ):


Login-in with a new user (with url http://local:3187/_Customizations/sso.aspx?


which as expected, created a new user in the local user store, and logged-in into TM as that user:


Note that if we paste that last URL in another browser:


we will be logged-in as that user (in that browser):


and wil; be logged-out in visual studio (where we were logged-in as that user)


and the user activity log will shown a number of new logins


This wraps up the current post.

We have the desired SSO solution which is easy to deploy, test and implement (in any 3.3 TM server).

Finally, can you think of better/more-secure ways to implement this SSO solution?

I have a number of ideas and PoCs that will be writing up in the next couple weeks, so I’ll be interrested in your views :)

Scripts used in this post:

1) version of the SSO.aspx page without MD5 check and with list of current users
   1: <%@ Page Language="C#"%>


   3: <%@ Import Namespace ="O2.DotNetWrappers.ExtensionMethods" %>

   4: <%@ Import Namespace="TeamMentor.CoreLib" %>


   6: <%

   7:     var xmlDatabase     = TM_Xml_Database.Current;

   8:     var userData        = xmlDatabase.UserData;

   9:     var authentication  = new TM_Authentication(null);

  10:     var request         = HttpContextFactory.Request;    


  12:     var ssoKey          = "AAAAAAAAAa12345BBBBBBB";

  13:     var userName        = request["userName"];

  14:     var requestToken    = request["requestToken"];

  15:     var expectedToken   = (userName + ssoKey).md5Hash();



  18:     var tmUser = userName.tmUser();                 // see if there is a user with the provided value


  20:     if (tmUser.isNull())                            // if not

  21:         tmUser = userData.newUser(userName)         // create it (returns new userId)

  22:                          .tmUser();                 // and get the user object from the userId


  24:     var loginGuid = tmUser.login();                 // login user in TM   

  25:     authentication.sessionID = loginGuid;           // triggers the update of user's cookies


  27: %> 


  29: <h2>TM SSO page</h2>

  30: userName provided <%=userName.htmlEncode()%><br/>


  32: <br/>

  33: There are currently <%=userData.TMUsers.size() %> users

  34: <p>

  35:     <pre>

  36:         <%= userData.TMUsers.@select(user=>user.UserName).toString() %>

  37:     </pre>

  38: </p>


  40:  Current User: <%= new TM_Authentication(null).currentUser.UserName %>

2) Final version of the SSO.aspx page:

   1: <%@ Page Language="C#"%>


   3: <%@ Import Namespace ="O2.DotNetWrappers.ExtensionMethods" %>

   4: <%@ Import Namespace="TeamMentor.CoreLib" %>


   6: <link href="../Javascript/bootstrap/bootstrap.v.1.2.0.css" rel="stylesheet" type="text/css" />


   8: <%

   9:     var xmlDatabase     = TM_Xml_Database.Current;

  10:     var userData        = xmlDatabase.UserData;

  11:     var authentication  = new TM_Authentication(null);

  12:     var request         = HttpContextFactory.Request;

  13:     var response        = HttpContextFactory.Response;


  15:     var ssoKey          = "AAAAAAAAAa12345BBBBBBB";

  16:     var userName        = request["userName"];

  17:     var requestToken    = request["requestToken"];

  18:     var expectedToken   = (userName + ssoKey).md5Hash();


  20:     try

  21:     {

  22:         if (userName.valid() && requestToken.valid() && expectedToken == requestToken)

  23:         {

  24:             var tmUser = userName.tmUser();             // see if there is a user with the provided value


  26:             if (tmUser.isNull())                        // if not

  27:                 tmUser = userData.newUser(userName)     // create it (returns new userId)

  28:                                  .tmUser();             // and get the user object from the userId


  30:             var loginGuid = tmUser.login();             // login user in TM   

  31:             authentication.sessionID = loginGuid;       // triggers the update of user's cookies

  32:             response.Redirect("/teammentor");           // redirects user to logged in user

  33:         }

  34:         else

  35:             "[TM SSO] Failed to SSO with the values provided: {0} {1}".error(userName, requestToken);

  36:     }

  37:     catch (Exception ex)

  38:     {

  39:         ex.log();

  40:     }


  42: %> 


  44: TM SSO: Failed to login user

3) VisualStudio C# script that creates the popup window (with another C# Repl script connected to a browser)

   1: var visualStudio = new VisualStudio_2010();

   2: var webBrowser= visualStudio.open_Panel("SSO test")

   3:                             .add_WebBrowser_with_NavigationBar();

   4: var firstScript = @"

   5: var url = "

   7: return webBrowser;"
   8: webBrowser.insert_Below().add_Script_Me(webBrowser)

   9:                          .set_Code(firstScript);


  11: return visualStudio.dte();

4) popup window that tests the SSO:

   1: var userName         = "";

   2: var ssoKey           = "AAAAAAAAAa12345BBBBBBB";

   3: var expectedToken   = (userName + ssoKey).md5Hash();


   5: var urlTemplate = "http://local:3187/_Customizations/sso.aspx?userName={0}&requestToken={1}";

   6: var url = urlTemplate.format(userName, expectedToken);


   8: return url;

   9: //return webBrowser;