Authenticating a NodeJS application using Thinktecture Identity Server v2

If you happen to be using Thinktecture Identity Server for authentication/single sign-on and were wondering how to authenticate a node.js application against it, here is how I did it.

Thinktecture Identity Server

For those of you who have not heard about Thinktecture Identity Server (IdSrv), here is an excerpt from their Github page:

Thinktecture IdentityServer is a light-weight security token service built with .NET 4.5, MVC 4, Web API and WCF.

IdSrv is an excellent solution to implement single sign-on. If you are using version 3 of IdSrv you will have no issues setting up your node application as IdSrv 3 supports OpenID Connect and there are active and well maintained Passport strategies for OpenID Connect. This is not the case with IdSrv 2. IdSrv 2 uses the WSFed protocol and I could find only one passport strategy - passport-wsfed-saml2 - maintained by Auth0. Unfortunately, it does not work straight out of the box, however with minimal tweaking it gets the job done. The WSFed protocol is an older protocol developed by Microsoft and it does not get any love these days and may I say - "rightly so".

IdSrv setup

Set up a new Relying Party in Identity Server and make a note of the Realm and the IdSrv thumbprint. Make sure you set the Return URL in the IdSrv (in our case, pointing to /login/callback).

node

Application setup

Let's first get a simple ExpressJS application working (I am assuming you know your way around nodeJS/expressJS/npm). Create a new folder and

npm init  

Install the necessary modules

 npm install express --save

Create your server (server.js)

touch server.js  

Create a simple express app

var express = require('express'),  
    app = express();
app.get('/', function(req, res) {  
    res.send('hello world');
});
app.get('/secure', function(req, res) {  
    res.send('you have access to secured resources');
});
app.listen(3000, function() {  
    console.log('Server started at port 3000');
});

Suppose we wish to secure our secure endpoint and want users to be authenticated by our IdSrv using WSFed, we first need to install Passport

Passport is authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based web application. A comprehensive set of strategies support authentication using a username and password, Facebook, Twitter, and more.

npm install passport --save  

Next, we need to install the passport-wsfed-saml2 strategy:

npm install passport-wsfed-saml2 --save  

To get Passport working we need the following express middleware:

npm install body-parser cookie-parser express-session --save  

Require the newly downloaded modules

var ...  
    bodyParser = require('body-parser'),
    cookieParser = require('cookie-parser'),
    session = require('express-session'),
    passport = require('passport'),
    wsfedsaml2 = require('passport-wsfed-saml2').Strategy;

Set up the passport strategy

//set up passport
passport.use('wsfed-saml2', new wsfedsaml2(  
  {
    realm: 'urn:node:app',
    identityProviderUrl: 'https://IdSrv/issue/wsfed',
    thumbprint: 'xxxx'
  },
  function(profile, done) {
        return done(null, new User(profile));
    }));
);

Plug in the realm and thumbprint that you noted while setting up the relying party in IdSrv. Note that the callback function calls the done function and passes a new instance of a User object. This is a custom object that will take what Passport parsed as json and convert it to a more meaningful object. The profile object looks something like the following depending on which claims have been set up on the IdSrv:

{ 
   sessionIndex: undefined,
  'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name':     'user@abc.net',           'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress': 'user@abc.net',
  'http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid': '123',
  'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier': 'user@abc.net',
  'http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod': 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password',
  issuer: 'http://youridsrv/',
  email: 'user@abc.net' 
}

Our User object will clean it up into more meaningful properties:

var User = function(user) {  
    this.id = user['http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid'];
    this.email = user.email;
};

Then add the appropriate middlewares:

//Add middlewares
app.use(cookieParser());  
//This is critical. IdSrv posts to the callback in a urlencoded format
app.use(bodyParser.urlencoded({  
    extended: true
}));
//set up session to store the authenticated user
app.use(session({  
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: true
}));
//initialize passport and passport session
app.use(passport.initialize());  
app.use(passport.session());  

In addition to the above, Passport needs to know how to serialize and deserialize the user object:

passport.serializeUser(function(user, done) {  
  done(null, user);
});
passport.deserializeUser(function(user, done) {  
    done(err, user);
});

Here I am serializing the entire User object but you are free to serialize whatever you want - maybe just the id or the email.
You need to create a route in your express application to map to the callback function (Return URL) that you set up in IdSrv - /login/callback

app.post('/login/callback', passport.authenticate('wsfed-saml2'),  
    function(req, res) {
        res.redirect('/secure');
    }
);

We are adding an additional middleware which authenticates using wsfed-saml2 and if the user is authenticated, only then is the callback called which redirects the user to the secure page. If you run the application as is and browse to http://localhost:3000 then you will see the home page with the hello world text showing. If you browse to http://localhost:3000/secure you should now be redirected to the IdSrv. Once you enter your credentials you will be redirected back to your callback function. You may be surprised to see that it does not think that you are authenticated and you will most likely be stuck in a loop where the call back redirects you back to the login page, which redirects you to the IdSrv, which in turn redirects you back to the callback. If you were authenticated you should have been redirected to the secure page.
The reason Passport fails to authenticate is because it is looking for an xml namespace that Azure uses which happens to be different from what Windows Identity Foundation sets in case of IdSrv. The passport-wsfed-saml2 searches the response from IdSrv for the following namespace:

http://schemas.xmlsoap.org/ws/2005/02/trust  

while Windows Identity Foundation sets the following namespace instead:

http://docs.oasis-open.org/ws-sx/ws-trust/200512  

I submitted a pull request to check for this namespace if the previous one does not exist, but they haven't accepted it yet. You can manually change this in the wsfederation.js file in the node_modules folder or you can

npm install fortepayments/passport-wsfed-saml2 --save  

instead of

npm install passport-wsfed-saml2  

This is a fork of the Auth0 passport-wsfed-saml2 repo with this change added.

This should do it. Run the server again, browse to the secure endpoint and this time you should have an authenticated user and depending on how you set up your User object, req.user will have the necessary information.

Here is a Gist with the complete code.