Wednesday 11 September 2013

Consuming password protected TeamMentor Articles using REST GET APIs (and creating mini-tool to view article's data)

As described by the TeamMentor’s CX integration requires TM instance that is serving the content to be open to anonymous access issue, there are times when programmatic access is needed to password protected TeamMentor articles.

Let’s take for example the Add Unique Tokens to HTTP Requests Using ESAPI article, which has the 7d647e95-e47f-42e3-bb84-fd0dd727245c GUID, and can be opened directly at https://teammentor.net/article/7d647e95-e47f-42e3-bb84-fd0dd727245c (free account is needed to see that link)

image

Here is what happens if we try to open that link without first being logged in (btw: next version of TM show a more friendly message in these cases):

image

Since there is quite a bit of Javascript when loading an /article/GUID link lets see what happens when we open an /content/GUID link (which just returns the article’s content), in this case the https://teammentor.net/content/7d647e95-e47f-42e3-bb84-fd0dd727245c page

image

Using Fiddler, we can see that what is holding the 'logged in status' is the Session Cookie value.

For example, this request (with a valid Session cookie value), will return the article content:

image

... and if we make the same request without the Session cookie, we will get a 302 redirect into the TeamMentor Login page:

image

This means that in order to get content from a live TeamMentor site we need to do two requests:

  1. login with a valid username and password and get a Session GUID
  2. make a request to get a specific article (using the session GUID in a cookie)
There are a number of ways to login (for example using the WebServices asmx API), but probably the easiest is to use the /rest/Login/{username}/{password} REST call (which btw should only be used over https connections)

Using the O2’s REPL script environment, here is a simple script that gets a login token:

Web.Https.ignoreServerSslErrors();                                    // in case we have fiddler on
var crendential     = "TestAccounts.xml".credential("TeamMentor");    // load username/password from disk
var loginRequest    = "https://teammentor.net/rest/login/{0}/{1}"     // create GET URL
                            .format(crendential.UserName, crendential.Password);
return loginRequest.GET();                                            // send request

… with the return value being the serialization of a GUID:

image

This xml string can be easily parsed like this:

var crendential     = "TestAccounts.xml".credential("TeamMentor");    // load username/password from disk
var loginRequest    = "https://teammentor.net/rest/login/{0}/{1}"     // create GET URL
                            .format(crendential.UserName, crendential.Password);
                            
return loginRequest.GET()                  // send request
                   .xRoot()                // parse XML and return root
                   .innerXml();            // get innerXml of root element
                 //.guid();                // transform string into a GUID object

 … so that we have a GUID string:

image

Tip: while developing the script it is useful to create Lamdba methods to help out and to store variables in the o2Cache object

Func<string> getLoginGuid = ()=>  loginRequest.GET()                // send request
                                              .xRoot().innerXml();  // get guid value value

var loginId =  "loginId".o2Cache<string>(getLoginGuid);             // get cache value (or call getLoginGuid)
return loginId;

Once we have a valid SessionId we can get the article’s content like this:

var loginId      = "login_Id".o2Cache<string>(getLoginGuid);        // get cache value (or call getLoginGuid)

var tmArticleUrl = "https://teammentor.net/content/" +              // REST GET call for content
                      "7d647e95-e47f-42e3-bb84-fd0dd727245c";       // GUID of article to get
var response     = tmArticleUrl.GET("Session=" + loginId);          // make GET request with provided cookie value
    
return response;    

… which will return the article’s HTML content:

image

Note that if a valid session ID is not provided the returned HTML will be the TM’s login page:

image

While on the C# REPL UI an easy way to view the HTML content is to show it on a Web Browser control:

var topPanel     = panel.clear().add_Panel();      // clear the REPL panel host control
var webBrowser   = topPanel.add_WebBrowser()       // add a Web Browser control
                           .silent(true)           // make it silent (don't show Javascript errors)
                           .set_Html(response);    // load article content into it

… which looks like this:

image

Tip: if you want to quickly apply some CSS, then wrap the article html code in an HTML body:

var response     = tmArticleUrl.GET("Session=" + loginId);          // make GET request with provided cookie value

var htmlTemplate = @"<html>
                        <head>
                            <link href='http://getbootstrap.com/dist/css/bootstrap.css' rel='stylesheet'>
                        </head>
                        <body>
                            {0}
                        </body>
                    </html>";
                    
var html         = htmlTemplate.format(response);   // create html from template and response
var topPanel     = panel.clear().add_Panel();       // clear the REPL panel host control
var webBrowser   = topPanel.add_WebBrowser()        // add a Web Browser control
                           .silent(true)            // make it silent (don't show Javascript errors)
                           .set_Html(html);         // load article content into it

 … which will make the HTML shown look better: 

image


Creating a stand alone tool to view Article’s content, raw data as xml and raw data as jsonp

Now that we have the capability to get content, let’s create a mini tool to view it.

Starting by wrapping the content fetch and css wrapping into its own methods:

Func<string,string,string,string> getArticle = 
    (contentType, articleId, sessionId)
        =>{
             var tmArticleUrl = "https://teammentor.net/{0}/{1}";  // REST GET call for content                                       
             return tmArticleUrl.format(contentType,articleId)     // set contentType and session
                               .GET("Session=" + sessionId);       // make GET request with provided cookie value
          };

Func<string,string> wrapCss = 
    (bodyContent) =>{
                        return @"<html>
                                    <head>
                                        <link href='http://getbootstrap.com/dist/css/bootstrap.css' rel='stylesheet'>
                                    </head>
                                    <body>
                                        {0}
                                    </body>
                                </html>".format(bodyContent);
                    };

… and then add a couple more UI controls:

var topPanel    = panel.clear().add_Panel();                                       // clear the REPL panel host control
    
var htmlViewer  = topPanel.title       ("Article as Html" ).add_WebBrowser();      // use webBrowser to see article's content                         
var rawViewer   = topPanel.insert_Right("Article as Raw"  ).add_WebBrowser();      // use webBrowser to see article's object as jsonp 
var jsonpViewer = topPanel.insert_Below("Article as Jsonp").add_WebBrowser();      // use webBrowser to see article's object as raw xml 
    
var userId      = "user_Id".o2Cache<string>(getLoginGuid);                         // get session id from getLoginGuid() or cache
var article     = "7d647e95-e47f-42e3-bb84-fd0dd727245c";
    
var html        = wrapCss(getArticle("content",article, userId));
var raw         =         getArticle("raw"    ,article, userId);                
var jsonp       =         getArticle("jsonp"  ,article, userId);                
    
htmlViewer     .set_Html(html);                         
rawViewer      .set_Html(raw);
jsonpViewer    .set_Html(jsonp);
    
return html;

These script changes will give us an UI with 3 panels:
  • Top left:       Article Html (with a little bit of css for mating)
  • Bottom left: Article XML article as a Jsonp object
  • Right:           Article XML
… which looks like this:

image

Next lets refactor the code so that we have one Lamdba method that triggers the entire workflow (login, fetch and show data)
Func<string,string,string> getLoginId = 
    (username, password)
          =>{
                  var loginRequest = "https://teammentor.net/rest/login/{0}/{1}"                   // create GET URL
                                            .format(username, password);    
                  return username.add(password)                                                    // use username+password as cache key
                                  .o2Cache<string>(()=>loginRequest.GET().xRoot().innerXml());     // login request made here
            };
                    
Action<string,string,string> showArticle = 
    (articleId, username, password)
          =>{
                var userId    = getLoginId(username, password);
                        
                var html      = wrapCss(getArticle("content",articleId, userId));
                var raw       =         getArticle("raw"    ,articleId, userId);             
                var jsonp     =         getArticle("jsonp"  ,articleId, userId);        
                
                htmlViewer    .set_Html(html);                         
                rawViewer     .set_Html(raw);
                jsonpViewer   .set_Html(jsonp);                                    
            };
    
var crendential  = "TestAccounts.xml".credential("TeamMentor");    // load username/password from disk
var article      = "7d647e95-e47f-42e3-bb84-fd0dd727245c";         // article to show
    
showArticle(article, crendential.UserName, crendential.Password);  // trigger show article workflow

 … add an ‘Actions’ panel to the top with some textboxes (article, username, password) and link

var topPanel         = panel.clear().add_Panel();    // clear the REPL panel host control
Action loadData      = null;                         // placeholder for this lamda method

var configPanel      = topPanel.insert_ActionPanel();
var username_TextBox = configPanel     .add_Label   ("Username:"      ).top(2).append_TextBox("");
var password_TextBox = username_TextBox.append_Label("Password:"      ).top(2).append_TextBox("").isPasswordField();
var article_TextBox  = password_TextBox.append_Label("Article to Show").top(2).append_TextBox("").width(400);                    
var loadData_Link    = article_TextBox .append_Link ("Load Data"   , () => loadData());

… and use these controls to set the default values and set the data

loadData = ()=>    showArticle(article_TextBox.get_Text(), username_TextBox.get_Text(), password_TextBox.get_Text());            
     
var crendential     = "TestAccounts.xml".credential("TeamMentor");      // load username/password from disk
    
username_TextBox.set_Text(crendential.UserName);                        //set default values on TextBoxes
password_TextBox.set_Text(crendential.Password);
article_TextBox.set_Text("7d647e95-e47f-42e3-bb84-fd0dd727245c");
    
loadData_Link.click();                                                  // trigger show article workflow

At the moment this mini-tool looks like this:

image

 Finally, now that we have the desired functionality (i.e. view an article’s data for a particular article Id under an specific user account), we can run the UI under a WinForms Form (vs the REPL’s panel) , by making this change:

//var topPanel       = panel.clear().add_Panel();             // use when in developent
var topPanel         = "PoC - View TeamMentor Article (using GUID)".popupWindow(1200,500);    // use when done

With the tool looking like this:

image

 To help with deployment and use, we can also save this script:

image

 … and package it as a stand-alone exe:

image

 Which can now be used independently:

image 

 I added a check to see if the credentials value is set (which will not be when running the stand-alone script)

image

… entering a valid username+password and article ID, will trigger the login and data loading:

image

… which will look like this:

image 

 … we can also now open any Article (using its ID) that exists on this server, for example the 6c470029-5c62-4394-99a9-8990bc48b0a8 one (whose title is: Change Session IDs During Authentication )

image


Download link:

If you want to try this mini-tool, you can get the stand-alone exe from here: PoC - View TeamMentor Article (using GUID v1.0.exe 


Source code

Here is the final source code of this script: https://gist.github.com/DinisCruz/6523494