Thursday 8 August 2013

Authentication in AngularJS, node.js and passport

I'm writing this as much for my own good as a reminder of how I got to here as a newbie Angular and node developer. The aim was to get an Angular app authenticating with a node.js server using passport middleware. I wanted to use a directive on the Angular side to manage the UI and have the Angular code layered so that I could plug in different authentication schemes if I needed to.

Passport is node 'middleware' that manages authentication for an application, the code currently uses 'local' authentication as this is simply a learning exercise.

On the client I created an Angular directive that shows the UI and calls a controller that manages the authentication. The controller looks like this:


AuthenticateCtrl = function ($scope, $http, $location, $cookies, authenticate) {

    var isSignedIn = false;

    $scope.isError = false;

    $scope.isSignedIn = function () {
        return authenticate.isAuthenticated;
    };
    $scope.signout = function () {
        var promise = authenticate.signOut();
        promise.success(function () {
            authenticate.isAuthenticated = false;
        }).error(function () {
        });
    };
    $scope.signin = function (user) {
        var promise = authenticate.signIn(user);
        promise.success(function (data, status, header) {
            authenticate.isAuthenticated = true
            $location.path( "/" );
        }).error(function () {
        });
    };
    $scope.register = function () {
        var promise = authenticate.register();
        promise.success(function (data, status, header) {
        }).error(function () {
        });
    };
};

The controller offers three methods and a property to the directive (this controller is used by the authentication directive). The isSignedIn property is used by the directive to manage the parts of the UI that are shown. The signIn method is where the action takes place. The controller is injected with an 'authentication' service and it's this service that does the work. The controller expects the service to have similar signIn and signOut functions and that these functions return promises. The code is written this way as I want the service to carry out the logic of the authentication process but I want the controller to be in charge of what to do on success/failure as this is a UI consideration. Here the controller sets a flag for the directive to use and also sets the browser location so that the app navigates if the authentication is successful.

Next the authentication server code:

authenticate = function ($http, $cookies) {

    var authenticated = $cookies.user ? true : false;

    var signIn = function (userData) {
        if (userData) {
            var promise = $http.post("/authenticate/local", userData);
            return promise;
        }
    };

    var signOut = function () {
        return $http.get("/logout");
    };


    var register = function () {
    }

    return {
        signIn: signIn,
        signOut: signOut,
        isAuthenticated: authenticated,
        register: register
    }
}

Surprisingly simple.

The code calls $http.post against a specific URL and returns the promise from that call, the URL should probably be parameterised. So far, so straightforward.

The only slightly odd thing is the way that the authenticated value is initialised, var authenticated = $cookies.user ? true : false;. It looks for a cookie called user and if that exists assumes that the user is authenticated. That is done for the case where the app is initialised through a 'refresh', for example the user typing a URL into the browser. In that case we will go straight to the server and not get a chance to run any authentication checks. The server will test that the user is authorised and if so return the content, at that point the app wants to display that content unless the user is not authenticated. The server will generate a user cookie if the user is authenticated and that will be stored permanently in the browser (the code does not handle expiry yet). If this cookie exists I assume the user is authenticated and set the 'authenticated' flag.

I wanted the authentication to work with routing, this means configuring the route provider.

var module = angular.module(appName, ['directives', 'localization', 'ngCookies']);

module.config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider) {
    $locationProvider.html5Mode(true);
    $routeProvider.
        when('/', {
            templateUrl: 'partials/index.html',
            controller: 'IndexCtrl',
            authenticate: true
        }).
        when('/login', {
            templateUrl: 'partials/login.html',
            authenticate: false
        }).
        when('/logout', {
            redirectTo: '/login',
            controller: 'LogoutCtrl',
            authenticate: false
        }).
        otherwise({
            redirectTo: '/'
        });
}]);

module.run(['$rootScope', '$location', 'authenticate', function ($rootScope, $location, authenticate) {
    $rootScope.$on("$routeChangeStart", function (event, next, current) {
        if((typeof(next.$route.authenticate) === "undefined" || next.$route.authenticate) 
            && !authenticate.isAuthenticated){
            $location.path( "/login" );
        }
    })
}]);

Firstly the routes. The routes are configured to have an 'authenticate' field which tells the code whether this route is protected, notice that login and logout are set to false otherwise nobody could ever login. The only other route is / and you have to be authenticated to go here. You can extend this scheme to do role based authentication as is shown here which is where I got this idea from.

The other key part of this is the handling of the route changed event. The code handles the $routeChangeStart event and checks two things: does the route require authentication and is the user currently authenticated? This is where Angular's singletons are very useful, the authentication service is a singleton, the isAuthenticated value will either have been set when we signed on, or it will have been set because $cookies.user existed. This means that if a route requires authentication and we are not authenticated then we set the browser location to the login page and get the user to login, otherwise we go to the actual route. Notice that the default is to require authentication, so if the authenticate flag is missing it's assumed to be true.

Finally the server. I've basically left the default passport code alone apart from adding two things. On successful authentication I add a cookie, and I remove the cookie when the user signs out.

app.post('/authenticate/local', function (req, res, next) {
    passport.authenticate('local', function (err, user, info) {
        if (err) {
            return next(err);
        }
        if (!user) {
            return res.send(401);
        }

        req.logIn(user, function (err) {
            if (err) {
                return next(err);
            }
            res.cookie('user', JSON.stringify({'id': user.id}), { httpOnly: false } );
            return res.send(200);
        });
    })(req, res, next);

});

app.get('/logout', function(req, res){
    req.logout();
    res.clearCookie("user");
    res.redirect('/');
});

Again, there's no explicit expiry on this yet, that's next

9 comments:

  1. Great posting, almost exactly what I was looking for! Clean and simple code. But as I newbie, I am craving for full examples ;) How does a simple directive take uses this look like and such. But ok, I will need to do research :)

    I was first stumbled by the fact the controller and the services did not use the "normal" style of "app.controller(..." and "app.factory(...", but I figured it out after reading more in the Angular docs.

    But I do not fully understand the "req.logIn(..." in the node code. "req" is as I understand an object containing information about the http request that raised the event. I am guessing the logIn is a function you wrote to make the actual login. But how did it get "attached" to the request object (and does it belong there)? Have not seen it like this in any tutorial I have read...

    Anyway, thanks for a great posting!

    ReplyDelete
    Replies
    1. Hey Mads,

      thanks for the comments.

      req.login is a method added by passport.js - I'm using passport to manage the authentication.

      http://passportjs.org

      I've put the code here: https://github.com/kevinrjones/timesheet

      Kevin

      Delete
  2. A small remark for people who just copy the code: the 'user' cookie can easily be changed by a hacker to another userid, thereby spoofing the request as another user.

    ReplyDelete
    Replies
    1. Not sure that is the case. The user cookie is only used by the client. The authentication and authorisation is done by passport on the server. The only thing the server does with the user cookie is to create it and clear it.

      Delete
  3. Great Tutorial! This is exactly what I was looking for.

    Excuse my silly question, but can the server be set to use Restify instead of Express?

    I'm trying to login users before giving access to a RESTful server built using Restify and Node.

    ReplyDelete
    Replies
    1. to clarify, when I say "server" i'm only referring to the API part, not the public/angular frontend

      Delete
    2. Faisai, I've not tried it (I was thinking of using Restify on another project, but hey, inertia!) but it should work

      Delete
  4. Hi Kevin. Thanks for your article. I have been looking for this article for a long time now, and I have also found this other related article that maybe interests you: https://vickev.com/#!/article/authentication-in-single-page-applications-node-js-passportjs-angularjs.

    ReplyDelete
  5. Hi Kevin.
    Thanks for your article. It has been very useful for me. I'm a newbie in angular. One question, Where do you define user property in your scope, because I've seen how you use to get username, password, role, etc.

    Thanks!!!!

    ReplyDelete