Friday 22 May 2015

Node.js OAuth handshake with DWA

In first post I show you basic OAuth handshake between your Node.JS application and DOORS Web Access Server.

This post will be longer than other ones, here I will show you how to create application step-by-step. In next posts I will be just extending functionality of this one by adding some services or new methods.

What you'll need to build your application

If you do not have Git on your machine I can recommend Eclipse eGit, which nicely integrates Git software control manager (Git SCM) into your development environment.


Since this application is written in Node.JS you might also want to install Nodeclipse to get default formatting and language highlighting.

Any time you can also use command line tools for which you will need Node.JS and some Git client.

That's all you need to start your local server development, but we know things can quickly escalate so you might want to have a look at IBM Cloud services.

To the Clouds and beyond

If you need more services, processes, memory, storage, you might want to host your application on the Cloud. IBM Bluemix is a Next-Generation App development platform. All you need is your IBM ID and you can start developing cloud applications.

If you already have an IBM ID follow this link to begin your free trial. Otherwise you can create one using the IBM Bluemix registration page.

Once you have your IBM Bluemix account trial started a good idea would be to complete the Getting Started with IBM Bluemix tutorial.

Install dependencies

Before we can write the application we should provide Node.js dependencies. This demo application makes use of the following modules: express, oauth, request, cookie-parser and libxmljs.


These need added those to your package.json
{
  "name": "HelloDWAOAuth",
  "version": "0.0.1",
  "description": "A sample nodejs app for OAuth with DWA",
  "dependencies": {
    "express": "3.4.7",
    "jade": "^1.1.4",
                "libxmljs": "0.13.0",
                "cookie-parser":"",
                "oauth": "",
                "request":"",
                "util": "",
                "url" : ""
  },
  "engines": {
    "node": "0.10.*"
  },
  "repository": {}
}

Open command line and navigate to your project directory, and type:

> npm install

This will install all of the required dependencies based on your package.json

Note:

Sometimes you can get errors like:
The builds tools for Visual Studio 2010 (Platform Toolset = 'v100') cannot be found.install new module.
 

It might be useful to try –msvs_version=[2005, 2008, 2012] switch if default build will fail.

Writing the server application

This demo uses Express. You will need to create express instance, define routes and provide functions that might be required to execute those routes. Here is the basic code for an express server.

var express = require('express');

// setup middleware
var app = express();
app.use(app.router);
app.use(express.errorHandler());
app.use(express.static(__dirname + '/public')); //setup static public directory
app.set('view engine', 'jade');
app.set('views', __dirname + '/views'); //optional since express defaults to CWD/views

// render index page - main route
app.get('/', function(req, res){
                    res.render('index');
});

var appInfo = JSON.parse(process.env.VCAP_APPLICATION || "{}");
var services = JSON.parse(process.env.VCAP_SERVICES || "{}");
var host = (process.env.VCAP_APP_HOST || 'localhost');

// The port on the DEA for communication with the application:
var port = (process.env.VCAP_APP_PORT || 3000);
// Start server
app.listen(port, host);
console.log('App started on port ' + port);

16 lines of code is all you need for a basic HTTP server written in Node.js. Put it into a new app.js file and that will be an entry point for our application.

Certificates

If you will be working in a development environment you might need to use self-signed certificates.

Those by default will be rejected and no connection will be made. If you want to prevent self-signed certificate error add following line:
 process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

Adding OAuth to your application

Let's add OAuth as a separate Node.js module so it won't clatter the main app.js code. Modular approach will also help you to separate OAuth module and reuse it in other applications.

So now we will have app.js and Oauth.js files.

App.js will contain main routes and application logic for our sample. OAuth 1.0 requires user to provide application key and secret used to authenticate the application with a remote server. Thus our application will have two pages, where the user will be asked to provide location of rootservices, application key and OAuth secret.

Once those are provided the user will be redirected to page providing basic access to protected resources.

In this example we are using oauth Node.js module which comes with very useful examples how to do a basic Oauth handshake.

Configuring OAuth

Let's start with gathering information required to start OAuth. Create body.jade to add a form with POST action to /set_key_and_secret:

form(name="loginform", action="/set_key_and_secret", method="post")
          | Rootservices location:
          input(type="text", name="rootservices")
          br
          | Consumer Key:
          input(type="text", name="key")
          br
          | OAuth Secret:
          input(type="password", name="secret")
          br
          input(type="submit", value="Submit")


Now we have to add POST route to our application (app.js):
app.post('/set_key_and_secret', function(req, res) {
    //if (typeof req.body.rootservices !== 'undefined') console.log(req.body.rootservices);
    if (typeof req.body !== 'undefined') {
        if (typeof req.body.rootservices !== 'undefined' && req.body.rootservices != '' &&
            typeof req.body.key !== 'undefined' && req.body.key != '' &&
            typeof req.body.secret !== 'undefined' && req.body.sercret != '')
        {
            dwaOauth.configure(req.body.rootservices, req.body.key, req.body.secret, process.env.VCAP_APP_PORT || 3000);
            console.log("got settings");
            res.redirect("/dwa_login");
        }
        else {
            res.redirect('/');
        }
    }
});
Note: request.body is not available by default in Node.js express, we need to inform our application we would like to have access to this object. In order to do this we need to add:

app.use(express.bodyParser());

This must be added before any application routes will be defined. Best place is to add it before:

app.use(app.router);

dwaOauth.js module

You could wonder what is dwaOauth.configure called in preview example:

var location=null, oauth_key=null, oauth_secret=null, port=null;
exports.configure = function(rootservices_, key_, secret_, port_) {
    location = rootservices_;
    oauth_key = key_;
    oauth_secret = secret_;
    port = port_;
}

It is a very simple function used to provide some rootservices location, application key and secret to your dwaOauth module.

dwaOauth.js module will handle OAuth 1.0 dance and will give application access to protected resources.
All we need it to get authorized OAuth access token. The following steps are required to successfully get the authorized OAuth token:
  1. Login to DWA to get JSESSIONID Cookie
  2. Request a OAuth token
  3. Authorize the OAuth token
  4. Get the access token

In '/dwa_login' route we create a OAuth object which will be used to (2) request the OAuth token and once it was correctly retrieved it will redirect user to (3) authorize it. OAuth module will handle those requests and it will also redirect user to perform (1) login to DWA in order to get JSESSIONID Cookie.

In app.js add:

//Request an OAuth Request Token, and redirects the user to authorize it
app.get('/dwa_login', dwaOauth.getOAuthURLs, dwaOauth.requestToken);


In routes/dwaOauth.js add:

exports.requestToken = function(req, res) {
    var oa = new OAuth(oauthRequestTokenUrl,
        oauthAccessTokenUrl,
        oauth_key,
        oauth_secret,
        "1.0",
        req.protocol+"://"+req.host+":"+port+ "/dwa_cb", // this should work on Bluemix and localhost
        "HMAC-SHA1");

    console.log("1. Request a OAuth Token - this will redirect to DWA login page");

    oa.getOAuthRequestToken(function(error, oauth_token, oauth_token_secret, results){
        if(error) {
            console.log('error : ' + util.inspect(error));
            res.end();
        }
        else {
            //store the tokens in the session
            req.session.oa = oa;
            req.session.oauth_token = oauth_token;
            req.session.oauth_token_secret = oauth_token_secret;

            console.log("2. Authorize the OAuth Token ");

            res.redirect(oauthUserAuthorizationUrl +"?oauth_token="+oauth_token);
        }
    });
}

Rootservices and OAuth

As you probably notice '/dwa_login' requires getOAuthURLs before it will be executed. getOAuthURLs method parses DWA rootservices so oauthRequestTokenUrl, oauthAccessTokenUrl and oauthUserAuthorizationUrl are discovered automatically. We do not need to store those in any configuration file.

// get URL from root services
exports.getOAuthURLs = function(req, res, next){
    console.log('Attemp to get OAuth URLs ' + location);
    if (location == null || oauth_key == null || oauth_secret == null) {
        return next('not configured');
    }

    request(location, function(error, response, body){
        if (!error && response.statusCode == 200) {
            var xmlDoc = libxmljs.parseXml(body);
            oauthRequestTokenUrl = xmlDoc.get('jfs:oauthRequestTokenUrl', ns.jfs).attr('resource').value();
            oauthAccessTokenUrl = xmlDoc.get('jfs:oauthAccessTokenUrl', ns.jfs).attr('resource').value();
            oauthUserAuthorizationUrl = xmlDoc.get('jfs:oauthUserAuthorizationUrl', ns.jfs).attr('resource').value();
            next();
        }
        else {
            console.log('error' + util.inspect(error));
            return next(error);
        }
    });
}
ns is defined as:
exports.ns = ns = {
  dcterms:{ 'dcterms': "http://purl.org/dc/terms/" },
  jfs: { 'jfs': 'http://jazz.net/xmlns/prod/jazz/jfs/1.0/' },
  oslc_rm:{ 'oslc_rm': 'http://open-services.net/ns/rm#' },
  oslc:   { 'oslc': 'http://open-services.net/ns/core#' },
  rdf: { 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' },
  doors:  { 'doors': 'http://jazz.net/doors/xmlns/prod/jazz/doors/2.0/' }
So namespaces are defined and easier to use.
Above code uses request to get rootservices and libxmljs to parse it. As mentioned earlier rootservices are the only resource not protected by OAuth so we do not need to worry about setting JSESSIONID cookie or OAuth while accessing it.

Add callback route

The last step in OAuth dance is to get the access token. We will do it in callback we provided earlier while constructing OAuth object. This callback is another route defined in app.js as '/dwa_cb':

// Callback for the authorisation page
app.get('/dwa_cb', dwaOauth.callback);

and in dwaOauth.js:

exports.callback = function(req, res) {
    var oa = new OAuth(req.session.oa._requestUrl,
            req.session.oa._accessUrl,
            req.session.oa._consumerKey,
            req.session.oa._consumerSecret,
            req.session.oa._version,
            req.session.oa._authorize_callback,
            req.session.oa._signatureMethod);
    console.log("3. Get the access token");

    oa.getOAuthAccessToken(
            req.session.oauth_token,
            req.session.oauth_token_secret,
            req.param('oauth_verifier'),
            function(error, oa_access_token, oa_access_token_secret, results) {
            if(error) {
                console.log('error' + error);
            }
            else {
                req.session.oauth_access_token = oa_access_token;
                req.session.oauth_access_token_secret = oa_access_token_secret;
                //redirect to home page
                res.redirect("/");
            }
    });
}

Access protected resource (finally!)

Once we have the access token we can access protected resources. Last line in '/dwa_cb' route will automatically redirect user to a root page which as we saw earlier routes to /resource.

Let's add a resources route to app.js:

app.get('/resource', checkLogin, function(req, res) {
    res.render('resource', {"resource": ""});
});


As you can see it renders 'resource' page with resource parameter. We will use HTTP Get request to get that resource. Add following to dwaOauth.js

exports.getProtectedResource = function(req, res, resourceURL, next, onError) {
    console.log('requesting protected resource: '+resourceURL);
    var oa = new OAuth(req.session.oa._requestUrl,
          req.session.oa._accessUrl,
          req.session.oa._consumerKey,
          req.session.oa._consumerSecret,
          req.session.oa._version,
          req.session.oa._authorize_callback,
          req.session.oa._signatureMethod);

    oa._headers['OSLC-Core-Version'] = '2.0';
    oa._headers['accept'] = "application/rdf+xml";
 
    // Try to get protected data
    oa.getProtectedResource(
        resourceURL,
        "GET",
        req.session.oauth_access_token,
        req.session.oauth_access_token_secret,
        function (error, data, response) {
            if (!error && next) {
                next(req, res, data);
            }
            else if (onError) {
                onError(error);
            }
        }
    );
}
This is a general HTTP request and should work with any OAuth server (DWA, DNG, QM, etc).

Once you have this your dwaOauth.js you can use it in your app.js with following route:

function getResource(req_, res_, next) {
    if (req_.body.res_url != "") {
        dwaOauth.getProtectedResource(req_, res_, req_.body.res_url, function(req, res, data) {
            var xmlDoc = libxmljs.parseXml(data);
            var mod = xmlDoc.get('//dcterms:title', dwaOauth.ns.dcterms).text();
            next(req_, res_, "", mod);
        });    
    }
    else next(req_, res_, "", "");
}

app.post('/resource', checkLogin, function(req, res) {
    getResource(req, res, render);

});


If you provide a valid resource link, below code will attempt to read dcterms:tile of the remote resource.
Update resource.jade to include a form for Resource URL input.

form(action="/resource", method="post")
      h2 Sample 1: Get resource title
      p Please provide a URL for one of your resources (say link to module)
      p Resource URL:
       input(type="text", name="res_url")
       input(type="submit", value="Submit")
       br
       if resource == ""
        p
       else
        p Resource title: '#{resource}'
        br

You just learned know how to read data from OAuth protected server! Easy ;)

You can find listing for dwaOauth.js here.

Update:

In my original post I forgot to mention checkLogin function. Thanks to Dani for pointing that!

// main redirection method
// make sure all your routes to protected resources will go via this method 
function checkLogin(req, res, next) {
    // if session doesn't have a oauth access token redirect to login page.
    if(!req.session.oauth_access_token) {
        res.redirect("/");
        return;
    }
    next();
}

but that's not all what's needed. It's only a redirection to the default route which I need to include here as well.

//root page
app.get('/', function(req, res){    

    // check if session has oAuth token
    if(!req.session.oauth_access_token) {
            res.render("index");
        }
        else {
            res.redirect("/resource");
        }
});
index is a simple form where user provides DWA location, oAuth key and secret:
//- index.jade
body
  table
    tr
      td(style= "width:30%;" )
        img(src="/images/newapp-icon.png")
      td
        h1 Hi there!
 
        p First you will need to setup your OAuth server details:
        
        form(name="loginform", action="/set_key_and_secret", method="post")
          | Rootservices location:
          input(type="text", name="rootservices")
          br
          | Consumer Key:
          input(type="text", name="key")
          br
          | OAuth Secret:
          input(type="password", name="secret")
          br
          input(type="submit", value="Submit")

set_key_and_secret is defined earlier.