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