Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AngularJS- Login and Authentication in each route and controller

I have an AngularJS application created by using yeoman, grunt and bower.

I have a login page that has a controller that checks for authentication. If the credentials are correct I reroute to home page.

app.js

'use strict';
//Define Routing for app
angular.module('myApp', []).config(['$routeProvider', '$locationProvider',
  function($routeProvider,$locationProvider) {
    $routeProvider
    .when('/login', {
        templateUrl: 'login.html',
        controller: 'LoginController'
    })
    .when('/register', {
        templateUrl: 'register.html',
        controller: 'RegisterController'
      })
    .when('/forgotPassword', {
        templateUrl: 'forgotpassword.html',
        controller: 'forgotController'
      })
   .when('/home', {
       templateUrl: 'views/home.html',
       controller: 'homeController'
    })
    .otherwise({
       redirectTo: '/login'
    });
//    $locationProvider.html5Mode(true); //Remove the '#' from URL.
}]);

angular.module('myApp').factory("page", function($rootScope){
    var page={};
    var user={};
    page.setPage=function(title,bodyClass){
        $rootScope.pageTitle = title;
        $rootScope.bodylayout=bodyClass;
    };
    page.setUser=function(user){
        $rootScope.user=user;
    }
    return page;
});

LoginControler.js

'use strict';

angular.module('myApp').controller('LoginController', function($scope, $location, $window,page) {
    page.setPage("Login","login-layout");
    $scope.user = {};
    $scope.loginUser=function()
    {
        var username=$scope.user.name;
        var password=$scope.user.password;
        if(username=="admin" && password=="admin123")
        {
            page.setUser($scope.user);
            $location.path( "/home" );
        }
        else
        {
            $scope.message="Error";
            $scope.messagecolor="alert alert-danger";
        }
    }
});

On the home page I have

<span class="user-info">
    <small>Welcome,</small>
    {{user.name}}
</span>
<span class="logout"><a href="" ng-click="logoutUser()">Logout</a></span>

In the loginController, I check the login info and if it's successful, I set the user object in the service factory. I don't know whether this is correct or not.

What I need is, When the user is logged in, It sets some value in the user object so that all other pages can get that value.

Whenever any route changes happen, the controller should check if the user is logged in or not. If not, it should reroute to the login page. Also, if the user is already logged in and come back to the page, it should go to home page. The controller should also check the credentials on all of the routes.

I have heard about ng-cookies, but I don't know how to use them.

Many of the examples I saw were not very clear and they use some kind of access roles or something. I don't want that. I only want a login filter. Can someone give me some ideas?

like image 739
iCode Avatar asked Jan 07 '14 10:01

iCode


7 Answers

My solution breaks down in 3 parts: the state of the user is stored in a service, in the run method you watch when the route changes and you check if the user is allowed to access the requested page, in your main controller you watch if the state of the user change.

app.run(['$rootScope', '$location', 'Auth', function ($rootScope, $location, Auth) {
    $rootScope.$on('$routeChangeStart', function (event) {

        if (!Auth.isLoggedIn()) {
            console.log('DENY');
            event.preventDefault();
            $location.path('/login');
        }
        else {
            console.log('ALLOW');
            $location.path('/home');
        }
    });
}]);

You should create a service (I will name it Auth) which will handle the user object and have a method to know if the user is logged or not.

service:

 .factory('Auth', function(){
var user;

return{
    setUser : function(aUser){
        user = aUser;
    },
    isLoggedIn : function(){
        return(user)? user : false;
    }
  }
})

From your app.run, you should listen the $routeChangeStart event. When the route will change, it will check if the user is logged (the isLoggedIn method should handle it). It won't load the requested route if the user is not logged and it will redirect the user to the right page (in your case login).

The loginController should be used in your login page to handle login. It should just interract with the Auth service and set the user as logged or not.

loginController:

.controller('loginCtrl', [ '$scope', 'Auth', function ($scope, Auth) {
  //submit
  $scope.login = function () {
    // Ask to the server, do your job and THEN set the user

    Auth.setUser(user); //Update the state of the user in the app
  };
}])

From your main controller, you could listen if the user state change and react with a redirection.

.controller('mainCtrl', ['$scope', 'Auth', '$location', function ($scope, Auth, $location) {

  $scope.$watch(Auth.isLoggedIn, function (value, oldValue) {

    if(!value && oldValue) {
      console.log("Disconnect");
      $location.path('/login');
    }

    if(value) {
      console.log("Connect");
      //Do something when the user is connected
    }

  }, true);
like image 162
gab Avatar answered Oct 04 '22 19:10

gab


Here is another possible solution, using the resolve attribute of the $stateProvider or the $routeProvider. Example with $stateProvider:

.config(["$stateProvider", function ($stateProvider) {

  $stateProvider

  .state("forbidden", {
    /* ... */
  })

  .state("signIn", {
    /* ... */
    resolve: {
      access: ["Access", function (Access) { return Access.isAnonymous(); }],
    }
  })

  .state("home", {
    /* ... */
    resolve: {
      access: ["Access", function (Access) { return Access.isAuthenticated(); }],
    }
  })

  .state("admin", {
    /* ... */
    resolve: {
      access: ["Access", function (Access) { return Access.hasRole("ROLE_ADMIN"); }],
    }
  });

}])

Access resolves or rejects a promise depending on the current user rights:

.factory("Access", ["$q", "UserProfile", function ($q, UserProfile) {

  var Access = {

    OK: 200,

    // "we don't know who you are, so we can't say if you're authorized to access
    // this resource or not yet, please sign in first"
    UNAUTHORIZED: 401,

    // "we know who you are, and your profile does not allow you to access this resource"
    FORBIDDEN: 403,

    hasRole: function (role) {
      return UserProfile.then(function (userProfile) {
        if (userProfile.$hasRole(role)) {
          return Access.OK;
        } else if (userProfile.$isAnonymous()) {
          return $q.reject(Access.UNAUTHORIZED);
        } else {
          return $q.reject(Access.FORBIDDEN);
        }
      });
    },

    hasAnyRole: function (roles) {
      return UserProfile.then(function (userProfile) {
        if (userProfile.$hasAnyRole(roles)) {
          return Access.OK;
        } else if (userProfile.$isAnonymous()) {
          return $q.reject(Access.UNAUTHORIZED);
        } else {
          return $q.reject(Access.FORBIDDEN);
        }
      });
    },

    isAnonymous: function () {
      return UserProfile.then(function (userProfile) {
        if (userProfile.$isAnonymous()) {
          return Access.OK;
        } else {
          return $q.reject(Access.FORBIDDEN);
        }
      });
    },

    isAuthenticated: function () {
      return UserProfile.then(function (userProfile) {
        if (userProfile.$isAuthenticated()) {
          return Access.OK;
        } else {
          return $q.reject(Access.UNAUTHORIZED);
        }
      });
    }

  };

  return Access;

}])

UserProfile copies the current user properties, and implement the $hasRole, $hasAnyRole, $isAnonymous and $isAuthenticated methods logic (plus a $refresh method, explained later):

.factory("UserProfile", ["Auth", function (Auth) {

  var userProfile = {};

  var clearUserProfile = function () {
    for (var prop in userProfile) {
      if (userProfile.hasOwnProperty(prop)) {
        delete userProfile[prop];
      }
    }
  };

  var fetchUserProfile = function () {
    return Auth.getProfile().then(function (response) {
      clearUserProfile();
      return angular.extend(userProfile, response.data, {

        $refresh: fetchUserProfile,

        $hasRole: function (role) {
          return userProfile.roles.indexOf(role) >= 0;
        },

        $hasAnyRole: function (roles) {
          return !!userProfile.roles.filter(function (role) {
            return roles.indexOf(role) >= 0;
          }).length;
        },

        $isAnonymous: function () {
          return userProfile.anonymous;
        },

        $isAuthenticated: function () {
          return !userProfile.anonymous;
        }

      });
    });
  };

  return fetchUserProfile();

}])

Auth is in charge of requesting the server, to know the user profile (linked to an access token attached to the request for example):

.service("Auth", ["$http", function ($http) {

  this.getProfile = function () {
    return $http.get("api/auth");
  };

}])

The server is expected to return such a JSON object when requesting GET api/auth:

{
  "name": "John Doe", // plus any other user information
  "roles": ["ROLE_ADMIN", "ROLE_USER"], // or any other role (or no role at all, i.e. an empty array)
  "anonymous": false // or true
}

Finally, when Access rejects a promise, if using ui.router, the $stateChangeError event will be fired:

.run(["$rootScope", "Access", "$state", "$log", function ($rootScope, Access, $state, $log) {

  $rootScope.$on("$stateChangeError", function (event, toState, toParams, fromState, fromParams, error) {
    switch (error) {

    case Access.UNAUTHORIZED:
      $state.go("signIn");
      break;

    case Access.FORBIDDEN:
      $state.go("forbidden");
      break;

    default:
      $log.warn("$stateChangeError event catched");
      break;

    }
  });

}])

If using ngRoute, the $routeChangeError event will be fired:

.run(["$rootScope", "Access", "$location", "$log", function ($rootScope, Access, $location, $log) {

  $rootScope.$on("$routeChangeError", function (event, current, previous, rejection) {
    switch (rejection) {

    case Access.UNAUTHORIZED:
      $location.path("/signin");
      break;

    case Access.FORBIDDEN:
      $location.path("/forbidden");
      break;

    default:
      $log.warn("$stateChangeError event catched");
      break;

    }
  });

}])

The user profile can also be accessed in the controllers:

.state("home", {
  /* ... */
  controller: "HomeController",
  resolve: {
    userProfile: "UserProfile"
  }
})

UserProfile then contains the properties returned by the server when requesting GET api/auth:

.controller("HomeController", ["$scope", "userProfile", function ($scope, userProfile) {

  $scope.title = "Hello " + userProfile.name; // "Hello John Doe" in the example

}])

UserProfile needs to be refreshed when a user signs in or out, so that Access can handle the routes with the new user profile. You can either reload the whole page, or call UserProfile.$refresh(). Example when signing in:

.service("Auth", ["$http", function ($http) {

  /* ... */

  this.signIn = function (credentials) {
    return $http.post("api/auth", credentials).then(function (response) {
      // authentication succeeded, store the response access token somewhere (if any)
    });
  };

}])
.state("signIn", {
  /* ... */
  controller: "SignInController",
  resolve: {
    /* ... */
    userProfile: "UserProfile"
  }
})
.controller("SignInController", ["$scope", "$state", "Auth", "userProfile", function ($scope, $state, Auth, userProfile) {

  $scope.signIn = function () {
    Auth.signIn($scope.credentials).then(function () {
      // user successfully authenticated, refresh UserProfile
      return userProfile.$refresh();
    }).then(function () {
      // UserProfile is refreshed, redirect user somewhere
      $state.go("home");
    });
  };

}])
like image 37
sp00m Avatar answered Oct 04 '22 19:10

sp00m


The most straightforward manner of defining custom behavior for individual routes would be pretty easy:

1) routes.js: create a new property (like requireAuth) for any desired route

angular.module('yourApp').config(function($routeProvider) {
    $routeProvider
        .when('/home', {
            templateUrl: 'templates/home.html',
            requireAuth: true // our custom property
        })
        .when('/login', {
            templateUrl: 'templates/login.html',
        })
        .otherwise({
            redirectTo: '/home'
        });
})

2) In a top-tier controller that isn't bound to an element inside the ng-view (to avoid conflict with angular $routeProvider ), check if the newUrl has the requireAuth property and act accordingly

 angular.module('YourApp').controller('YourController', function ($scope, $location, session) {
     
     // intercept the route change event
     $scope.$on('$routeChangeStart', function (angularEvent, newUrl) {
         
         // check if the custom property exist
         if (newUrl.requireAuth && !session.user) {
             
             // user isn’t authenticated
             $location.path("/login");
         }
     });
 });
like image 41
Shaya Avatar answered Oct 04 '22 18:10

Shaya


I wrote a post a few months back on how to set up user registration and login functionality with Angular, you can check it out at http://jasonwatmore.com/post/2015/03/10/AngularJS-User-Registration-and-Login-Example.aspx

I check if the user is logged in the $locationChangeStart event, here is my main app.js showing this:

(function () {
    'use strict';
 
    angular
        .module('app', ['ngRoute', 'ngCookies'])
        .config(config)
        .run(run);
 
    config.$inject = ['$routeProvider', '$locationProvider'];
    function config($routeProvider, $locationProvider) {
        $routeProvider
            .when('/', {
                controller: 'HomeController',
                templateUrl: 'home/home.view.html',
                controllerAs: 'vm'
            })
 
            .when('/login', {
                controller: 'LoginController',
                templateUrl: 'login/login.view.html',
                controllerAs: 'vm'
            })
 
            .when('/register', {
                controller: 'RegisterController',
                templateUrl: 'register/register.view.html',
                controllerAs: 'vm'
            })
 
            .otherwise({ redirectTo: '/login' });
    }
 
    run.$inject = ['$rootScope', '$location', '$cookieStore', '$http'];
    function run($rootScope, $location, $cookieStore, $http) {
        // keep user logged in after page refresh
        $rootScope.globals = $cookieStore.get('globals') || {};
        if ($rootScope.globals.currentUser) {
            $http.defaults.headers.common['Authorization'] = 'Basic ' + $rootScope.globals.currentUser.authdata; // jshint ignore:line
        }
 
        $rootScope.$on('$locationChangeStart', function (event, next, current) {
            // redirect to login page if not logged in and trying to access a restricted page
            var restrictedPage = $.inArray($location.path(), ['/login', '/register']) === -1;
            var loggedIn = $rootScope.globals.currentUser;
            if (restrictedPage && !loggedIn) {
                $location.path('/login');
            }
        });
    }
 
})();
like image 25
Jason Watmore Avatar answered Oct 04 '22 19:10

Jason Watmore


I feel like this way is easiest, but perhaps it's just personal preference.

When you specify your login route (and any other anonymous routes; ex: /register, /logout, /refreshToken, etc.), add:

allowAnonymous: true

So, something like this:

$stateProvider.state('login', {
    url: '/login',
    allowAnonymous: true, //if you move this, don't forget to update
                          //variable path in the force-page check.
    views: {
        root: {
            templateUrl: "app/auth/login/login.html",
            controller: 'LoginCtrl'
        }
    }
    //Any other config
}

You don't ever need to specify "allowAnonymous: false", if not present, it is assumed false, in the check. In an app where most URLs are force authenticated, this is less work. And safer; if you forget to add it to a new URL, the worst that can happen is an anonymous URL is protected. If you do it the other way, specifying "requireAuthentication: true", and you forget to add it to a URL, you are leaking a sensitive page to the public.

Then run this wherever you feel fits your code design best.

//I put it right after the main app module config. I.e. This thing:
angular.module('app', [ /* your dependencies*/ ])
       .config(function (/* you injections */) { /* your config */ })

//Make sure there's no ';' ending the previous line. We're chaining. (or just use a variable)
//
//Then force the logon page
.run(function ($rootScope, $state, $location, User /* My custom session obj */) {
    $rootScope.$on('$stateChangeStart', function(event, newState) {
        if (!User.authenticated && newState.allowAnonymous != true) {
            //Don't use: $state.go('login');
            //Apparently you can't set the $state while in a $state event.
            //It doesn't work properly. So we use the other way.
            $location.path("/login");
        }
    });
});
like image 20
Yurelle Avatar answered Oct 04 '22 18:10

Yurelle


app.js

'use strict';
// Declare app level module which depends on filters, and services
var app= angular.module('myApp', ['ngRoute','angularUtils.directives.dirPagination','ngLoadingSpinner']);
app.config(['$routeProvider', function($routeProvider) {
  $routeProvider.when('/login', {templateUrl: 'partials/login.html', controller: 'loginCtrl'});
  $routeProvider.when('/home', {templateUrl: 'partials/home.html', controller: 'homeCtrl'});
  $routeProvider.when('/salesnew', {templateUrl: 'partials/salesnew.html', controller: 'salesnewCtrl'});
  $routeProvider.when('/salesview', {templateUrl: 'partials/salesview.html', controller: 'salesviewCtrl'});
  $routeProvider.when('/users', {templateUrl: 'partials/users.html', controller: 'usersCtrl'});
    $routeProvider.when('/forgot', {templateUrl: 'partials/forgot.html', controller: 'forgotCtrl'});


  $routeProvider.otherwise({redirectTo: '/login'});


}]);


app.run(function($rootScope, $location, loginService){
    var routespermission=['/home'];  //route that require login
    var salesnew=['/salesnew'];
    var salesview=['/salesview'];
    var users=['/users'];
    $rootScope.$on('$routeChangeStart', function(){
        if( routespermission.indexOf($location.path()) !=-1
        || salesview.indexOf($location.path()) !=-1
        || salesnew.indexOf($location.path()) !=-1
        || users.indexOf($location.path()) !=-1)
        {
            var connected=loginService.islogged();
            connected.then(function(msg){
                if(!msg.data)
                {
                    $location.path('/login');
                }

            });
        }
    });
});

loginServices.js

'use strict';
app.factory('loginService',function($http, $location, sessionService){
    return{
        login:function(data,scope){
            var $promise=$http.post('data/user.php',data); //send data to user.php
            $promise.then(function(msg){
                var uid=msg.data;
                if(uid){
                    scope.msgtxt='Correct information';
                    sessionService.set('uid',uid);
                    $location.path('/home');
                }          
                else  {
                    scope.msgtxt='incorrect information';
                    $location.path('/login');
                }                  
            });
        },
        logout:function(){
            sessionService.destroy('uid');
            $location.path('/login');
        },
        islogged:function(){
            var $checkSessionServer=$http.post('data/check_session.php');
            return $checkSessionServer;
            /*
            if(sessionService.get('user')) return true;
            else return false;
            */
        }
    }

});

sessionServices.js

'use strict';

app.factory('sessionService', ['$http', function($http){
    return{
        set:function(key,value){
            return sessionStorage.setItem(key,value);
        },
        get:function(key){
            return sessionStorage.getItem(key);
        },
        destroy:function(key){
            $http.post('data/destroy_session.php');
            return sessionStorage.removeItem(key);
        }
    };
}])

loginCtrl.js

'use strict';

app.controller('loginCtrl', ['$scope','loginService', function ($scope,loginService) {
    $scope.msgtxt='';
    $scope.login=function(data){
        loginService.login(data,$scope); //call login service
    };

}]);
like image 30
Jaydeep Gondaliya Avatar answered Oct 04 '22 20:10

Jaydeep Gondaliya


You can use resolve:

angular.module('app',[])
.config(function($routeProvider)
{
    $routeProvider
    .when('/', {
        templateUrl  : 'app/views/login.html',
        controller   : 'YourController',
        controllerAs : 'Your',
        resolve: {
            factory : checkLoginRedirect
        }
    })
}

And, the function of the resolve:

function checkLoginRedirect($location){

    var user = firebase.auth().currentUser;

    if (user) {
        // User is signed in.
        if ($location.path() == "/"){
            $location.path('dash'); 
        }

        return true;
    }else{
        // No user is signed in.
        $location.path('/');
        return false;
    }   
}

Firebase also has a method that helps you install an observer, I advise installing it inside a .run:

.run(function(){

    firebase.auth().onAuthStateChanged(function(user) {
        if (user) {
            console.log('User is signed in.');
        } else {
            console.log('No user is signed in.');
        }
    });
  }
like image 34
Diogo Machado Avatar answered Oct 04 '22 20:10

Diogo Machado