Posts Tagged ‘OAuth’

How to add Authentication to a MarkLogic App?

February 4, 2013

 

Learn how to add authentication to a MarkLogic Roxy App.

This blog post, I will show the code needed to add a simple authentication service.

In the previous post, I created the two column layout where the top of the home page had a login form. At the time the login form was not wired up.

For this post, I will wire up the login form. To do this, I’ll show how to build a simple authentication service that searches a user database, verifies the password, and generates an authentication token that will expire after 5 minutes.

This demo application will also show how to provide a simple RESTful API for search. This Search API will utilize the authentication token to restrict access to the search service.

The app will not show any role based restricted views. A more fully featured role based access control will be shown in a future post.

This demo will use the boing-roxy demo that is posted here.  http://ps2.demo.marklogic.com:8090/

A zip file containing the source code for this demo application is posted here.  => source code

Overall Approach

The solution will use the following items to authenticate a user and create an application token that gives the user access to the RESTful API for a 5 minute period.

  1. Registration Form – used to create the user profile documents. This form should only be visible to an admin user but is currently visible to all for demo purposes.
  2. User Directory – Each user will have a dedicated user directory in the MarkLogic database.
  3. User Profile Document – User profile document (/users/janedoe/profile.xml) will reside in the user’s directory. It will contain the username/password. The username must be unique. It can also be used to store the user’s role and organization information. This demo will not utilize a party management solution but it can be extended to do so.
  4. Session Document – A session document will be created when a user successfully logs in. The Session Document will be stored in the User Directory.
  5. Authentication Token – the token will be stored in the session document. Each RESTful API request must include the token in its header.
  6. ROXY Router – Will be the checkpoint or the single point of entry for each request. This is where the user token is verified for each RESTful API request. A key function called Find-Session-by-Token() verifies the session and dispatches the request if valid.
  7. Login – If the username and password is valid, the token is created. If a token already exists and has not yet expired then the same token will be used.
  8. Session Expiration – Session Expiration will be 5 minutes from initial login. The 5 minute duration is for demo purposes. Typical session expiration duration is 24 hours. Session expiration time will be UTC based. UTC is Coordinated Universal Time.
  9. Logout – Terminates the session by deleting the session document that contains the token.

 

Related Notes:

  1. Passwords are never part of the RESTful API transport except the Login API request.
  2. “Remember Me” cookie – This solution can support a “Remember Me” cookie where the token is stored in the cookie and not the password. Remember Me cookies typically expire after 90 days which is longer than the token expiration period.
  3. Verify API – A good approach for refreshing the token stored in a “90 day login cookie” is described here. A Verify API is typically used to verify the Username and Token. If they match then a new token is generated whenever the existing token has expired. The 90-day cookie web app will need to call the Verify API to refresh the token stored in the cookie.
  4. Passwords are currently stored in the User Profile doc but they are MD5 hashed.
  5. Current solution shows how to use the MarkLogic Search API with the user profile document, session document and token to provide an adequate security solution.
  6. OAuth2 – Open Authentication version 2 (OAuth2) is a widely used protocol that provides a federated user profile solution. The key benefit for this example is that user passwords do not need to be stored in MarkLogic. However, this is a topic for a future post. The OAuth2 developer details are here: https://developers.google.com/accounts/docs/OAuth2

 

1. Registration Form

The registration form creates the user profile data.

You can access it here. http://ps2.demo.marklogic.com:8090/user

Here’s a snapshot view.

image

 

2. User Directory

The registration form above creates the user profile data that is stored in a User Profile Document in the respective user directory. The Session Document is also stored in the User Directory.

Example User Directory URIs

/users/grusso/profile.xml
/users/grusso/session/2ccda41cd|2/4/2013 8:28:41 PM.xml

 

3. User Profile Document

Here’s the format of the simple user profile document. Please note that the password is stored as an MD5 hash.

<user-profile>
  <firstname>Gary</firstname>
  <lastname>Russo</lastname>
  <username>grusso</username>
  <password>5f4dcc3b5aa765d61d8327deb882cf99</password>
  <created>2013-02-02T13:45:42.856718-08:00</created>
  <modified>2013-02-03T17:51:59.585527-08:00</modified>
</user-profile>

Roxy code that generates the user profile document is:

  1. user controller – /apps/controllers/user.xqy
  2. user model – /apps/models/user-model.xqy

 

4. Session Document:

Here’s the format of the session document.

<session user-sid="2cc3bdfc38bf03c63a4de6bda41cd91b|2/4/2013 8:28:41 PM">
  <username>grusso</username>
  <created>2013-02-04T20:25:41.891354-08:00</created>
  <expiration>2013-02-04T20:28:41.891354-08:00</expiration>
</session>

Please note that the @user-sid attribute in the above XML is the authentication token.

Session Document URI:

/users/grusso/session/2cc3bdfc38bf03c63a4de6bda41cd91b|2/4/2013 8:28:41 PM.xml

The above document URI has the expiration date/time appended to it. Some JavaScript client code will use the appended expiration date/time to trigger a token refresh.

Roxy code that creates and deletes the session document is:

  1. web login controller – /apps/controllers/appbuilder.xqy
  2. rest login controller – /apps/controllers/login.xqy
  3. rest logout controller – /apps/controllers/logout.xqy 
  4. authentication model – /apps/models/authentication.xqy

 

5. Authentication Token:

In the above example, the authentication token is the following string:

2cc3bdfc38bf03c63a4de6bda41cd91b|2/4/2013 8:28:41 PM

This string needs to be added to the request header of each RESTful API request. If not the response will be a “401 unauthorized” error. The token must be prefixed with “X-Auth-Token” as follows.

X-Auth-Token: 2cc3bdfc38bf03c63a4de6bda41cd91b|2/4/2013 8:28:41 PM

The source code that extracts the X-Auth-Token value is in the router code. See line 87 of  /src/app/lib/router.xqy.

let $token := xdmp:get-request-header("X-Auth-Token")

If using Firefox Poster tool, the header can be added as shown.

 

image

 

6. ROXY Router:

As discussed above, the router is the checkpoint for all http requests. It is the ideal place to apply a security policy logic such as:

  1. Token check
  2. Requests per minute
  3. Maximum Requests per day

This post only handles the token check but this code can be extended to support all security policy logic.

The following xquery code handle the token check. Please note that certain request (e.g., login, ping) bypass the token check.

 

let $valid-request :=
  if(fn:not($config:SESSION-AUTHENTICATE)) then fn:true()
  else if(xs:string($controller) = ("ping")) then fn:true()
  else if(xs:string($controller) = ("login")) then fn:true()
  else if(xs:string($controller) = ("logout")) then fn:true()
  else if(xs:string($controller) = ("verify")) then fn:true()
  else
  (
    let $token := xdmp:get-request-header("X-Auth-Token")
    return
      if($token) then
      (
        let $valid-session := auth:findSessionByToken($token)
        return
          if($valid-session) then
          (
            fn:true(),
            auth:cacheSession($valid-session)
          )
          else
            fn:false()
      )
      else fn:false()
  )

7. Login Code:

The login code does the following:

  1. Find user profile document – Searches the the user profile documents using the username.
  2. Check password – If a document with the username is found then the password is checked.
  3. Find session document by username – If the password matches then code looks for a session document with its respective expiration date.
  4. Session Document – If the session expiration has not expired then use current session document. If session document has expired then delete it and then create a new session document containing new Authentication Token.
  5. Return Authentication Token

This code resides in the following files:

  1. rest login controller – /apps/controllers/login.xqy
  2. authentication model – /apps/models/authentication.xqy

The URI to request a Login is:

http://ps2.demo.marklogic.com:8090/login

The username and password is bundled into the request using the Authorization Header.

So the request header will need this:

Authorization: Basic Z3J1c3NvOnBhc3N3b3Jk

The encrypted string after the word Basic contains the base64 encoded username and password.

Here the code that extracts the username/password is in the login controller (/src/app/controllers/login.xqy).

declare function c:main() as item()*
{
  let $userPwd  :=
    xdmp:base64-decode(
      fn:string(
        fn:tokenize(
          xdmp:get-request-header("Authorization"), "Basic ")[2]
      )
    )
  let $username :=
    fn:string(
      (xdmp:get-request-header("username"),
           fn:tokenize($userPwd, ":")[1])[1]
    )
    
  let $password :=
    fn:string(
      (xdmp:get-request-header("password"),
           fn:tokenize($userPwd, ":")[2])[1]
    )

  let $result   := auth:login($username, $password)

  return
  (
    ch:add-value("res-code", xs:int($result/json:responseCode) ),
    ch:add-value("res-message", xs:string($result/json:message) ),
    ch:add-value("result",  $result),
    ch:add-value(
          "res-header", 
          element header {
            element Date {fn:current-dateTime()},
            element Content-Type
            {
              req:get("req-header")/content-type/fn:string()
            }
          }
        )
  )
};

The code that searches for a session document by username and its expiration date/time uses the following function. Please note the element range index query.

declare function auth:findSessionByUser($username)
{
  let $query :=
    cts:and-query((
      cts:directory-query(auth:sessionDirectory($username),"infinity"),
      cts:element-range-query(
            xs:QName("expiration"),">",
            auth:getCurrentDateTimeUTC())
    ))

  let $uri := cts:uris("",("document","limit=1"), $query )
  
  return
    fn:doc($uri)
};
 

8. Session Expiration:

The code to check the session expiration is invoked by the router code.

See line 89 in /src/app/lib/router.xqy

auth:findSessionByToken($token)

Here’s the code. Please note the element range query.

declare function auth:findSessionByToken($token as xs:string)
{
  let $query :=
    cts:and-query((
      cts:element-attribute-value-query(
        xs:QName("session"),
        xs:QName("user-sid"),
        $token
      ),
      cts:element-range-query(
        xs:QName("expiration"),">",
            auth:getCurrentDateTimeUTC()
      )
    ))

  let $uri := cts:uris("",("document","limit=1"), $query )
  let $doc := fn:doc($uri)
  
  let $current := fn:current-dateTime()
  
  return
    if ($doc) then
    (
      let $expiration := xs:dateTime($doc//expiration)
      let $diff := ($expiration - $current)
      
      return
      (
        if($diff < ($auth:SESSION-TIMEOUT div 2) ) then
          xdmp:node-replace(
              $doc//expiration/text(),
              text{fn:current-dateTime()}
            )
        else (),
        $doc/session
      )
    )
    else ()
};
 

9. Logout Code:

The logout code terminates the session by deleting the session document. Here’s the code.

declare function auth:logout($username as xs:string)
{
    let $session := auth:findSessionByUser($username)
    let $user := auth:userFind($username)

    let $token :=
      if($session) then
        $session/session/@user-sid/fn:string()
      else ()
      
    let $__ := auth:clearSession($username)
        return
           <json:object type="object">
              <json:responseCode>200</json:responseCode>
              <json:message>Logout Successful - Token Deleted</json:message>
              <json:authToken>{$token}</json:authToken>
              <json:username>{$user/username/text()}</json:username>
              <json:fullName>
              {
                fn:string-join
                ((($user/firstName,$user/firstname)[1],
                  ($user/lastName,$user/lastname)[1]), " ")
              }
              </json:fullName>
           </json:object>
};
 

Conclusion

Hopefully, the authentication code described in this demo application has been informative. It shows an approach that I have recently used in a ROXY Application.

I will be building on this solution in future posts. The most pressing next step is to add support for OAuth v2 and role based restricted views. So stay tuned.

As always, please let me know if any further clarifications or details are needed in the comments section.