[Legacy] Authentication with AngularJS and a Node.js REST api

Disclamer: This post is quite old, I'm keeping it only for legacy reason. I once read if you're not ashamed of your previous codes, you're not learning. Well, I'm ashamed of this.

Few months ago, I got intrigued by AngularJS and I decided to start learning it by building some really simple applications. BlogJS is one of them.

Blogjs is a simple blog written with AngularJS, Node.js and MongoDB. You can found a working demo of the front end here and the administration part is accessible here. The username is demo and the password is demo.

You can check the code on github.

Purpose

The goal of this project is to learn how to build an authentication and authorization mecanism with AngularJS and a RESTful api running on a Node.js server. We can't use cookies or sessions like an ordinary web application. So, we will use a token mecanism to authenticate our users.

When a user sends his credentials, the Node.js server checks that they are correct and answers with a unique token built with the user informations. The AngularJS application stores the token in the user' SessionStorage and adds an Authorization header containing the token in every requests made after that. If the endpoint is restricted to authenticated users, the server checks the validity of the token and returns the data if the token is valid or a status code 401 otherwise. In addition to that, the AngularJS application checks if the user is logged in and if he can access the requested route. Otherwise, the user is redirected to the login page.

 Features
  • Create articles
  • Edit articles
  • Delete articles
  • Publish articles
  • Unpublish articles
  • Display articles per date
  • Display articles per tag
  • Authentication & Authorization
Stack
  • AngularJS
  • Node.js with express.js, express-jwt and mongoose
  • MongoDB
Authentication with AngularJS

First, let's create our AdminUserCtrl controller to handle login and logout action.

appControllers.controller('AdminUserCtrl', ['$scope', '$location', '$window', 'UserService', 'AuthenticationService',  
    function AdminUserCtrl($scope, $location, $window, UserService, AuthenticationService) {

        //Admin User Controller (login, logout)
        $scope.logIn = function logIn(username, password) {
            if (username !== undefined && password !== undefined) {

                UserService.logIn(username, password).success(function(data) {
                    AuthenticationService.isLogged = true;
                    $window.sessionStorage.token = data.token;
                    $location.path('/admin');
                }).error(function(status, data) {
                    console.log(status);
                    console.log(data);
                });
            }
        }

        $scope.logout = function logout() {
            if (AuthenticationService.isLogged) {
                AuthenticationService.isLogged = false;
                delete $window.sessionStorage.token;
                $location.path('/');
            }
        }
    }
]);

This controller use two services: UserService and AuthenticationService. The first one handles the calls to the REST api with the credentials. The last one handles the authentication of the user. It's just a boolean indicating if the user is authenticated or not.

appServices.factory('AuthenticationService', function() {  
    var auth = {
        isLogged: false
    }

    return auth;
});
appServices.factory('UserService', function($http) {  
    return {
        logIn: function(username, password) {
            return $http.post(options.api.base_url + '/login', {username: username, password: password});
        },

        logOut: function() {

        }
    }
});

Now, we need to create our authentication form:

    <form class="form-horizontal" role="form">
        <div class="form-group">
            <label for="inputUsername" class="col-sm-4 control-label">Username</label>
            <div class="col-sm-4">
                <input type="text" class="form-control" id="inputUsername" placeholder="Username" ng-model="login.email">
            </div>
        </div>
        <div class="form-group">
            <label for="inputPassword" class="col-sm-4 control-label">Password</label>
            <div class="col-sm-4">
                <input type="password" class="form-control" id="inputPassword" placeholder="Password" ng-model="login.password">
            </div>
        </div>
        <div class="form-group">
            <div class="col-sm-offset-4 col-sm-10">
                <button type="submit" class="btn btn-default" ng-click="logIn(login.email, login.password)">Log In</button>
            </div>
        </div>
    </form>

When the user sends his credentials, our controller sends them to our Node.js server and if the credentials are good, we set the isLogged boolean to true in our AuthenticationService. We also store the Token received from the server for the next restricted requests. But we'll see that when we take a look at the Node.js part.

Now, we add a specific header to each request: Authorization: Bearer <Stored Token>. To do that, we create a service TokenInterceptor.

appServices.factory('TokenInterceptor', function ($q, $window, AuthenticationService) {  
      return {
        request: function (config) {
            config.headers = config.headers || {};
            if ($window.sessionStorage.token) {
                config.headers.Authorization = 'Bearer ' + $window.sessionStorage.token;
            }
            return config;
        },

        response: function (response) {
            return response || $q.when(response);
        }
      };
});

Update 9/26/2014: The code changes a bit since this post was written. Now, if the server sends a 401 response (Unauthorized), we remove the session storage key. And, if we receive a 200 response, and the AuthenticationService.isAuthenticated boolean is false but a token exists in the session storage, we set the user as Authenticated through our AuthenticationService (This use case happens when a authenticated user hits refresh).

appServices.factory('TokenInterceptor', function ($q, $window, $location, AuthenticationService) {  
    return {
        request: function (config) {
            config.headers = config.headers || {};
            if ($window.sessionStorage.token) {
                config.headers.Authorization = 'Bearer ' + $window.sessionStorage.token;
            }
            return config;
        },

        requestError: function(rejection) {
            return $q.reject(rejection);
        },

        /* Set Authentication.isAuthenticated to true if 200 received */
        response: function (response) {
            if (response != null &amp;&amp; response.status == 200 &amp;&amp; $window.sessionStorage.token &amp;&amp; !AuthenticationService.isAuthenticated) {
                AuthenticationService.isAuthenticated = true;
            }
            return response || $q.when(response);
        },

        /* Revoke client authentication if 401 is received */
        responseError: function(rejection) {
            if (rejection != null &amp;&amp; rejection.status === 401 &amp;&amp; ($window.sessionStorage.token || AuthenticationService.isAuthenticated)) {
                delete $window.sessionStorage.token;
                AuthenticationService.isAuthenticated = false;
                $location.path(&quot;/admin/login&quot;);
            }

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

And we add this interceptor in the $httpProvider:

app.config(function ($httpProvider) {  
    $httpProvider.interceptors.push('TokenInterceptor');
});

Now, we have to configure our routes to let AngularJS knows when a specific route needs an authentication, and when it's the case, we have to check the user is authenticated by checking the isLogged value of the AuthenticationService:

app.config(['$locationProvider', '$routeProvider',  
  function($location, $routeProvider) {
    $routeProvider.
        when('/', {
            templateUrl: 'partials/post.list.html',
            controller: 'PostListCtrl',
            access: { requiredLogin: false }
        }).
        when('/post/:id', {
            templateUrl: 'partials/post.view.html',
            controller: 'PostViewCtrl',
            access: { requiredLogin: false }
        }).
        when('/tag/:tagName', {
            templateUrl: 'partials/post.list.html',
            controller: 'PostListTagCtrl',
            access: { requiredLogin: false }
        }).
        when('/admin', {
            templateUrl: 'partials/admin.post.list.html',
            controller: 'AdminPostListCtrl',
            access: { requiredLogin: true }
        }).
        when('/admin/post/create', {
            templateUrl: 'partials/admin.post.create.html',
            controller: 'AdminPostCreateCtrl',
            access: { requiredLogin: true }
        }).
        when('/admin/post/edit/:id', {
            templateUrl: 'partials/admin.post.edit.html',
            controller: 'AdminPostEditCtrl',
            access: { requiredLogin: true }
        }).
        when('/admin/login', {
            templateUrl: 'partials/admin.login.html',
            controller: 'AdminUserCtrl',
            access: { requiredLogin: false }
        }).
        when('/admin/logout', {
            templateUrl: 'partials/admin.logout.html',
            controller: 'AdminUserCtrl',
            access: { requiredLogin: true }
        }).
        otherwise({
            redirectTo: '/'
        });
}]);

app.run(function($rootScope, $location, AuthenticationService) {  
    $rootScope.$on(&quot;$routeChangeStart&quot;, function(event, nextRoute, currentRoute) {
        if (nextRoute.access.requiredLogin &amp;&amp; !AuthenticationService.isLogged) {
            $location.path(&quot;/admin/login&quot;);
        }
    });
});
Authentication with Node.js and MongoDB

To handle the authentication in our RESTful api, we are using express-jwt (JSON Web Token) to generate a unique Token based on the user informations and to verify it.

First let's create the User Schema in MongoDB. We also create a middleware called before saving any new users into the database in order to bcrypt the password. And we need a method to encrypt the password sent by the user to check if there is a match.

var Schema = mongoose.Schema;

// User schema
var User = new Schema({  
    username: { type: String, required: true, unique: true },
    password: { type: String, required: true}
});

// Bcrypt middleware on UserSchema
User.pre('save', function(next) {  
  var user = this;

  if (!user.isModified('password')) return next();

  bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
    if (err) return next(err);

    bcrypt.hash(user.password, salt, function(err, hash) {
        if (err) return next(err);
        user.password = hash;
        next();
    });
  });
});

//Password verification
User.methods.comparePassword = function(password, cb) {  
    bcrypt.compare(password, this.password, function(err, isMatch) {
        if (err) return cb(err);
        cb(isMatch);
    });
};

Now we are going to create the method to authenticate an user and create the Token:

exports.login = function(req, res) {  
    var username = req.body.username || '';
    var password = req.body.password || '';

    if (username == '' || password == '') {
        return res.send(401);
    }

    db.userModel.findOne({username: username}, function (err, user) {
        if (err) {
            console.log(err);
            return res.send(401);
        }

        user.comparePassword(password, function(isMatch) {
            if (!isMatch) {
                console.log(&quot;Attempt failed to login with &quot; + user.username);
                return res.send(401);
            }

            var token = jwt.sign(user, secret.secretToken, { expiresInMinutes: 60 });

            return res.json({token:token});
        });

    });
};

Finally, we have to add the jwt middleware in any routes we want to restrict the access to authenticated user:

/*
    Get all published posts
*/
app.get('/post', routes.posts.list);  
/*
    Get all posts
*/
app.get('/post/all', jwt({secret: secret.secretToken}), routes.posts.listAll);

/*
    Get an existing post. Require url
*/
app.get('/post/:id', routes.posts.read);

/*
    Get posts by tag
*/
app.get('/tag/:tagName', routes.posts.listByTag);

/*
    Login
*/
app.post('/login', routes.users.login);

/*
    Logout
*/
app.get('/logout', routes.users.logout);

/*
    Create a new post. Require data
*/
app.post('/post', jwt({secret: secret.secretToken}), routes.posts.create);

/*
    Update an existing post. Require id
*/
app.put('/post', jwt({secret: secret.secretToken}), routes.posts.update);

/*
    Delete an existing post. Require id
*/
app.delete('/post/:id', jwt({secret: secret.secretToken}), routes.posts.delete);  

Your application is now restricted to authenticated users only. Shoot me a tweet if you have any questions: @kdelemme

comments powered by Disqus