Authentication with Rails, Devise and AngularJS

I recently started building a Rails driven web app and decided to use Devise for authentication. This would be pretty straight forward to implement but I planned to use AngularJS to power the front-end and decided to only use Rails as a JSON API.

Getting down to development on that path, I quickly ran into some problems structuring AngularJS to recognize Devise sessions. Thanks to some useful examples on GitHub I was able to get around those issues and get them to play nice. Here’s how:

Let’s first look at the main application.js file of my Angular app:

angular.module('radd', ['sessionService','recordService','$strap.directives'])
  .config(['$httpProvider', function($httpProvider){
        $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content');

        var interceptor = ['$location', '$rootScope', '$q', function($location, $rootScope, $q) {
            function success(response) {
                return response
            };

            function error(response) {
                if (response.status == 401) {
                    $rootScope.$broadcast('event:unauthorized');
                    $location.path('/users/login');
                    return response;
                };
                return $q.reject(response);
            };

            return function(promise) {
                return promise.then(success, error);
            };
        }];
        $httpProvider.responseInterceptors.push(interceptor);
  }])
  .config(['$routeProvider', function($routeProvider){
    $routeProvider
      .when('/', {templateUrl:'/home/index.html'})
      .when('/record', {templateUrl:'/record/index.html', controller:RecordCtrl})
      .when('/users/login', {templateUrl:'/users/login.html', controller:UsersCtrl})
      .when('/users/register', {templateUrl:'/users/register.html', controller:UsersCtrl});
  }]);

First, you’ll see I’m setting the request header with a CSRF token to make sure Rails doesn’t create a new session for every request that goes out. Second, I’m creating an interceptor which will basically intercept any 401 Unauthorized responses and direct them to the login page.

Next up let’s create a sessions controller (derived from Devise::SessionsController) which will give us some CRUD functionality through a JSON interface:

class SessionsController < Devise::SessionsController
  respond_to :json
  def create
    resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#failure")
    render :status => 200,
           :json => { :success => true,
                      :info => "Logged in",
                      :user => current_user
           }
  end

  def destroy
    warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#failure")
    sign_out
    render :status => 200,
           :json => { :success => true,
                      :info => "Logged out",
           }
  end

  def failure
    render :status => 401,
           :json => { :success => false,
                      :info => "Login Credentials Failed"
           }
  end

  def show_current_user
    warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#failure")
    render :status => 200,
           :json => { :success => true,
                      :info => "Current User",
                      :user => current_user
           }

  end
end

Now let’s create an AngularJS Session service which would interact with that controller:

angular.module('sessionService', [])
    .factory('Session', function($location, $http, $q) {
        // Redirect to the given url (defaults to '/')

        function redirect(url) {
            url = url || '/';
            $location.path(url);
        }
        var service = {
            login: function(email, password) {
                return $http.post('/login', {user: {email: email, password: password} })
                    .then(function(response) {
                        service.currentUser = response.data.user;
                        if (service.isAuthenticated()) {
                            //TODO: Send them back to where they came from

                            //$location.path(response.data.redirect);

                            $location.path('/record');
                        }
                    });
            },

            logout: function(redirectTo) {
                $http.post('/logout').then(function() {
                    service.currentUser = null;
                    redirect(redirectTo);
                });
            },

            register: function(email, password, confirm_password) {
                return $http.post('/users.json', {user: {email: email, password: password, password_confirmation: confirm_password} })
                .then(function(response) {
                    service.currentUser = response.data;
                    if (service.isAuthenticated()) {
                        $location.path('/record');
                    }
                });
            },
            requestCurrentUser: function() {
                if (service.isAuthenticated()) {
                    return $q.when(service.currentUser);
                } else {
                    return $http.get('/current_user').then(function(response) {
                        service.currentUser = response.data.user;
                        return service.currentUser;
                    });
                }
            },

            currentUser: null,

            isAuthenticated: function(){
                return !!service.currentUser;
            }
        };
        return service;
    });

That pretty much does it. Now if you call a service which tries to access any Rails controller with a “before_filter :authenticate_user!” in it, you will automatically be kicked out and prompted to login.

I’ve put up a working demo on GitHub: https://github.com/jesalg/RADD.

If you have any questions or comments, please post them below. If you liked this post, you can share it with your followers or follow me on Twitter!