Learn how to make authentication in your Angular applications simpler and more consistent

Learn how to make authentication in your Angular applications simpler and more consistent
Photo by Parsoa Khorsand / Unsplash

Auth0! Parse.com! Firebase!

They are just a few of the so many cool services and libraries that are available today to easily authenticate users in your Angular application.

Unfortunately such a rich set of options also come with an equally diverse set of API's:

<!-- auth0 -->
<h1>Welcome {{ profile.first_name }}</h1>

<!-- parse.com -->
<h1>Welcome {{ currentUser.get('username') }}</h1>

<!-- firebase -->
<h1>Welcome user {{ authData.uid }}</h1>

<!-- angular-client-side-auth -->
<h1>Welcome {{ user.username }}</h1>

What if your project suddenly requires a different authentication strategy just before being released? Surely you don't want to change all your scripts and view templates the day before a release?

And what about maintenance? How can you conveniently support and maintain multiple projects if you have all kinds of API's to support?

In this article I share some of the best practices and tactics I came up with over the years to make authentication in Angular applications simpler and more consistent.

By the end of the article you will know exactly how you can create your own meaningful API to consistently:

  • access session details in a view
  • control access in your entire application
  • show/hide content when a user is not logged in
  • restrict route or state access for non authorized users
  • pass credentials to a RESTful service when using ngResource

in different applications, even if they use different authentication libraries.

So let's get started!

The goal

Before we dive into the code, let's think of what we are actually trying to accomplish:

  • On one hand we want to be able to deal with authentication such as logging in and out.
  • On the other hand we want to be able to store session related data such as the user's first name, last name, or any information you receive from the authentication service you are dealing with.

Both are related but have different concerns.

The former is concerned with the actual logic while the latter is concerned with storing and persisting data so we can refresh the page and things keep working.

Our goal is to create a consistent API for both authentication and session so we can easily recognize intentions in scripts and views without knowing the exact underlying authentication logic.

A demo backend authentication service

For the sake of demonstration, let us assume that we have a backend authentication service that exposes the following endpoints:

POST /login

Log in user. Upon successful login, it returns:

HTTP 200

{
  "user": {
    "name": "John Doe"
    "email": "john@doe.com"
  },
  "accessToken": "some-access-token"
}

GET /logout

Log out user. Upon successful login, it returns:

HTTP 200

The Angular side

To make sure we can conveniently access both the authentication logic and the session data in our entire application, we will create 2 separate Angular services.

Using services has multiple benefits:

  • you can easily inject services anywhere in your application
  • you can easily unit test services
  • services are singletons so there is only one instance of them, which is perfect for what we need here

I like to name them auth and session to keep their names small and meaningful but you can name them anything you like.

The auth service

The auth service lets us abstract the actual authentication logic of the authentication library we need to support in the project.

Here is an example of what an auth service can look like for our demo backend authentication service (don't worry about session yet, we'll get there shortly):

(function (angular) {

  function AuthService($http, session){

    /**
    * Check whether the user is logged in
    * @returns boolean
    */
    this.isLoggedIn = function isLoggedIn(){
      return session.getUser() !== null;
    };

    /**
    * Log in
    *
    * @param credentials
    * @returns {*|Promise}
    */
    this.logIn = function(credentials){
      return $http
        .post('/api/login', credentials)
        .then(function(response){
          var data = response.data;
          session.setUser(data.user);
          session.setAccessToken(data.accessToken);
        });
    };

    /**
    * Log out
    *
    * @returns {*|Promise}
    */
    this.logOut = function(){
      return $http
        .get('/api/logout')
        .then(function(response){
        
          // Destroy session in the browser
          session.destroy();      
        });
      
    };

  }

  // Inject dependencies
  AuthService.$inject = ['$http', 'session'];

  // Export
  angular
    .module('app')
    .service('auth', AuthService);

})(angular);

In this case we are using $http to call our backend service but that's not relevant.

The thing to remember here is that you create your own API:

  • auth.logIn()
  • auth.logOut()
  • auth.isLoggedIn()

that hides away the actual implementation logic.

If you decide to use another authentication library, you only have to rewrite the logic in these functions.

Here is an example when using Firebase authentication:

(function (angular) {

  function AuthService($q, $firebaseAuth, session){

    var ref = new Firebase('https://<your-url>.firebaseio.com/');
    var authObj = $firebaseAuth(ref);

    this.isLoggedIn = function isLoggedIn(){
      return session.getAuthData() !== null;
    };

    this.logIn = function(){
      return authObj
        .$authWithOAuthPopup('github', {
          scope: 'user'
        })
        .then(
          function(authData){
            session.setAuthData(authData);
            return authData;
          },
          function(error){
            $q.reject(error);
          }
        );
    };

    this.logOut = function(){
      authObj.unauth();
      session.destroy();
    };

  }

  // Inject dependencies
  AuthService.$inject = ['$q', '$firebaseAuth', 'session'];

  // Export
  angular.module('app')
    .service('auth', AuthService);

})(angular);

It is perfectly fine to create additional methods depending on the authentication service you are dealing with.

For example, you could be dealing with roles and add a method auth.isAdmin() that checks if a certain role is stored in the session.

This is not a form of inconsistency between applications. The main goal for consistency here is that you always use auth to implement logic.

Note that both examples expose a similar API, while using completely different logic under the hood.

The session service

The session service takes care of storing data that we receive from the authentication service after successfully logging in.

Here we provide a simple API to store the user and accessToken we receive from the authentication service.

I am a fan of creating getters and setters. If you use an IDE like webstorm it will provide you with autocompletion in your application. A second benefit is that you can create extra methods like session.getFirstName(), session.getFullName() or whatever you feel you need in your application.

For our demo backend authentication service, we can use localStorage to persist the session during page reloads:

(function (angular) {

  function sessionService($log, localStorage){

    // Instantiate data when service
    // is loaded
    this._user = JSON.parse(localStorage.getItem('session.user'));
    this._accessToken = JSON.parse(localStorage.getItem('session.accessToken'));

    this.getUser = function(){
      return this._user;
    };

    this.setUser = function(user){
      this._user = user;
      localStorage.setItem('session.user', JSON.stringify(user));
      return this;
    };

    this.getAccessToken = function(){
      return this._accessToken;
    };

    this.setAccessToken = function(token){
      this._accessToken = token;
      localStorage.setItem('session.accessToken', token);
      return this;
    };

    /**
     * Destroy session
     */
    this.destroy = function destroy(){
      this.setUser(null);
      this.setAccessToken(null);
    };

  }

  // Inject dependencies
  sessionService.$inject = ['$log', 'localStorage'];

  // Export
  angular
    .module('app')
    .service('session', sessionService);

})(angular);

As you can see, the getters merely return the requested data while the setters actually store the data in localStorage as well to make sure the data is persisted in case the browser window is closed.

The localStorage service is a simple wrapper that provides access to the browser's native localStorage. It looks like this:

(function (angular) {

  function localStorageServiceFactory($window){
    if($window.localStorage){
      return $window.localStorage;
    }
    throw new Error('Local storage support is needed');
  }

  // Inject dependencies
  localStorageServiceFactory.$inject = ['$window'];

  // Export
  angular
    .module('app')
    .factory('localStorage', localStorageServiceFactory);

})(angular);

Depending on the data you receive from your authentication service, you may have different getters and setters in your session. That's okay and it's not a form of inconsistency across applications. The main goal for consistency here is that you always use the session to access session specific data.

Here is an example when using Firebase authentication:

(function (angular) {

  function sessionService($log, localStorage){

    this._authData = JSON.parse(localStorage.getItem('session.authData'));

    this.getAuthData = function(){
      return this._authData;
    };

    this.setAuthData = function(authData){
      this._authData = authData;
      localStorage.setItem('session.authData', JSON.stringify(authData));
      return this;
    };

    this.getGitHubAccessToken = function(){
      if(this._authData && this._authData.github && this._authData.github.accessToken){
        return this._authData.github.accessToken;
      }
      return null;
    };

    /**
     * Destroy session
     */
    this.destroy = function destroy(){
      this.setAuthData(null);
    };

  }

  // Inject dependencies
  sessionService.$inject = ['$log', 'localStorage'];

  // Export
  angular.module('app')
    .service('session', sessionService);

})(angular);

Assign both services to $rootScope

I always assign both services to $rootScope:

(function (angular) {

  function assignServicesToRootScope($rootScope, auth, session){
    $rootScope.auth = auth;
    $rootScope.session = session;
  }

  // Inject dependencies
  assignServicesToRootScope.$inject = ['$rootScope', 'auth', 'session'];

  // Export
  angular
    .module('app')
    .run(assignServicesToRootScope);

})(angular);

You may think: "Hey, it's not a good practice to assign data to $rootscope!".

You're right, but here we are not assigning data. Both auth and session are application wide services so it makes perfect sense to assign them to $rootScope in order to make them available in all our view templates.

It will make your life a whole lot easier and your code a whole lot simpler, as you'll see next.

Show/hide content when user is not logged in

Because we assigned both services to $rootScope, we can now easily hide/show content in all our application view templates:

<!-- hide title if logged in -->
<h1 class="ng-cloak" ng-hide="auth.isLoggedIn()">
  Hi stranger!
</h1>

<!-- show title if logged in -->
<h1 class="ng-cloak" ng-show="auth.isLoggedIn()">

  <!-- show session data -->
  Hi {{ session.getFullName() }}
</h1>

Notice how auth is intuitively used for logic (ng-if, ng-show, etc) while session is used for displaying data, which is exactly how we wanted things to behave when we defined our goal.

To reiterate: the key benefit is that you can now use your own custom API in your markup so it becomes consistent across all your applications, so next time you see something like:

<h1 ng-if="auth.isLoggedIn()">
  Access denied!
</h1>

in one of your application view templates, you'll immediately know what it will do without having to worry about the exact authentication logic that was implemented behind the scenes.

If you're working with designers that edit view templates, they will also appreciate a consistent API when working together with you on different projects. They may even be able to reuse parts of view templates across projects because the markup will be a lot more consistent.

Important: keep in mind that even though you hide content in the view, the content can still be viewed in the page source. So if certain pieces of content are too sensitive to show up in the page source, you will have to make sure your server doesn't send them in the first place.

Check access in scripts

Because Angular allows you to inject services anywhere in your application, detecting authentication state becomes dead simple:

(function (angular) {

  function SomeController(auth){
  
    // Do something if logged in
    if(auth.isLoggedIn()){
      // ...
    }
  }

  // Inject dependencies
  SomeController.$inject = ['auth'];

  // Export
  angular
    .module('app')
    .controller('SomeController', SomeController);

})(angular);

Control access during application bootstrap

Some applications depend on third party access tokens and only allow access to users that arrive with a valid token.

To prevent the entire application interface from loading, you can control access during the Angular bootstrap process using the run() method of your application module:

(function (angular) {

  function checkAccessDuringApplicationBootstrap($window, auth){

    if(auth.isLoggedIn()){
      return;
    }

    // Redirect to third party login page
    $window.location = '...';
    
    // Make sure bootstrap process is stopped
    throw new Error('Access denied');
  }

  // Inject dependencies
  checkAccessDuringApplicationBootstrap.$inject = ['$window', 'auth'];

  // Export
  angular
    .module('app')
    .run(checkAccessDuringApplicationBootstrap);

})(angular);

Control access during route or state changes

You may also want to control access whenever the route or state changes.

The event listener below contains code for both ngRoute and ui-router:

(function (angular) {

  function checkAccessOnStateChange($rootScope, auth){

    // Listen for location changes
    // This happens before route or state changes
    $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl){
      if(!auth.isLoggedIn()){
      
        // Redirect to login
        
        // Prevent location change
        event.preventDefault();
      }
    });

    // Listen for route changes when using ngRoute
    $rootScope.$on('$routeChangeStart', function(event, nextRoute, currentRoute){
    
      // Here we simply check if logged in but you can
      // implement more complex logic that inspects the
      // route to see if access is allowed or not
      if(!auth.isLoggedIn()){
      
        // Redirect to login
      
        // Prevent state change
        event.preventDefault();
      }
    });

    // Listen for state changes when using ui-router
    $rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
    
      // Here we simply check if logged in but you can
      // implement more complex logic that inspects the
      // state to see if access is allowed or not
      if(!auth.isLoggedIn()){
      
        // Redirect to login
      
        // Prevent state change
        event.preventDefault();
      }
    });
  }

  // Inject dependencies
  checkAccessOnStateChange.$inject = ['$rootScope', 'auth'];

  // Export
  angular
    .module('app')
    .run(checkAccessOnStateChange);

})(angular);

As noted in the comments, you can create your own custom access control logic where you inspect routes or states before allowing access or not.

Pass credentials to a RESTful service when using ngResource

If you are using Angular's ngResource module to communicate with a RESTful backend and you need to pass credentials, you can easily add session details to the HTTP requests that ngResource makes.

In this example we pass the accessToken in the headers in every HTTP request that ngResource makes:

(function (angular) {

  function BooksResourceFactory($resource, session){

    var url = '/books/:_id';

    // Pass session token as Authorization header
    var headers = {
      'Authorization': 'Bearer ' + session.getAccessToken()
    };

    // Assemble actions with custom headers attached
    var actions = {
      'get'   : {method: 'GET', headers: headers},
      'save'  : {method: 'POST', headers: headers},
      'create': {method: 'POST', headers: headers},
      'query' : {method: 'GET', isArray: true, headers: headers},
      'remove': {method: 'DELETE', headers: headers},
      'delete': {method: 'DELETE', headers: headers},
      'update': {method: 'PUT', headers: headers}
    };

    var Books = $resource(url, {_id: '@_id'}, actions);

    return Books;
  }

  BooksResourceFactory.$inject = ['$resource', 'session'];

  angular.module('app')
    .factory('BooksResource', BooksResourceFactory);

})(angular);

If your application has a header property value that can change at runtime, you can assign a function instead of a static value so the value is generated dynamically at runtime.

For example, to let AngularJS dynamically evaluate the Authorization header value every time a resource action is called, you would replace:

// Pass session token as Authorization header
// Value is evaluated only once when resource is instantiated
var headers = {
  'Authorization': 'Bearer ' + session.getAccessToken()
};

with:

// Pass session token as Authorization header
// and re-evaluate the value every time an action is called
var headers = {
  'Authorization': function(){
      return 'Bearer ' + session.getAccessToken();
  }
};

Conclusion

The code snippets in this article just scratch the surface of what you can do with auth and session.

By abstracting your:

  • authentication logic in an auth service
  • session data in a session service

you can create your own consistent API that you can use in all your applications.

An API that makes sense to you and your team and hides away the underlying authentication logic of the application.

I could have saved myself a lot of time, money and refactoring over the years if only some kind of blueprint was available at the time.

So hopefully this article can be for you what I needed back then.

Have a great one!