Integrating a Rails API backend with an Angular frontend using token authentication

In this blog post, I'll share some code that demonstrates integration between a Rails API backend and an Angular frontend with token based authentication via: ng-token-auth and devise_token_auth.

Part 1: the Rails backend

# create directory and RVM files:
mkdir rails-angular-2015
echo ruby-2.2.3 > rails-angular-2015/.ruby-version
echo rails_angular_2015 > rails-angular-2015/.ruby-gemset
cd rails-angular-2015

# install rails gem and create a new project
gem install rails
rails new --skip-spring --database=postgresql --skip-test-unit .

# setup database
rake db:create && db:migrate

Add Ruby gems, edit file: Gemfile

group :development, :test do
  gem 'better_errors'
  gem 'binding_of_caller'
  gem 'bullet'
  gem 'meta_request'
  gem 'pry-awesome_print'
  gem 'pry-rails'
  gem 'quiet_assets'
  gem 'rspec-rails'
  gem 'rubocop', require: false
end

gem 'devise_token_auth'
gem 'puma'
gem 'rack-cors'

Execute bundle install to install the gems.

Setup devise_token_auth by executing:

rails g devise_token_auth:install User api/auth

For this tutorial I decided to use basic email authentication (not omniauth), so I edited the file: app/models/user.rb

class User < ActiveRecord::Base
  # Include default devise modules.
  devise :database_authenticatable, :registerable,
          :recoverable, :rememberable, :trackable, :validatable,
          :confirmable
          # commented out:
          # :omniauthable
  include DeviseTokenAuth::Concerns::User
end

Executed rake db:migrate to update the database.

Add rack cors config to file: config/application.rb. Please note: this is wide open and should be locked down in a production environment.

module RailsAngular2015
  class Application < Rails::Application

    # ...snip...

    config.middleware.use Rack::Cors do
      allow do
        origins '*'
        resource '*',
          :headers => :any,
          :expose  => ['access-token', 'expiry', 'token-type', 'uid', 'client'],
          :methods => [:get, :post, :options, :delete, :put]
      end
    end
  end
end

Add a simple model and scaffold a controller:

rails g model Post title:string body:string
rake db:migrate
rails generate scaffold_controller api/Post --model-name=Post

Add the nested API routes, edit: config/routes.rb

Rails.application.routes.draw do
  mount_devise_token_auth_for 'User', at: 'api/auth'

  namespace :api do
    resources :posts
  end
end

At this point, the link_to and redirect_to paths in the controller and views may need to updated. See: git grep -i -E "(redirect|link)_to"

Update the jbuilder views to include all model attributes:

# edit file: app/views/api/posts/show.json.jbuilder

json.extract! @post, :id, :title, :body, :created_at, :updated_at

# edit file: app/views/api/posts/index.json.jbuilder

json.array!(@posts) do |post|
  json.extract! post, :id, :title, :body, :created_at, :updated_at
end

Update the controller, edit file: app/controllers/api/posts_controller.rb. I removed the format.html in each respond_to block, and updated the permitted/required params list.

class Api::PostsController < ApplicationController
  # we will require authentication after we know the controller works:
  # before_action :authenticate_user!

  before_action :set_post, only: [:show, :edit, :update, :destroy]

  def index
    @posts = Post.all
  end

  def show
  end

  def new
    @post = Post.new
  end

  def edit
  end

  def create
    @post = Post.new(post_params)

    respond_to do |format|
      if @post.save
        format.json { render :show, status: :created }
      else
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  def update
    respond_to do |format|
      if @post.update(post_params)
        format.json { render :show, status: :ok }
      else
        format.json { render json: @post.errors, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    @post.destroy
    respond_to do |format|
      format.json { head :no_content }
    end
  end

  private

    def set_post
      @post = Post.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def post_params
      params
        .require(:post)
        .permit(:title, :body)
    end
end

Update application controller, file: app/controllers/application_controller.rb, and revise the CSRF token settings.

class ApplicationController < ActionController::Base
  include DeviseTokenAuth::Concerns::SetUserByToken
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.

  protect_from_forgery with: :exception, if: Proc.new { |c| c.request.format != 'application/json' }
  protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }
end

At this point, you should be able to start rails and cURL the request to create a Post.

# I chose port 3030 to avoid having the angular frontend attempt to bind to the same port
rails s -p 3030 -b 0.0.0.0

# create a few posts via cURL
curl -XPOST 'http://localhost:3030/api/posts' -d '{"title":"test title","body":"test body"}' -H "Content-Type: application/json"

Now that the unauth API is working, I added user authentication. Edit file: app/controllers/api/posts_controller.rb

class Api::PostsController < ApplicationController
  before_action :authenticate_user!

  # ...snip...
end

Part 2: the Angular frontend

For the Angular frontend, I chose to use a Yeoman generator and Gulp. I tried out numerous generators (generator-angular, generator-hottowel, Grunt versions, etc), and decided to use generator-gulp-angular because it consistently works for me out of the box.

Install the global NPM packages, and create the new project.

# install global NPM packages
npm install -g yo bower gulp generator-gulp-angular

# create project
mkdir angular-rails-2015 && cd $_
yo gulp-angular angular-rails

# the options I chose:
? Which version of Angular do you want? 1.4.x (stable)
? What Angular modules would you like to have? (default)
? Do you need jQuery or perhaps Zepto? jQuery 2.x (new version, lighter, IE9+)
? Would you like to use a REST resource library? ngResource, the official support for RESTful services
? Would you like to use a router? UI Router, flexible routing with nested views
? Which UI framework do you want? Bootstrap, the most popular HTML, CSS, and JS framework
? How do you want to implement your Bootstrap components? Angular UI Bootstrap, Bootstrap components written in pure AngularJS by the AngularUI
? Which CSS preprocessor do you want? Sass (Node), Node.js binding to libsass, the C version of the popular stylesheet preprocessor, Sass.
? Which JS preprocessor do you want? None, I like to code in standard JavaScript.
? Which HTML template engine would you want? None, I like to code in standard HTML.

The first thing I modified was the gulp server file, to proxy all "/api" requests to the rails backend. edit file: gulp/server.js, revised the following line:

function browserSyncInit(baseDir, browser) {
  // ...snip...
  server.middleware = proxyMiddleware('/api', {target: 'http://localhost:3030', changeOrigin: true});
  // ...snip...
}

At this point if you execute gulp serve, the angular frontend will launch on port 3000. You should see the default boilerplate code ('Allo, 'Allo!). To ensure the backend proxy code is working, browse to "http://localhost:3000/api/posts", and it should be routed directly to rails.

Install 2 more bower packages for token auth and rails resources:

bower install --save angularjs-rails-resource ng-token-auth

Include the new modules, edit file: src/app/index.module.js

(function() {
  'use strict';

  angular
    .module('angularRails', ['ngAnimate', 'ngCookies', 'ngTouch', 'ngSanitize', 'ngMessages', 'ngAria', 'ngResource', 'ui.router', 'ui.bootstrap', 'toastr', 'rails', 'ng-token-auth']);

})();

To override the default ng-token-auth authProvider configuration, edit file: src/app/index.config.js

(function() {
  'use strict';

  angular
    .module('angularRails')
    .config(config);

  /** @ngInject */
  function config($logProvider, toastrConfig, $authProvider) {

    // ...snip...

    // ng-token-auth config:
    $authProvider.configure({
      // note: the defaults are fine for now
      // @see: https://github.com/lynndylanhurley/ng-token-auth#complete-config-example
    });
  }

})();

Edit the routes file, src/app/index.route.js. I added the Post factory, and the new route for posts:

(function() {
  'use strict';

  angular
    .module('angularRails')
    .config(routerConfig)
    .factory('Post', ['railsResourceFactory', function(railsResourceFactory) {
      return railsResourceFactory({
        url: '/api/posts',
        name: 'post'
      });
    }]);

  /** @ngInject */
  function routerConfig($stateProvider, $urlRouterProvider) {
    $stateProvider
      .state('home', {
        url: '/',
        templateUrl: 'app/main/main.html',
        controller: 'MainController',
        controllerAs: 'main'
      })
      .state('posts', {
        url: '/posts',
        templateUrl: 'app/posts/posts.html',
        controller: 'PostsController',
        controllerAs: 'posts'
      });

    $urlRouterProvider.otherwise('/');
  }

})();

Add the new posts controller, new file: src/app/posts/posts.controller.js

(function() {
  'use strict';

  angular
    .module('angularRails')
    .controller('PostsController', function($rootScope, $scope, $state, $stateParams, $auth, Post) {

      // method to query the posts api and store the results in $scope
      // note: the linter will complain, but that can be fixed later:
      // You should not set properties on $scope in controllers. Use controllerAs syntax and add data to "this"
      var post_query = function(){
        Post.query().then(function(posts){
          $scope.posts = posts;
        });
      }

      // when the user logs in, fetch the posts
      $rootScope.$on('auth:login-success', function(ev, user) {
        post_query();
      });

      // when the user logs out, remove the posts
      $rootScope.$on('auth:logout-success', function(ev) {
        $scope.posts = null;
      });

      // will get a "401 Unauthorized" if the user is not authenticated
      post_query();

    });

})();

Update the navbar component and link to the new posts route, edit file: src/app/components/navbar/navbar.html

<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-6">
  <ul class="nav navbar-nav">
    <li><a ng-href="#">Home</a></li>
    <li><a ng-href="#">About</a></li>
    <li><a ng-href="#">Contact</a></li>
    <li><a ui-sref="posts">Posts</a></li>
  </ul>
</div>

Create the template file for posts, new file: src/app/posts/posts.html

<div class="container">

  <!-- navbar component -->
  <div>
    <acme-navbar creation-date="main.creationDate"></acme-navbar>
  </div>

  <!-- Sign in/out form and user signed in details -->
  <div class="panel panel-default">
    <div class="panel-heading">Sign in via email</div>
    <div class="panel-body">

      <label>user signed in?</label>
      <p>{{ user.signedIn ? "true" : "false" }}</p>
      <label>user email</label>
      <p>{{ user.email || "n/a" }}</p>

      <!-- sign in form -->
      <form ng-submit="submitLogin(loginForm)" role="form" ng-init="loginForm = {}" ng-if="!user.signedIn">
        <div class="form-group">
          <label for="email">Email address</label>
          <input type="email" class="form-control" id="email" ng-model="loginForm.email" placeholder="Email">
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input type="password" class="form-control" id="password" ng-model="loginForm.password" placeholder="Password">
        </div>
        <button type="submit" class="btn btn-default">Submit</button>
      </form>

      <!-- sign out button -->
      <button class="btn btn-default" ng-click="signOut()" ng-if="user.signedIn">Sign out</button>

    </div>
  </div>

  <!-- show posts data -->
  <div class="panel panel-default">
  <div class="panel-heading">Posts</div>
    <div class="panel-body">
      <ul ng-repeat="post in posts" ng-if="user.signedIn">
        <li>{{ post.title }} - {{ post.body }}</li>
      </ul>

      <p ng-if="!user.signedIn">You must be signed in to request posts</p>
    </div>
  </div>

</div>

The frontend posts page should now resemble:

Angular signed out

Create a new user via the rials console, rails c:

User.create({email: 'test@example.com', password: 'password', confirmed_at: DateTime.now})

Now you can sign into the frontend, and request posts:

Angular signed in