Tuesday, July 30, 2013

Setting up Auth in an Angular App

So I was tasked to set up some type of simple authentication system using Angular.  The basis was that there be a login page, and a main page.  If a user navigates directly to the app, then they go to the login page.  If the user clicks an authorized link, they are logged in and taken to the main page.  So to get started I laid out my app with three routes:


var tmapp = angular.module('tmapp', [])
.config(['$routeProvider', function($routeProvider) {
    $routeProvider
        .when('/',{templateUrl: '/partials/admin/login.html',   controller: LoginController})
        .when('/main',{templateUrl:'/partials/admin/splash.html',controller:SplashController})
        .otherwise({redirectionTo:'/'});
}]);

Simple enough, the base route displays the login page, and the main route displays the splash page. Now for problem number one, if I type in /main, the I am able to visit that page. Obfuscating the code would make it harder, but definitely not too hard to see the routes my app has, so this isn't secure.

To solve the problem, we can add a .run function. This will get ran when the app is initialized. We can tie in some pretty important event bindings at this level to ensure that the proper authentication is happening.

tmapp.run(['$rootScope',function($rootScope){
}]);

Now inside, we can use the $rootScope as a $scope variable. Think of $rootScope as the parent of $scope, and all $scope variables inherit the properties of $rootScope. So if I make an event binding on $rootScope, then the child $scope also has that binding. For instance:

tmapp.run(['$rootScope',function($rootScope){
    $rootScope.$on('$routeChangeStart', function(event, next, current){
        //check if logged in
        //redirect if not
    }
}]);

Now every time a route is called, this function will be called. Of course, nothing yet exists in the function, but we could test it using console.log or alert and we would see that every time the route changed, the function was called.

If I want to check logged in status, where do I put it? Not in a controller, because those only handle functions specific for the route, and this function would need to be for ALL routes AND be persistent between routes. If I set $scope.myVar=1 with a button on my view that increments it by 1 each click, you would see the count increase. If I refreshed the route, the count would be back at 1 again, so $scope data is not persistent. The answer is to create a service. Services are static across all routes, so it would be a perfect place to check and store authentication. To set up a service, you use the factory method.

.factory('sessionHandler',['$http','$location',function($http,$location){
    var setLoggedIn=function(){
        s['auth']=true;
    };
    var redirectHome=function(){
        s={};
        $location.path('/',true);
    };
    var s={};
    return {
        set:function(key,val){
            s[key]=val;
        },
        get:function(key){
            if(!angular.isDefined(s[key])){
                return false;
            }
            return s[key];
        },
        logout:function(){
            $http.get('/admin/logout').success(function(){
                s={};
                window.location=window.location.origin+window.location.pathname;
            });
        },
        checkLoggedIn:function(){
            if(s['auth']){
                return true;
            }else{
                $http.get('/admin/authStatus').success(setLoggedIn).error(redirectHome);
            }
        }
    };
}]);

This is a big blob of code that I am now going to walk through and explain.  Again, the first line is the simple syntax to set up your service, injecting specific angular services into my service.  Then we move on to some private functions (setLoggedIn and redirectHome).  The first sets a variable called s['auth'] as true, which is the property we look for when the service starts to see if a user is logged in.  The second function essentially unsets our s variable and takes them to the login screen, so this is called when they are not logged in.  Finally, we return the service functions that interact with the private functions.  The functions set and get can be used to set/get data across the controllers, much like session variables can be used on the server.  The logout function sends the request to the server to log the user out, resets the s variable, and refreshes the app, which takes them back to the initial route (/).  The last function, checkLoggedIn, checks for s['auth'].  If it can't find it, it makes the request to the server to see if the user is logged in on the server.  If it gets a successful response, it sets the s['auth'] variable, otherwise it redirects them to the home and unsets any data they had.

So now to implement this, in our app.run function.

tmapp.run(['$rootScope','sessionHandler','$location',function($rootScope,sessionHandler,$location){
    $rootScope.$on('$routeChangeStart', function(event, next, current){
        if(sessionHandler.checkLoggedIn()&&($location.path()===''||$location.path()==='/')){
            $location.path('/main',true);    
        }
    }
}]);

Let's walk through this. First, notice the additional dependencies I have injected: my service (sessionHandler), and $location. Second, the if statement executes checkLoggedIn(). If it fails, it will automatically redirect the user to the home page because that is what checkLoggedIn does in the function. Next, it checks the current location. If they are on the login page, which has a blank or null route, then it will redirect them to the main page. Otherwise, the function just leaves the user alone.

If a user navigates to the main page without logging in, the service will catch that and redirect the user back to the login page. If they login, and then try navigating back to the login page, the check will catch that as well and redirect the user to the main page. That is it!

Now for some advanced material. Using sessionStorage. If a user refreshes the page, their data will be lost. s['auth'] will be undefined, and the check will need to take place again. Any other variables you set in the sessionHandler service will be wiped out. So to implement this, we need to modify the sessionHandler.

.factory('sessionHandler',['$http','$location',function($http,$location){
    var setLoggedIn=function(){
        s['auth']=true;
        sessionStorage.data=angular.toJson(s);
    };
    var redirectHome=function(){
        s={};
        sessionStorage.data=angular.toJson(s);
        $location.path('/',true);
    };
    if(angular.isDefined(sessionStorage.init)){
        var s=angular.fromJson(sessionStorage.data);
    }else{
        var s={};
        sessionStorage.init=true;
    }
    return {
        set:function(key,val){
            s[key]=val;
            sessionStorage.data=angular.toJson(s);
        },
        get:function(key){
            if(!angular.isDefined(s[key])){
                return false;
            }
            return s[key];
        },
        logout:function(){
            $http.get('/admin/logout').success(function(){
                s={};
                sessionStorage.data=angular.toJson(s);
                window.location=window.location.origin+window.location.pathname;
            });
        },
        checkLoggedIn:function(){
            if(s['auth']){
                return true;
            }else{
                $http.get('/admin/authStatus').success(setLoggedIn).error(redirectHome);
            }
        }
    };
}]);

You will notice just a few additional lines, but most notably, the if statement. What it does is check to see if a sessionStorage variable is currently set. If it is, then it grabs the data and parses it into our "s" variable. If not, it initializes the session storage and an empty "s" variable. All the other lines simply store data to the sessionStorage.data variable in JSON format. This is done to minimize the amount of variables stored by the browser, and hopefully help hide some of the data. The other reason I chose to do it this way is because sessionStorage does not store objects. Even simple objects without functions cannot be stored in sessionStorage, only strings. So by JSONifying the object, you can store it easily in sessionStorage. Everything else is pretty self explanatory. You can now use the service in your controllers and persist data AND logins across a page refresh.

That concludes my tutorial for now. It is very simple, and I didn't cover anything else except using the app.run and app.factory methods. I also didn't include data on the logout function, but I am sure you can add the route yourselves and call the service function. If there is any other questions or issues, let me know!