Monthly Archives: October 2017

Angular + Nodejs Authentication with Passport & JWT

The codebase for this lesson can be found at ng-node-passport.kit

This builds off of the earlier work of nodejs-starter-kit. You should have a firm grasp of angular and nodejs from this example before reading on.

The authentication is built from passportjs and jwt. Let’s first talk about these two.

PassportJs

This is an authentication middleware for Node.js. It has many ways to authenticate users (they call these “Strategies”). You can use it to authenticate users via their Facebook, Google, or Twitter account for example.

In this template, we use a basic authentication scheme where the user database is stored in house (specifically in a mysql database). We leverage the passport-http Strategy for this.

There are other strategies we could have employed with various trade-offs. Here is a comparison of a shortlist of them on StackOverflow.

JWT

JWT stands for JSON Web Tokens. It’s an open industry standard to represent claims between parties. For us, this simply means we can log in once and then pass a token to maintain our authentication session.

Authentication Model

Before we dive into the code, let’s understand what we will be doing.

Registration Workflow

The user submits their username and password and it gets submitted in plaintext across the wire (so please use SSL/HTTPS).

In the backend, we salt and encrypt the password before storing it into the database so the original plaintext password can (virtually) never be revealed.

Login Workflow

On the login page, the user submits their username and password and it gets submitted in plaintext across the wire¬†(so please use SSL/HTTPS). In our implementation, this is done by creating an Authorization header and base64-encoding the username+”:”+password so it looks something like this:

Authorization: Basic dXNlcjp0ZXN0

The backend compares the credentials with the ones stored in the database. If there is a match, then we sign a JWT token, store a payload containing the username and send that back.

You should note that while you can put anything in the payload, you should never put anything sensitive here, like a password. The JWT token is signed but not encrypted. This means anyone can read it. The signing only prevents someone from modifying it. If you wish to put sensitive info in the payload, you should look into JWE, an encrypted implementation of JWT. See this article

When the frontend receives this token, it should store it somewhere. On future requests, it should include it as an Authorization header like so

Authorization: Bearer eyJdbGciOiJIU44I1NIsInR5cCI6IkXXVCJ9

When new requests are made, the backend first decodes the JWT token and pulls out the payload with the user info. At this time, you can be sure that no one has modified the token and the user is real since it can verify that it was signed by itself.

Backend Code

server.js

This is the main server file.

app.use(auth.passport.initialize());

This line is needed to initialize passport.

app.post('/auth/register'
        , auth.registerUser);
app.post('/auth/login'
//        , auth.passport.authenticate('basic',{session:false}) // causes challenge pop-up on 401
        , auth.authenticateViaPassport
        , auth.generateJWT
        , auth.returnAuthResponse
        );

These two routes define the two workflows we discussed above. We’ll dive into each of them below in shared/auth.js.

One thing I want to bring your attention to now is the /auth/login route. It has 3 express middlewares addressing the request: authenticateViaPassport, generateJWT, and returnAuthResponse. The way express middlewares work is that each layer handles the request and then the next layer continues the processing. Any layer can reject the request, stopping the processing at any point.

app.get('/stuff/:stuffId'
      , auth.ensureAuthenticatedElseError
      , myroute.getStuff);

To protect an API by requiring authentication, simply add the auth.ensureAuthenticatedElseError middleware. We’ll go into the code in the next section.

shared/auth.js

var jwt = require('jsonwebtoken');
const JWT_EXPIRATION = (60 * 60); // one hour
var uuidv4 = require('uuid/v4');
const SERVER_SECRET = uuidv4();

jsonwebtoken is the node module you’ll need to sign and verify jwt tokens. The expiration is encoded into the token and taken into account when verifying whether it’s still valid.

You need a SERVER_SECRET to sign and verify the tokens. It can be any string you’d like. It’s like a password so don’t give it out. So what I did here was to generate a random one every time the server starts up. (It means tokens are not valid after a restart, but you get a little more security)

exports.passport = require('passport');
var BasicStrategy = require('passport-http').BasicStrategy;

These lines bring in the passport dependencies.

exports.registerUser = function(req, res) {
  var userid = req.body.userid;
  var plaintextPassword = req.body.password;

  bcrypt.hash(plaintextPassword, saltRounds)
    .then(function(hash) {
      var sql = 'INSERT INTO user(userid,passhash) VALUES(?,?)';
      return dbp.pool.query(sql, [userid, hash]);
    })
...

The registration method takes the credentials, encrypts the password and stores it in the DB. Our user table consists of an userid and passhash column.

The next 4 snippets of code implement the login functions.

exports.authenticateViaPassport = function(req, res, next) {
  exports.passport.authenticate('basic',{session:false},
    function(err, user, info) {
      if(!user){
        res.set('WWW-Authenticate', 'x'+info); // change to xBasic
        res.status(401).send('Invalid Authentication');
      } else {
        req.user = user;
        next();
      }
    }
  )(req, res, next);
};

This first middleware is not always necessary. The /auth/login route could have directly called “auth.passport.authenticate(‘basic’,{session:false})” instead of this. But on authentication failure, it sends back a 401 HTTP status along with the header “WWW-Authenticate: Basic …” These two things cause browsers to pop up a username/password dialog. I wanted to handle 401’s in a custom way, so this layer simply changes the WWW-Authenticate header, preventing the dialog. We could also have changed the 401 status code.

The auth.passport.authenticate() method eventually calls this:

exports.passport.use(new BasicStrategy(
  function(userid, plainTextPassword, done) {
    var sql = 'SELECT *'
            +' FROM user'
            +' WHERE userid=?';

    dbp.pool.query(sql, [userid])
      .then(function(rows) {
        if( rows.length ) {
          var hashedPwd = rows[0].passhash;
          return bcrypt.compare(plainTextPassword, hashedPwd);
        } else {
          return false;
        }
      })
...

This takes the login credentials, compares its hashed form with the database entry and returns the user to the next layer.

exports.generateJWT = function(req, res, next) {
  var payload = {
      exp: Math.floor(Date.now() / 1000) + JWT_EXPIRATION
    , user: req.user,
//    , role: role
  };
  req.token = jwt.sign(payload, SERVER_SECRET);
  next();
}

If we get to the generateJWT layer, that means the credentials were valid. So we now sign and generate a JWT token. Notice we put the user into the payload. You can put anything you’d like in it, including permissions and roles.

exports.returnAuthResponse = function(req, res) {
  res.status(200).json({
    user: req.user,
    token: req.token
  });
}

This simply returns the user and JWT token to the login request. This completes the login implementation.

exports.ensureAuthenticatedElseError = function(req, res, next) {
  var token = getToken(req.headers);
  if( token ) {
    var payload = jwt.decode(token, SERVER_SECRET);
    if( payload ) {
//      console.log('payload: ' + JSON.stringify(payload));
      // check if user still exists in database if you'd like
      res.locals.user = payload.user;
      next();
...

This middleware is used whenever you want to protect an API. It parses the Authorization header’s jwt token and decodes it. If successful, you should have the payload that contains the user. I stick this in res.locals so the next request middleware/handler has access to it. If you look inside routes/sampleroute.js, specifically at the getStuff() method, you’ll see the line that accesses it. You may want to return resources specifically relevant to the given user.

With this backend, you can actually test it using Postman or curl. You can send the appropriate json messages and Authorization headers to simulate registration, login, and accessing protected API’s.

For example, you can try the following procedure:

  1. POST /auth/register with {userid:user, password:test}
    This should create an entry in your database with a hashed password
  2. POST /auth/login with an Authorization header “Basic dXNlcjpwYXNz”
    This should give you back a json response like
    {user:{userid: “user”}, token: “[JWT_TOKEN]”}
  3. GET /stuff/1 with Authorization header “Bearer¬†[JWT_TOKEN]” (copy JWT_TOKEN from step 2.
    This should give you back a list of stuff

Frontend Code

We use angular to handle the authentication handshake. There are some angular mechanisms that make this easy to do.

I’ll skip the ng/register/controller.js and ng/login/controller.js. They’re trivial and pretty self-explanatory.

The core of the authentication is in assets/js/auth.js.

assets/js/auth.js

The AuthService factory implements the register, logIn, and logOut functions.

  authService.logIn = function(userid, password) {
    return $http({
      method: 'POST',
      url: '/auth/login',
      headers: {
        'Authorization': 'Basic ' + btoa(userid + ':' + password)
      }
    })
      .then(function(resp) {
        var user = null;
        if( resp.data ) {
          user = resp.data.user;
          UserSession.create(user, resp.data.token);
        }
        return user;
      })
  };

In logIn(), notice we set the Authorization header, passing a base64-encoded userid and password. Remember that on successful authentication, we get back a user and token. We store that in a UserSession service. For now, this is all you need to know. We’ll dive deeper into it later.

Next, I want to direct your attention to the http interceptor:

app.factory('authHandler', [
    '$q'
  , '$window'
  , '$location'
  , 'UserSession'
  , function($q
           , $window
           , $location
           , UserSession
           ) {
    return {
      request: function(config) {
        config.headers = config.headers || {};
        var token = UserSession.getToken();
        if( token
        &&  !config.headers.Authorization
        ) {
            config.headers.Authorization = 'Bearer ' + token;
        }
        return config;
      },
      responseError: function(rejection) {
        if (rejection != null && rejection.status === 401) {
          UserSession.destroy();
          $location.url("/login");
        }

        return $q.reject(rejection);
      }
    };
}]);

This code intercepts all http requests and responses that result in an error.

The request interceptor checks if we have a token from UserSession and attaches it as an Authorization header.

The response error interceptor looks for 401 authentication errors. Remember that our backend throws a 401 when the user is not authenticated when using a protected API. Here, we intercept the 401 and redirect the user to the /login page.

Now let’s jump back to the UserSession. First at the top of the auth.js file, you’ll notice this

const USER_ACTIVE_UNTIL = {
  pageRefresh: 'localvar',
  sessionExpires: 'sessionStorage',
  forever: 'localStorage'
}

const SESSION_PERSISTANCE = USER_ACTIVE_UNTIL.forever;

You can manually set how the login session should be persisted. What does this mean and why does it matter? So we can store the user and token either as local variables, in the session storage, or the local storage.

If you store it in local variables, as soon as you refresh the page, it all goes away. Certainly if you open another tab or close/reopen your browser, you will no longer be logged in because the user and token are gone. If you want this behavior, set SESSION_PERSISTANCE to USER_ACTIVE_UNTIL.pageRefresh

If you set SESSION_PERSISTANCE to USER_ACTIVE_UNTIL.sessionExpires, then as long as you’re in the browser tab, you’re logged in. You can refresh the page and still be logged in. However if you close the tab, or ctrl-click to open a new tab, it won’t carry the authentication data and you’ll have to relogin.

If you set SESSION_PERSISTANCE to USER_ACTIVE_UNTIL.forever, then you’ll always be logged in (until your token expires, currently one hour by default). You can close your browser and you’ll be logged in when you go back. You can ctrl-click to open other tabs and those will share your login session as well. This is the default behavior configured.

One last thing to note about this model, which is less about authentication and more about display. You’ll probably want to display the user and notify widgets on your page when the user is logged in and out.

I followed the technique here for this model. It involves broadcasting logins, logouts, and failures which can be seen in ng/userCtrl/controller.js. This controller also holds the user in its scope. And most importantly, this controller is bound to the top-level node, which is the <body> of the index.html page.

From this model, you can have various components listen for the broadcast messages and do something appropriate. e.g. once user logs in, expose user controls or perform some other action.

Having the controller at the top-level and a $scope.user means you can access the user info from any of your pages. (We could also have used $rootScope but I also try not to use global variables in my apps whenever possible.)

I hope this description has helped you understand and apply authentication to your application. You can use the template and build your app from it, or copy the relevant components over (primarily the backend auth.js and frontend auth.js). Keep in mind also that since the protocol is standard and over HTTP, there’s nothing that stops you from replacing the frontend or backend here. You can swap out the angular frontend with something else that adds in the Authorization headers appropriately. Similarly, you can swap out the nodejs backend with something that signs and verifies jwt tokens.

Advertisements
Tagged , , , ,