Rolling your own simple token-based authentication in AngularJS with Rails

PUBLISHED ON AUG 1, 2014 — ANGULARJS, AUTHENTICATION, RAILS

Recently I was interested in implementing token-based authentication with a decoupled frontend and backend. I googled about ways to do it the Angular-Rails way. Turns out it is not that simple and straightfoward. After a couple of days of reading fragmented sources of information on the web, I finally rolled my own authentication system.

Granted that one could easily use Devise gem for their authentication needs, however there’s too much cruft associated with it and I want to implement authentication from the bottom-up to solidify my understanding.

Given that the resources are scattered out there, I thought I’d write a tutorial to synthesize them into a coherent whole for the benefit of the reader. A large chunk of my tutorial (the Rails backend) is adapted from Michael Hartl’s famous RoR Tutorial, with the rest culled from various resources like StackOverflow and other blogs.

This tutorial is for those who want to understand how token-based authentication works. We’ll be rolling a simple solution that authenticates users based on their email and password. Log-ins, sign-outs and simple authorization are included as well. I stress that the auth system in this tutorial is definitely not industrial-grade but it won’t be prone to easy hack-ins either.

You may want to read this article for a rundown of the evolution of API authentication and this article to understand how token-based auth compares with the traditional cookie-based auth.

We’ll be using token-based auth in this tutorial because it is more conventional and confers more advantages compared to cookie-based auth.

Even though this tutorial is mainly for developers using Angular and Rails, the process of token-based authentication is generic enough such that a framework-agnostic flow can be gleaned from it:

  1. Client sends the user provided login credentials to the server.
  2. Server authenticates credentials and responds with a generated token.
  3. Client stores the token somewhere (local storage, cookies, or just in memory).
  4. Client sends the token as an authorization header on every request to the server.
  5. Server verifies the token and acts accordingly with either sending the requested > resource, or an 401 (or something alike).

Without further ado, let’s get started.

Rails API setup

We will be starting a Rails API server from scratch. Run rails new sample-api in terminal.

We will be using bcrypt gem as the backbone of our auth machinery. Hence we have to uncomment bcrypt-ruby gem in Gemfile and then do bundle install.

gem 'bcrypt-ruby', '~> 3.1.2'

Next, we create our User model. It will have these attributes: name, email, password digest and remember token. We will not be storing passwords in plaintext as hackers can easily retrieve user login credentials through the database. Create the User model by running this command:

rails g model User name:string email:string password_digest:string remember_token:string

In the database migration file auto-generated by the last command, add in a remember token index for easy retrieval since we will be accessing users by their token often.

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name
      t.string :email
      t.string :password_digest
      t.string :remember_token

      t.timestamps
    end
    add_index :users, :remember_token
  end
end

Run rake db:migrate to update the database schema. Your schema should now look like this:

ActiveRecord::Schema.define(version: 20140801074722) do
  create_table "users", force: true do |t|
    t.string   "name"
    t.string   "email"
    t.string   "password_digest"
    t.string   "remember_token"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "users", ["remember_token"], name: "index_users_on_remember_token"
end

Next, we modify our User model class in models/user.rb to utilise the bcrypt gem by adding has_secure_password. We will also use some boilerplate from Michael Hartl’s Rails Tutorial (thanks Michael) to validate user’s credentials. Class-level methods used to generate and digest tokens are added as well. Finally, we define a private instance-level method to assign the newly generated token to the remember_token field of User model.

class User < ActiveRecord::Base
  has_secure_password # using bcrypt

  # boilerplate from Hartl's tutorial to validate user input
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence:   true,
                    format:     { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }

  validates :password, length: { minimum: 6 }
  before_create :create_remember_token

  # class-level methods to generate and digest tokens
  def User.new_remember_token
    SecureRandom.urlsafe_base64
  end

  def User.digest(token)
    Digest::SHA1.hexdigest(token.to_s)
  end

  # private instance-level method to assign token
  private
    def create_remember_token
      self.remember_token = User.digest(User.new_remember_token)
    end
end

Next, we move on to the controllers. Before creating the users controller, we have to configure the application controller. Since we are decoupling the frontend and the backend, each of them lives in a separate domain. Hence the frontend app have to make cross-domain requests to the Rails backend. Such requests will not be acknowledged unless the server is configured to implement cross-origin resource sharing (CORS).

As the application controller is the main controller, changes made to it affect the rest of the controllers which inherit from it. Hence it makes sense to implement CORS in application controller.

Since we are not sending the auth token on the client side through the custom X-CSRF-Token header, we have to change protect_from_forgery to use :null_session along with a guard to handle CSRF token verification issue. We included respond_to :json as the Rails API will be mainly serving JSON responses.

We have also included SessionsHelper which contains helper methods (to be implemented later) for sign-ins and sign-outs. Finally, there is a callback method used to restrict access to unauthorized users.

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }
  include SessionsHelper # include helper methods used for sign in and sign out
  respond_to :json

  before_filter :set_cors_headers
  before_filter :cors_preflight

  def set_cors_headers
    headers['Access-Control-Allow-Origin'] = '*'
    headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
    headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
    headers['Access-Control-Max-Age'] = '3628800'
  end

  # before every actual request, the browser sends a OPTIONS request 
  # to check if the server implements CORS
  # for such requests, send an empty response with 200 status
  def cors_preflight
    head :ok if request.method == 'OPTIONS'
  end

  # callback method used for authorization 
  private
    def restrict_access
      if request.headers['HTTP_AUTHORIZATION']
        token = request.headers['HTTP_AUTHORIZATION'].split(" ").second
        user = User.find_by(remember_token: User.digest(token))
      end
      head :unauthorized unless user # send 401 response unless user is authorized
    end
end

Sign-up, sign-in and sign-out

To implement sign-up, we have to create a Users controller first as this is where we define the creation of new users. Run rails g controller Users in terminal.

Define the create method in controllers/users_controller.rb as shown below:

class UsersController < ApplicationController
  def create
    user = User.new(user_params)
    if user.save
      token = sign_in user
      render status: 200,
        json: {
          success: true,
          info: "Signed up successfully",
          data: {
            auth_token: token
          }
        }
    else
      render status: 200,
        json: {
          success: false,
          info: "Signed up failed, user exists",
          data: {}
        }
    end
  end

  private
    def user_params
      params.require(:user).permit!
    end
end

The alert reader can see that sign_in method is not defined in this class; it is defined in SessionsHelper module instead. Since we want the user to be automatically signed in after registration, we have to proceed with the implementation of sign-in process, and followed by sign-out. This is done with sessions. A session is considered as a resource in the server, hence we have to create a controller for it.

Run rails g controller Sessions to create the SessionsController. As a side-effect, SessionsHelper is automatically created too.

Let’s define the create and destroy methods in controllers/sessions_controller.rb:

class SessionsController < ApplicationController
  def create
    user = User.find_by(email: params[:user][:email].downcase)
    if user && user.authenticate(params[:user][:password])
      #sign_in helper method from SessionsHelper
      token = sign_in user
      render status: 200,
        json: {
          success: true,
          info: "Logged in successfully",
          data: {
            auth_token: token
          }
        }
    else
      render status: 200,
      json: {
        success: false,
        info: "Log in failed",
        data: {}
      }
    end
  end

  def destroy
    if request.headers['HTTP_AUTHORIZATION']
      token = request.headers['HTTP_AUTHORIZATION'].split(" ").second
      user = User.find_by(remember_token: User.digest(token))
    end
    if user
      #sign_out helper method from SessionsHelper
      sign_out user
      render status: 200,
        json: {
          success: true,
          info: "Signed out successfully",
          data: {}
        }
    else
      render status: 200,
      json: {
        success: false,
        info: "Signed out failed, no such user",
        data: {}
      }
    end
  end
end

Looking at the above code, there’s nothing out of the ordinary except for this snippet user.authenticate(params[:user][:password]). The authenticate() method is part of the bcrypt gem, and is used to digest the given password to compare it with the stored password digest in the users database, returning a boolean value.

We also observe that we invoke the helper methods from the SessionsHelper module in helpers/sessions_helper.rb. Without further ado, let’s flesh the module out.

module SessionsHelper
  def sign_in(user)
    remember_token = User.new_remember_token
    user.update_attribute(:remember_token, User.digest(remember_token))
    remember_token
  end

  def sign_out(user)
    user.update_attribute(:remember_token, User.digest(User.new_remember_token))
  end
end

Simple authorization

Let’s define a restricted API call to illustrate the authorization concept. We make it such that only logged-in users can access the index of all users from the API.

We return to controllers/users_controller.rb to effect this change by defining the index method and using a before_action callback hook to restrict access to it.

class UsersController < ApplicationController
  before_action :restrict_access, only: :index
  def index
    respond_with User.all
  end

  # rest of the code here

The restrict_access method is defined in ApplicationController and UsersController is a subclass of it. That’s why we are able to invoke restrict_access method here.

The index method will respond with a JSON containing all users, this is made possible by the respond_to :json method in ApplicationController.

Routings

This is the final part of our Rails API. We have to tie up the loose ends by connecting the methods in our controllers to the API endpoints – the URLs.

Rails.application.routes.draw do
  resources :users, defaults: {format: 'json'}
  match '/users', to: 'users#set_cors_headers', via: :options
  resources :sessions
  match '/signin',    to: 'sessions#create',    via: [:post, :options]
  match '/signout',   to: 'sessions#destroy',   via: [:delete, :options]
  match '/signup',    to: 'users#create',       via: [:post, :options]
end

We have to take care of CORs preflight by specifying that the API endpoints can respond to OPTIONS request.

Notice that even though we defined every possible RESTful endpoints for /users using resources :users, defaults: {format: 'json'}, we still have to handle the OPTION request for /users by directing it to set_cors_headers method defined in ApplicationController.

To be honest, I’m not sure whether my approach is considered best practice or not. Either way, it doesn’t look elegant. If you have any better solutions, do let me know!

This concludes the set-up for Rails API.

Setting up the Angular frontend

We will be creating a sample app using Angular framework. I’ll assume that the reader has already scaffolded the app with Yeoman.

If you haven’t already, please follow this codelab until Step 5.

We will need to configure our Angular app by adding a dependency to store auth tokens on the client side.

First, we run bower install --save angular-local-storage in terminal to install the local storage module.

We then have to edit the application module in scripts/app.js to include the local storage module:

var app = angular
  .module('sampleAppApp', [
    'ngAnimate',
    'ngCookies',
    'ngResource',
    'ngRoute',
    'ngSanitize',
    'ngTouch',
    'LocalStorageModule'
  ]);
  app.config(['localStorageServiceProvider', function(localStorageServiceProvider){
    localStorageServiceProvider.setPrefix('ls');
  }]);

Just a caveat: Yeoman named my app as sampleAppApp because I ran yo angular in a folder named sample-app. Please replace sampleAppApp with the name that Yeoman generated for you.

Notice that we assigned the application module to a variable app. This is used to easily refer to the application module later on.

We also have to configure the local storage module by setting its prefix to avoid namespace conflicts.

Moving on, we lay out the routings in the application module:

 app.config(function ($routeProvider) {
  $routeProvider
    .when('/', {
      templateUrl: 'views/main.html',
      controller: 'MainCtrl'
    })
    .when('/about', {
      templateUrl: 'views/about.html',
      controller: 'AboutCtrl'
    })
    .when('/login', {
      templateUrl: 'views/login.html',
      controller: 'LoginCtrl'
    })
    .when('/signup', {
      templateUrl: 'views/signup.html',
      controller: 'SignupCtrl'
    })
    .when('/signout', {
      templateUrl: 'views/signout.html',
      controller: 'SignoutCtrl'
    })
    .when('/restricted', {
      templateUrl: 'views/users.html',
      controller: 'UsersCtrl'
    })
    .otherwise({
      redirectTo: '/'
    });
  })

In addition to the two default routes created by Yeoman, we now have four routes, with one route being restricted only to authorized users.

Having defined the routings, we proceed to flesh out the rest of the app by creating the controllers and the views.

We start with the sign-up controller by creating a new file in scripts/controllers/signup.js:

app.controller('SignupCtrl', function($location, $scope, $http, localStorageService) {
  $scope.user = {};
  if (localStorageService.get('token')) {
      $scope.info = "Logged in" ;
  }
  $scope.signup = function() {
      console.log($scope.user);
      $http({
          url: 'http://localhost:3000/signup',
          method: 'POST',
          data: {
              user: $scope.user
          }
      }).success(function(response){
          $scope.info = response.info;
          localStorageService.set('token', response.data.auth_token)
      });
  };
});

In this controller, we make a POST request containing user credentials to the Rails API (port number 3000). If successful, the API will return an auth token which will be stored in the local storage. We also display the relevant information in the API response so that we know whether the sign-up is successful or not.

Now we need to create a sign-up form to carry out form-based authentication. Let’s create a new file in views/signup.html and add the HTML as follows:

<form ng-submit="signup()" class="well">
  <h3>Sign up</h3>
  <input ng-model="user.name" type="text" placeholder="Name">
  <input ng-model="user.email" type="text" placeholder="Email">
  <input ng-model="user.password" type="password" placeholder="Password">
  <input ng-model="user.password_confirmation" type="password" placeholder="Confirm password">
  <button type="submit" class="btn">Sign up</button>
</form>

<div>
  { { info } }
  <!-- please remove spacing between curly brackets! -->
</div>

Our user credentials consist of four parameters: name, email, password and password confirmation. The last parameter may seem optional, but it is required by bcrypt to generate a password digest.

We have also included a string interpolation enclosed in a div block to display result of the log-in process. As Markdown is unable to escape double curly brackets, I deliberately added a space between them. You have to remove the spacing to make Angular’s string interpolation work.

At this point you may wish to test out the app and API by running grunt serve and rails s respectively. But don’t forget to include the signup.js file in the index.html.

If there’s no problem, perfect! Moving on, we will create the log-in controller (scripts/controllers/login.js):

app.controller('LoginCtrl', function($location, $scope, $http, localStorageService) {
  if (localStorageService.get('token')) {
    $scope.info = "Logged in";
  }
  $scope.login = function() {
    $http({
      url: 'http://localhost:3000/signin',
      method: 'POST',
      data: {
        user: $scope.user
      }
    }).success(function(response){
      $scope.info = response.info;
      localStorageService.set('token', response.data.auth_token)
    });
  };
});

The log-in controller is more or less the same as sign-up controller. The difference lies in the parameters passed to API. Log-in process is simpler, requiring only two parameters: email and password. The email is used to find the user, and the password is used for authentication. Let’s flesh out the form used to input the log-in parameters in views/login.html:

<form ng-submit="login()" class="well form-inline">
  <h3>Login</h3>
  <input ng-model="user.email" type="text" placeholder="Email">
  <input ng-model="user.password" type="password" placeholder="Password">
  <button type="submit" class="btn">Log in</button>
  <button ng-click="signup()" class="btn">Sign up</button>
</form>

<div>
  { { info } }
  <!-- please remove spacing between curly brackets! -->
</div>

Finally, we complete the authentication cycle by fleshing out the sign-out controller in scripts/controllers/signout.js:

app.controller('SignoutCtrl', function($location, $scope, $http, localStorageService) {
  $scope.signout = function() {
    $http({
      url: 'http://localhost:3000/signout',
      method: 'DELETE',
      data: {}
    }).success(function(response){
      $scope.info = response.info;
      localStorageService.remove('token')
    });
  };
  $scope.signout();
});

Here, we make a DELETE request to the API, if successful, we clear the token in local storage and return the relevant information to be displayed.

The sign-out view (views/signout.html) is a just a HTML file to display the result:

<div>
  { { info } }
  <!-- please remove spacing between curly brackets! -->
</div>

HTTP interceptors and authorization

The alert reader may notice that we didn’t pass anything to the body of the DELETE request. So how do the API know which user to delete the session for?

Even though the authentication cycle is compete, we still have to construct the underlying pipes of our Angular app to make it work.

The idea is that the HTTP request headers should contain the token every time we make a request. How do we do that?

Turns out Angular has a built-in service to modify the HTTP request message before it is being sent to the server. We shall use a subset of the $http service – HTTP interceptors – for this purpose.

Let’s create a file named auth-interceptor.js in scripts/services folder (create the folder if you haven’t) and add the code as shown below:

app.factory('AuthInterceptor', function(localStorageService, $q, $location) {
  return {
      request: function(config) {
          config.headers = config.headers || {};
          if (localStorageService.get('token')) {
              config.headers.Authorization = 'Bearer ' + localStorageService.get('token');
          }
          return config || $q.when(config);
      },
      response: function(response) {
          return response || $q.when(response);
      },
      responseError: function(rejection) {
          if (rejection.status == 401) {
              $location.path("/login");
          }
          return $q.reject(rejection);
      }
  };
});

app.config(function ($httpProvider) {
  $httpProvider.interceptors.push('AuthInterceptor');
});

The idea is simple. For every HTTP request, if the token exists in local storage, we set the token in the HTTP Authorization header. Otherwise the header would not be set and a 401 response will be expected when we tried to access a restricted page.

We also deal with intercepting responses here. For every responses with status 401 (meaning unauthorized), we direct the user to the login page.

All in all, auth-interceptor.js essentially takes care of the authorization process for you.

Having done the necessary plumbing to make authentication work. We will create a restricted view that only logged-in users can access. The restricted view will contain a raw JSON response of all the user data.

First, we create a controller to fetch the user data from the API in scripts/controllers/users.js:

app.controller('UsersCtrl', function($scope, $http) {
  $http.get('http://localhost:3000/users').success(function(data) {
    $scope.users = data;
  });
});

We are calling the restricted API endpoint here. If the user is authorized, display the data, otherwise the user will be redirected to the log-in page by auth-interceptor.js.

To finally create the restricted view (views/users.html) is very simple. Just add the string interpolations as shown below:

<div>
  { { users } }
  <!-- please remove spacing between curly brackets! -->
</div>

Conclusion

You may notice that we didn’t create any links for our routes; we have to type in the URL path manually in the address bar every time we want to test the auth. Implementing the links is left as an exercise for the diligent reader.

There is one more thing though. We have to tell Angular where to load up our controllers and services by adding those dependencies in index.html:

    <!-- build:js({.tmp,app}) scripts/scripts.js -->
    <script src="scripts/app.js"></script>
    <script src="scripts/controllers/main.js"></script>
    <script src="scripts/controllers/about.js"></script>
    <script src="scripts/controllers/login.js"></script>
    <script src="scripts/controllers/signup.js"></script>
    <script src="scripts/controllers/signout.js"></script>
    <script src="scripts/controllers/users.js"></script>
    <script src="scripts/services/auth-interceptor.js"></script>
    <!-- endbuild -->
</body>
</html>

This concludes my tutorial finally.

To play with the auth, start the frontend and backend servers with grunt serve and rails s respectively and type any of the URLs in the address bar:

  • /signup
  • /login
  • /signout
  • /restricted

Have fun!

comments powered by Disqus