1

My friend and I are building an app - my friend is on the backend (Node.js) and I'm on the front.

He implemented sessions on his end and provided me with the URL I need to call to log in. For example, a POST request

http://ourapp.heroku.com/login

with which username and password are passed.

On my side, in the Angular app, I create a login page which calls an Angular service when Login is clicked. If this service receives a 200 from the server, it does:

$cookieStore.put(cookieNames.LOGGED_IN_COOKIE, true);
$state.go('home', {}, {reload: true});

The problem is that we're having weird issues with the app on the front end. For example logging in and out often don't work. Also, users are able to go to pages even after they log out. I figured out (at least I think) that I'm not properly storing the Cookie I receive from the server, I'm only storing my own.

This whole Angular thing is still weird to me, because in PHP or Python apps you get a page request from the client and verify if he's logged in before sending him the page he requested. In Angular it's different - the user has all of the pages already. So how do I limit what he can see without logging in and how to I properly keep track of the server's cookie?

1
  • you should look for route change events to see if user needs to be login for next page... as it seems you are using ui-router for routing so adding custom data to state defination like requiresLogin: true and check them in event block (stateChangeStart) Commented Mar 9, 2015 at 13:57

1 Answer 1

2

If you use ui-router, you can do something similar to this:

First introduce some kind of access-levels to your states

$stateProvider
        .state('admin', {
            url: "/admin",
            templateUrl: "/app/views/admin.html",
            controller: "AdminController",
            data: {
                accessLevel: 'admin'
            }
        })

then you have to check on state change, if your logged in user has the required access-level:

You can create an auth service which implements your logic to log your user in, as example you can use this service

angular.module('app')
   .factory("AuthService", ["$rootScope", "$http", "AuthSession", "AuthHttpBuffer", "AUTH_EVENTS", function ($rootScope, $http, AuthSession, AuthHttpBuffer, AUTH_EVENTS) {

        function loginFailed() {
            $rootScope.$broadcast("auth-change", AUTH_EVENTS.loginFailed);
        };

        AuthSession.load();

        $rootScope.$on('$stateChangeStart', function (event, nextState) {
            if (nextState.data && nextState.data.accessLevel && !service.isAuthorized(nextState.data.accessLevel)) {
                event.preventDefault();
                $rootScope.$broadcast('auth-change', AUTH_EVENTS.loginRequired, nextState.name);
            }
        });

        var service = {
            login: function (credentials) {
                return $http
                            .post('/api/account/login', credentials)
                            .success(function (data, status) {
                                if ((status < 200 || status >= 300) && data.length >= 1) {
                                    loginFailed();
                                    return;
                                }

                                AuthSession.create(data.AccessToken, data.User);
                                $rootScope.$broadcast("auth-change", AUTH_EVENTS.loginSuccess);
                                AuthHttpBuffer.retryAll();
                            }).error(function (data, status) {
                                loginFailed();
                            });
            },
            cancel: function () {
                AuthHttpBuffer.rejectAll();
            },
            logout: function () {
                AuthSession.destroy();
                $rootScope.$broadcast("auth-change", AUTH_EVENTS.logoutSuccess);
            },
            isAuthenticated: function () {
                return (AuthSession.token !== null);
            },
            isAuthorized: function (accessLevel) {
                if (!accessLevel) return true;

                return (this.isAuthenticated() && AuthSession.user.UserRoles.indexOf(accessLevel) !== -1);
            }

        }
        return service;
    }]);

and your AuthSession service:

angular.module('app')
      .factory("AuthSession", ["$rootScope", "$window", "AUTH_EVENTS", function ($rootScope, $window, AUTH_EVENTS) {

        var sessionService = {
            user: null,
            token: null,

            //load the stored session data
            load: function () {
                var user = ...yourdata... //TODO implement load user data;
                var token = ...yourdata... //implement load user data;

                if (!user || !token) return;

                if (!this.checkTokenExpiration(token)) return;

                this.user = user;
                this.token = token;

                $rootScope.$broadcast("auth-change", AUTH_EVENTS.loginSuccess);
            },

            //save the current data to the session storage
            save: function () {
                //TODO save your userdata/token etc.
            },

            //create the current user with the assosiated token
            create: function (token, user) {
                this.token = token;
                this.user = user;

                if (!angular.isArray(this.user.UserRoles))
                    this.user.UserRoles = [this.user.UserRoles];

                this.save();
            },

            //destroy an user with all assosiated data
            destroy: function () {
                this.token = null;
                this.user = null;

                //TODO clear your saved data here
            },

            //check if the supplied access token data is expired
            checkTokenExpiration: function (token) {
                if (token === undefined || token === null) return false;

                var retval = (new Date(token.TokenExpires).getTime() > new Date().getTime());

                if (retval === false) {
                    sessionService.destroy();
                    $rootScope.$broadcast("auth-change", AUTH_EVENTS.sessionTimeout);
                }

                return retval;
            }
        }

        return sessionService;

      }]);

and the constants:

angular.module('app')
      .constant('AUTH_EVENTS', {
        loginSuccess: 'auth-login-success',
        loginFailed: 'auth-login-failed',
        logoutSuccess: 'auth-logout-success',
        loginRequired: 'auth-login-required',
        sessionTimeout: 'auth-session-timeout',
        notAuthorized: 'auth-not-authorized'
      });

If you want be able to catch urls, where you haven't the right accesrights, you can send the request to a http buffer:

angular.module('app')
      .factory('AuthHttpBuffer', ["$injector", function ($injector) {
        /** Holds all the requests, so they can be re-requested in future. */
        var buffer = [];

        /** Service initialized later because of circular dependency problem. */
        var $http;

        function retryHttpRequest(config, deferred) {
            function successCallback(response) {
                deferred.resolve(response);
            }
            function errorCallback(response) {
                deferred.reject(response);
            }
            $http = $http || $injector.get('$http');
            $http(config).then(successCallback, errorCallback);
        }

        return {
            /**
                 * Appends HTTP request configuration object with deferred response attached to buffer.
                 */
            append: function (config, deferred) {
                buffer.push({
                    config: config,
                    deferred: deferred
                });
            },

            /**
                 * Abandon or reject (if reason provided) all the buffered requests.
                 */
            rejectAll: function (reason) {
                if (reason) {
                    for (var i = 0; i < buffer.length; ++i) {
                        buffer[i].deferred.reject(reason);
                    }
                }
                buffer = [];
            },

            /**
                 * Retries all the buffered requests clears the buffer.
                 */
            retryAll: function () {
                for (var i = 0; i < buffer.length; ++i) {
                    retryHttpRequest(buffer[i].config, buffer[i].deferred);
                }
                buffer = [];
            }
        };
      }]);

and if you haven't enough you can also add an interceptor, that triggers an auth change event, if the server response is unauthorized:

    angular.module('app')
      .factory('AuthInterceptor', ["$rootScope", "$q", "AuthSession", "AuthHttpBuffer", "AUTH_EVENTS", function ($rootScope, $q, AuthSession, AuthHttpBuffer, AUTH_EVENTS) {

        return {
            request: function (config) {
                config.headers = config.headers || {};
                if (AuthSession.token) {
                    config.headers.Authorization = 'Bearer ' + AuthSession.token.TokenKey;
                }
                return config;
            },
            responseError: function (rejection) {
                if (rejection.status === 401) {
                    var deferred = $q.defer();

                    AuthHttpBuffer.append(rejection.config, deferred);

                    if (AuthSession.token) {
                        $rootScope.$broadcast('auth-change', AUTH_EVENTS.notAuthorized);
                    } else {
                        $rootScope.$broadcast('auth-change', AUTH_EVENTS.loginRequired);
                    }
                    return deferred.promise;
                }
                return $q.reject(rejection);
            }
        }
      }]);

this interceptor also adds a session token to all requests if available.

to use this interceptor, you have to add the following two lines to your app.config():

    $httpProvider.defaults.withCredentials = true;
    $httpProvider.interceptors.push("AuthInterceptor");
Sign up to request clarification or add additional context in comments.

2 Comments

wow cool detailed answer! Thanks I will implement with my next project. Hope it works
Thanks. Since I'm using this in my project, it should work. (Maybe I left away too much, because I implemented some additional functionality in my project). Please keep in mind, that you should also secure your backend with the same/similar accessrights.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.