Rails and Angular: Authentication with Devise

So you have an Angular application running on a domain (http://myfabulousapp.com) and the Rails API running on another domain (http://api.myfabulousapp.com). It’s all working well, but now you want to implement authentication using Devise. How do you do this?


Before start:

Our “test” project folder structure:

test
  +--- api (Rails, running on localhost:3000)
  +--- app (Angular, running on localhost:3001)
        +--- index.html
        +--- app.js
        +--- bower_components/
        +--- views
               +--- home.html

Let’s create our “test” project with these commands:

# create aplication folder
mkdir test
cd test

# create backend (api, Rails)
rails new api --api
cd api

# install Devise
gem 'devise' >> Gemfile
bundle install
rails generate devise:install
rails generate devise user
rake db:migrate

# start backend server
rails s

Open another terminal window:

# create frontend folder (app, Angular)
cd test
mkdir app
cd app

# install Angular and the router library
bower install angular --save
bower install angular-ui-router --save

# install a minimalist html webserver
# gem install adsf

# start frontend server
adsf -p 3001

The app/index.html:


  

The app/app.js:

angular.module('myApp', ['ui.router'])
  .config(['$stateProvider', '$urlRouterProvider', '$locationProvider', function ($stateProvider, $urlRouterProvider, $locationProvider) {

    if (window.history && window.history.pushState) {
      $locationProvider.html5Mode(true); // Uses "/some-url" instead of "/!#some-url"
    }

    $stateProvider
      .state('home', {
        url: '/',
        templateUrl: '/views/home.html',

      .otherwise({
        redirectTo: '/'
    });

  }]);

The main page:

Our Awesome Home Page

1. Install rack-cors gem

gem 'rack-cors', :require => 'rack/cors'
bundle install

2. Allow Angular to access Rails

# CORS
config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*' # on production, use the line below instead
#   origins 'localhost:3001', 'myfabulousapp.com'
    resource '*', :headers => :any, :methods => [:get, :post, :delete, :put, :head]
  end
end

PS: don’t put an “:options”. If you do that, you won’t be able to destroy sessions, because Angular will replace “delete” by “options” when communicating with another domain (and Devise won’t recognize the “option” verb as a “delete”)

PS 2: don’t forget to restart the server!!!


2.5. If Rails “–api”, enable Sessions

# Session
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
config.session_store :cookie_store, :key => '_namespace_key'
config.action_dispatch.cookies_serializer = :json

3. Install angular_devise library on your Angular app

cd test/app
bower install --save angular-devise

  

4. Configure Angular:

A) Add Devise module to the app:

angular.module('myApp', ['Devise'])

B) Add $http to always provide credentials:

$httpProvider.defaults.withCredentials = true;

C) Set Rails API urls (change api_url to your actual API location):

// API path
var api_url = 'http://localhost:3000/';

// sign in
AuthProvider.loginPath(api_url + 'users/sign_in.json');
AuthProvider.loginMethod('POST');
AuthProvider.resourceName('user');

// sign up
AuthProvider.registerPath(api_url + 'users.json');
AuthProvider.registerMethod('POST');
AuthProvider.resourceName('user');

// sign out
AuthProvider.logoutPath(api_url + 'users/sign_out.json');
AuthProvider.logoutMethod('DELETE');

Or a more extensive example:

var app = angular.module('myApp', [
  'ui.router',
  'Devise'
])

app.config([
  '$stateProvider',
  '$urlRouterProvider',
  '$locationProvider',
  '$httpProvider',
  'AuthProvider',

  function (
    $stateProvider,
    $urlRouterProvider,
    $locationProvider,
    $httpProvider,
    AuthProvider
  ) {

    $httpProvider.defaults.withCredentials = true;

    var api_url = 'http://localhost:3000/';

    // sign in
    AuthProvider.loginPath(api_url + 'users/sign_in.json');
    AuthProvider.loginMethod('POST');
    AuthProvider.resourceName('user');

    // sign up
    AuthProvider.registerPath(api_url + 'users.json');
    AuthProvider.registerMethod('POST');
    AuthProvider.resourceName('user');

    // sign out
    AuthProvider.logoutPath(api_url + 'users/sign_out.json');
    AuthProvider.logoutMethod('DELETE');

    if (window.history && window.history.pushState) {
      $locationProvider.html5Mode(true); // Uses "/some-url" instead of "/!#some-url"
    }

    $stateProvider
      .state('home', {
        url: '/',
        templateUrl: '/views/home.html',

      .otherwise({
        redirectTo: '/'
    });

  }]);

5. Remove the CSRF protection from Rails (I know, I know…)

respond_to :html, :json
# protect_from_forgery

PS: notice that I’m also including a “:json” to force Devise to respond to json calls.

PS: after do some tests and it’s all working, you can use a better solution to deal with CSRF, like use this angular_rails_csrf gem (which doesn’t worked for me, but maybe you have more luck – see my tries here).

PS II: “I don’t recommend to disable CSRF protection, blah, blah, blah…” Yeah, me neither, it’s just a test in order to troubleshooting why sessions are being lost after a refresh.


6. After a refresh, get the session back

angular.module('myModule', ['Devise'])
  .run(['Auth', function (Auth) {
    Auth.currentUser().then(function(user) {
      console.log(user);
      console.log(Auth._currentUser);
    });
  }]);
Close Menu