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 https://xyz.teammentor.net/_Customizations/sso.aspx?requestToken=[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

 image

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

image

and use Tbot’s to reload the TM cache:

image

That test file will be copied to the web root:

image

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

image

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

image

and if the new file is an *.aspx:

image

we can run c# code in there:

image

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:

image

which looks like this:

image

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)

image

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

image

Ok, back to the script,

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

image

which looks like this when executed:

image

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

image

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

image

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)

image

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)

image

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:

image

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 :

image

will login into the current user test:

image

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

image

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

image

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

image

so it now looks like this:

image

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

image

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)

image

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

image

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

image

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 ):

image

Login-in with a new user (with url http://local:3187/_Customizations/sso.aspx?userName=A_New@user.com&requestToken=0ea6e75eca7682d0acff45d367f6aaf6)

image

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

image

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

image

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

image

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

image

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

image

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#"%>

   2:  

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

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

   5:  

   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;    

  11:     

  12:     var ssoKey          = "AAAAAAAAAa12345BBBBBBB";

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

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

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

  16:     

  17:     

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

  19:  

  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

  23:  

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

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

  26:  

  27: %> 

  28:  

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

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

  31:  

  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>

  39:  

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

2) Final version of the SSO.aspx page:


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

   2:  

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

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

   5:  

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

   7:  

   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;

  14:     

  15:     var ssoKey          = "AAAAAAAAAa12345BBBBBBB";

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

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

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

  19:  

  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

  25:  

  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

  29:  

  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:     }

  41:  

  42: %> 

  43:  

  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 = "
"http://local:3187/_Customizations/sso.aspx"";
   6: webBrowser.open(url);

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

   9:                          .set_Code(firstScript);

  10:  

  11: return visualStudio.dte();

4) popup window that tests the SSO:


   1: var userName         = "A_New_123@user.com";

   2: var ssoKey           = "AAAAAAAAAa12345BBBBBBB";

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

   4:  

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

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

   7: webBrowser.open(url);

   8: return url;

   9: //return webBrowser;