Authentication with Rails, Devise and AngularJS
08 Aug 2013I 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.