10: Adding devise integration – logon and security

In this portion of the tutorial we add Devise integration to provide logon to the application, and provide custom pages for password reset and account unlock.  This content is based somewhat on the equivalent content from the rails 3 version of the tutorial.

This will break down into three main elements:

  1. Provision of a registration page that matches the Devise expectations and calls the Devise register method and password change method
  2. Provision of a logon page that matches the Devise expectations and that calls the Devise logon method and associated methods such as password resets etc
  3. Provision of functionality such that AngularJS can detect that a user is not logged on and redirect the user to the logon page, rather than just having each server interaction fail

If you’ve dropped into the middle of the tutorial, you can find the code for the previous section at github: PaulL : tutorial_9.  You can go to the index page for this tutorial, or you can hit the tutorial menu above and see all the posts in the Rails 4 tutorial.

First we’re going to add devise to our project following the instructions at the devise wiki.  We start by adding it to the Gemfile :

gem 'devise'

Then run bundle install.

Next we’ll create the initialiser through:

rails generate devise:install

We follow the instructions in the messages that follow to set our mail host in config/environments/development.rb

    config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

and we make sure we have a root in our routes.rb.  I’m setting my root as:

  root to: 'home#index'

Later we’ll come back and create a home controller.

We go and look in config/initializers/devise.rb to see what settings we might want to change. In our case we changed:

  • config.paranoid = true   By default if you enter a username to do a password reset for, it will confirm whether or not that username exists (i.e. if it doesn’t exist it says “username doesn’t exist”, which helps a hacker).  With paranoid, it just accepts and silently fails if the username is wrong, meaning the hacker has to guess both username and password
  • config.allow_unconfirmed_access_for = 2.days   Allows people to use the application for 2 days without verifying their e-mail address
  • config.confirm_within = 5.days     Users get 5 days to confirm their account, then they need to request a new confirmation link/token
  • config.timeout_in = 30.minutes     Timeout after 30 minutes of inactivity
  • config.lock_strategy = :failed_attempts Lock user account after too many failed password attempts
  • config.unlock_strategy = :both Allow unlock after a period of time, and by using an unlock link
  • config.maximum_attempts = 20 Lock account after 20 failed password attempts (a real user would have given up by then anyway)
  • config.unlock_in = 1.hour Automatically unlock after 1 hour

We then generate the users model, to which devise will be connected:

rails generate devise user

We want to use some extra modules, so before we migrate we go into the migration and edit it to uncomment the confirmable and lockable modules (including the associated indexes). Then:

rake db:migrate

We open the user.rb model, and add the confirmable, lockable and timeoutable modules.

We are following the instructions at upgrading devise to 2.2, and we want our devise installation to respond to json, so we open config/application.rb and within the Class Application add the block:

config.to_prepare do
   DeviseController.respond_to :html, :json
end

We restart the rails server, and we’re ready to write the AngularJS end of this integration.

You should be able to visit the native Devise pages at this point: http://localhost:3000/users/sign_up and http://localhost:3000/users/sign_in.  We haven’t told our controllers that they need authentication before returning a response, so we can still get to our application without being logged in.

Next we modify our application controller to handle CSRF validation more completely, following the content from this blog post:

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: :exception   
  after_filter :set_csrf_cookie_for_ng  
    
  def intercept_html_requests     
    redirect_to('/') if request.format == Mime::HTML   
  end

  def set_csrf_cookie_for_ng     
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?   
  end      

  rescue_from ActionController::InvalidAuthenticityToken do |exception|     
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?     
    message = 'Rails CSRF token error, please try again'     
    render_with_protection(message.to_json, {:status => :unprocessable_entity}) 
  end
    
  def render_with_protection(object, parameters = {})
    render parameters.merge(content_type: 'application/json', text: ")]}',\n" + object.to_json)
  end

protected

  def verified_request?
    super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
  end

end

Our next step is to modify our controllers (both clubs_controller.rb and teams_controller.rb) to require authentication before processing any transactions.  Add near the top of each controller:

class ClubsController < ApplicationController
  before_filter :authenticate_user!  
  ...

This is also the point where we’ll sort out our root or index page. Previously if you visited http://localhost:300 you’d have received the default rails index page, we’d like it to redirect to UI/index.html. So, delete public/index.html, and try again. Now we’ll get a routing error saying that the home controller doesn’t exist. So we’ll create that:

rails generate controller home

And edit that new file app/controllers/home_controller.rb to include an index action that performs a redirect:

class HomeController < ApplicationController
  def index
    redirect_to('/UI/index.html')
  end
end

This should mean that when you visit http://localhost:3000/ that you get redirected to /UI/index.html.  When you go to the clubs or teams page (when not signed in) it should be blank.  If you go to users/sign_in or users/sign_up, and sign in, then clubs and teams should start working.  You currently cannot log out, as that’s a delete action, but you can clear your cookies for localhost, which will remove the session.  We now have a situation where we can only see data from our server when we’re logged in, but we’re using the native Devise pages, and our usability is poor.

Next we’re going to address this by creating AngularJS register and login pages, and by automatically redirecting people to the login page any time they’re not authenticated.

Start by creating a login folder in src/app/login, and a login page src/app/login/login.tpl.html.  

At the moment we want a username (email) and password, and a button each for login and logout.

<div>
  <h1>Login</h1>
</div>

<div class="body">
  <div>Email: <input type="email" ng-model="login_user.email" /></div>
  <div>Password: <input type="password" ng-model="login_user.password" /></div>

  <button ng-click="login()" class="btn btn-primary" >Login</button>
  <button ng-click="logout()" class="btn btn-primary" >Logout</button>
</div>

Next, we need a controller to control that.  Again, following the ng-boilerplate format, we’re using the UI router, and we’re choosing to put all the logic for that in a single javascript file, src/app/login/login.js:

/**
 * Login module
 */
angular.module( 'league.login', [
  'ui.state'
])

/**
 * Define the route that this module relates to, and the page template and controller that is tied to that route
 */
.config(function config( $stateProvider ) {
  $stateProvider.state( 'login', {
    url: '/login',
    views: {
      "main": {
        controller: 'LoginCtrl',
        templateUrl: 'login/login.tpl.html'
      }
    },
    data:{ pageTitle: 'Login' }
  });
})

/**
 * And of course we define a controller for our route.
 */
.controller( 'LoginCtrl', function LoginController( $scope, $http ) {
  $scope.login_user = {email: null, password: null};

  $scope.login = function() {
    $http.post('../users/sign_in.json', {user: {email: $scope.login_user.email, password: $scope.login_user.password}});
  };

  $scope.logout = function() {
    $http({method: 'DELETE', url: '../users/sign_out.json', data: {}});
  };
})
;

We also need to add league.login to the top of app.js.

angular.module( 'league', [
  'templates-app',
  'templates-common',
  'league.home',
  'league.about',
  'league.club',
  'league.team',
  'league.login',
  'ui.state',
  'ui.route'
])

What this is doing is defining our routes following the ui-router format.  We then declare a controller and within that a model, and we call the http services directly for the login and logout actions, rather than using a resource.

We’re at this stage giving no feedback to the user on whether it works or not, but if you were to run grunt build you should find that this runs if you navigate tohttp://localhost:3000/UI/index.html#/login.  You’ll need to use the vanilla devise page to register a test user (http://localhost:3000/users/sign_up), once you’ve done that you should find that the login and logout work (when looking at the rails server console you should see a 201 success response).

Next, we refactor our logic a bit to create generic success and failure handlers, and put these into a new service.  This service provides a success handler and a failure handler. The success handler deals with the fact that only a 201 or 204 response are true success – Devise will sometimes return other codes that are strictly (in http terms) a success, but that don’t result in a login. We also process failed messages, splitting those that give field level errors from those that give only generic messages. All this content gets loaded into an error block, which we later attach to our login html page.

Create src/common/errorService/error_service.js:

/*
 * Error handling service, deals with return values from the Devise $http calls
 * 
 */

angular.module( 'common.errorService', [])

.service('errorService', function( ) {
  return {
    success: function( data, status, success_message, $scope ) {
      if (status == 201 || status == 204){
        $scope.errors.message = success_message;
        $scope.reset_users();
      } else {
        if (data.error) {
          $scope.errors.message = data.error;
        } else {
          // note that JSON.stringify is not supported in some older browsers, we're ignoring that
          $scope.errors.message = "Success, but with an unexpected success code, potentially a server error, please report via support channels as this indicates a code defect.  Server response was: " + JSON.stringify(data);
        }
      }
    },
  
    failure: function( data, status, $scope ) {
      if (status == 422) {
        $scope.errors.errors = data.errors;          
      } else {
        if (data.error) {
          $scope.errors.message = data.error;
        } else {
          // note that JSON.stringify is not supported in some older browsers, we're ignoring that
          $scope.errors.message = "Unexplained error, potentially a server error, please report via support channels as this indicates a code defect.  Server response was: " + JSON.stringify(data);
        }
      }
    }
  };
});

And then add this module to the top of app.js:

angular.module( 'league', [
  'templates-app',
  'templates-common',
  'common.errorHandling',
  'common.errorService',
  'league.home',

Modify login.js to include failure and success handlers:

.controller( 'LoginCtrl', function LoginController( $scope, $http, $state, errorService ) {
  $scope.reset_users = function() {
    $scope.register_user = {email: null, password: null, password_confirmation: null};
  };
  
  $scope.reset_users();
  $scope.errors = {message: null, errors: {}};
  
  $scope.login = function() {
    $http.post('../users/sign_in.json', {user: {email: $scope.login_user.email, password: $scope.login_user.password}})
      .success( function( data, status ) {
        errorService.success( data, status, 'logged in', $scope);
      })
      .error( function( data, status ) {
        errorService.failure( data, status, $scope);
      });
  };

  $scope.logout = function() {
    $http({method: 'DELETE', url: '../users/sign_out.json', data: {}})
      .success( function( data, status ) {
        errorService.success( data, status, 'logged out', $scope);
      })
      .error( function( data, status ) {
        errorService.failure( data, status, $scope);
      });
  };
})
;

And modify the login.tpl.html to display the messages:

<div>
  <h1>Login</h1>
</div>

<div ng-show="errors.message" class="error">
  <p>{{errors.message}}</p>
</div>
<div class="body">
  <div ng-class="{error: errors.errors.email}">Email: <input type="email" ng-model="login_user.email" />
    <div ng-show="errors.errors.email">
      <div ng-repeat="field_error in errors.errors.email">{{field_error}}</div>
    </div>
  </div>

  <div ng-class="{error: errors.errors.password}">Password: <input type="password" ng-model="login_user.password" />
    <div ng-show="errors.errors.password">
      <div ng-repeat="field_error in errors.errors.password">{{field_error}}</div>
    </div>
  </div>

  <button ng-click="login()" class="btn btn-primary" >Login</button>
  <button ng-click="logout()" class="btn btn-primary" >Logout</button>
</div>

Finally, modify index.html to change the readme link to be a login link:

                <li>
                  <a ui-route="/login" ng-class="{active:$uiRoute}">
                    <i class="icon-login"></i>
                    Login/Logout
                  </a>
                </li>

Run grunt build and try visiting the new login page, and logging in and out.  Try also pressing the login button without the e-mail or password entered, and (hopefully) see an error.

Next, we’d like to extend to the confirm and unlock functionality.

Confirm is used to confirm an e-mail address.  As we’ve configured above the user should get an e-mail upon registering that requests them to confirm their e-mail address by clicking on a link.  Since we’re running our rails server in development mode, it won’t actually send the e-mail, it will just write out the text on our rails server console.  We can see this by logging out, then visiting http://localhost:3000/users/sign_up.  Use a new e-mail address, and you should see an e-mail on your rails console, something like:

Sent mail to user@email.com (8.4ms)
Date: Fri, 22 Aug 2014 15:44:27 +1200
From: please-change-me-at-config-initializers-devise@example.com
Reply-To: please-change-me-at-config-initializers-devise@example.com
To: user@email.com
Message-ID: 53f6bc9baaa5_3f4e3fd4dbd752a49956@local.mail
Subject: Confirmation instructions
Mime-Version: 1.0
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

Welcome user@email.com!
You can confirm your account email through the link below:
href="http://localhost:3000/users/confirmation?confirmation_token=N5xvEXbjRt8AHQKjynh2"Confirm my account

If you copy that url, you’ll see that it will hit the devise base controller, and then redirect you to the application homepage. We are OK with this behaviour, but we’d like to adjust the mail template a little, and we’d like to put a message on our AngularJS home page that tells us that we’ve successfully confirmed our e-mail address.

Let’s start by customising the e-mail template. Execute:

   rails generate devise:views

This should create all the devise views under app/views/devise. Delete all the folders other than mailer, and edit confirmation_instructions.html.erb:

<p>Welcome to the league application <%= @email %>!</p>

<p>You can confirm your account email through the link below:</p>

<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>

Register a new user, and see the modified e-mail content.

Next, we’d like to go to the login page when we confirm an e-mail and give the user a message telling them that their e-mail is confirmed. We’re assuming here the user won’t be logged in, so they’ll get an e-mail, they’ll click on the link to confirm the e-mail, and they’ll end up on our login page with a message saying “e-mail confirmed, please login.”

First, we need to tailor the confirmations controller to redirect us to the login page, and to tell us that we came from the confirmation controller. Create the file app/controllers/confirmations_controller.rb:

class ConfirmationsController < Devise::ConfirmationsController

  protected

  def after_confirmation_path_for(resource_name, resource)
    '/UI/index.html#login?message=confirm'
  end

end

Adjust your config/routes.rb to tell devise that you’ve overridden the standard confirmations controller:

devise_for :users, :controllers => {confirmations: ‘confirmations’}

Try the confirmation process again, and verify that you now end up on the login page, and that the url has “?message=confirm” on the end.

Next, we’re going to adjust src/app/login.js to notice the confirmation message. First, we tell our state that a message could be on the url:

.config(function config( $stateProvider ) {
  $stateProvider.state( 'login', {
    url: '/login?message',

And then we modify the code of the controller to look in the $stateParams for the confirm message, and put a message up when the confirmation occurs.

.controller( 'LoginCtrl', function LoginController( $scope, $http, $state, $stateParams, errorService ) {
  if( $stateParams.message == 'confirm' ) {
    $scope.errors = {message: 'Your e-mail has ben confirmed, please log in', errors: {}};
  } else {
    $scope.errors = {message: null, errors: {}};
  }

We want to provide users with the ability to request a new confirmation link on their e-mail, we also would like them to be able to request a password reset when they’ve forgotten their password, and to request an unlock e-mail if they try their password incorrectly too many times. Add these buttons onto login.tpl.html:

  <button ng-click="confirm()" class="btn btn-primary" >Confirm</button>
  <button ng-click="unlock()" class="btn btn-primary" >Unlock</button>
  <button ng_click="forgot_password()" class="btn btn-primary" >Forgot Password</button>

Add the methods for these to login.js:

  $scope.forgot_password = function() {
    $http({method: 'POST', 
           url: '../users/password.json',
           data: {user: {email: $scope.login_user.email}}})
      .success( function( data, status ) {
        errorService.success( data, status, 'If your e-mail address is on file then password reset instructions have been sent to that e-mail address', $scope);
      })
      .error( function( data, status ) {
        errorService.failure( data, status, $scope);
      });
  };
  
  $scope.unlock = function() {
    $http({method: 'POST', 
           url: '../users/unlock.json',
           data: {user: {email: $scope.login_user.email}}})
      .success( function( data, status ) {
        errorService.success( data, status, 'If your e-mail address is on file and your account is locked then unlock instructions have been sent to that e-mail address', $scope);
      })
      .error( function( data, status ) {
        errorService.failure( data, status, $scope);
      });
  };
  
  $scope.confirm = function() {
    $http({method: 'POST', 
           url: '../users/confirmation.json',
           data: {user: {email: $scope.login_user.email}}})
      .success( function( data, status ) {
        errorService.success( data, status, 'If your e-mail address is on file then a new confirmation link has been sent to that e-mail address', $scope);
      })
      .error( function( data, status ) {
        errorService.failure( data, status, $scope);
      });
  };

We could customise the e-mails for reset and unlock, but we won’t. We do want to create an unlocks controller in rails so that we can display a message saying “your account has been unlocked”, we do this by creating app/controllers/unlocks_controller.rb as follows:

class UnlocksController < Devise::UnlocksController

  protected

  def after_unlock_path_for(resource)
    '/UI/index.html#login?message=unlock'
  end

end

And adjusting config/routes.rb again:

  devise_for :users, :controllers => {confirmations: 'confirmations', unlocks: 'unlocks'}

Finally, adjusting src/app/login/login.js to display the unlock message:

.controller( 'LoginCtrl', function LoginController( $scope, $http, $state, $stateParams, errorService ) {
  if( $stateParams.message == 'confirm' ) {
    $scope.errors = {message: 'Your e-mail has ben confirmed, please log in', errors: {}};
  } else if( $stateParams.message == 'unlock' ) {
    $scope.errors = {message: 'Your account has been unlocked, please log in', errors: {}};
  } else {
    $scope.errors = {message: null, errors: {}};
  }

You can set your failed attempts in config/initializers/devise.rb to a low value to test this (the unlock logic won’t do anything unless the account is actually locked):

  config.maximum_attempts = 2

Try logging in with the wrong password 3 times, you should see an e-mail on the rails console, and the link for that e-mail should unlock the account then redirect to the login page with an appropriate message.

Now, we want to build the register and change password page, including permitting users to reset their password using the token from the forgot password e-mail.

First, we need to modify the e-mail template app/views/devise/mailers/reset_password_instructions.html.erb, changing the link that we provide:

<p><%= link_to 'Change my password', root_url + "UI/index.html#register?resetToken=" + @token %></p>

This will give us a url of http://localhost:3000/UI/index.html?go=register&resetToken=token.

Then, we want to build the registration page and controller. The registration page needs both a password and a password confirmation field, and shows the reset button if there’s a reset token, otherwise it shows the register and change password buttons, and the current password field src/app/register/register.tpl.html:

<div>
  <h1>Register</h1>
</div>

<div ng-show="errors.message" class="error">
  <p>{{errors.message}}</p>
</div>
<div class="body">
  <div ng-show="!register_user.reset_password_token" ng-class="{error: errors.errors.email}">Email: <input type="email" ng-model="register_user.email" />
    <div ng-show="errors.errors.email">
      <div ng-repeat="field_error in errors.errors.email">{{field_error}}</div>
    </div>
  </div>

  <div ng-class="{error: errors.errors.password}">Password: <input type="password" ng-model="register_user.password" />
    <div ng-show="errors.errors.password">
      <div ng-repeat="field_error in errors.errors.password">{{field_error}}</div>
    </div>
  </div>

  <div ng-class="{error: errors.errors.password}">Password confirmation: <input type="password" ng-model="register_user.password_confirmation" />
    <div ng-show="errors.errors.password_confirmation">
      <div ng-repeat="field_error in errors.errors.password_confirmation">{{field_error}}</div>
    </div>
  </div>

  <div ng-if="!register_user.reset_password_token" ng-class="{error: errors.errors.password}">Current password: <input type="password" ng-model="register_user.current_password" />
    <div ng-show="errors.errors.current_password">
      <div ng-repeat="field_error in errors.errors.current_password">{{field_error}}</div>
    </div>
  </div>


  <button ng-if="!register_user.reset_password_token" ng-click="register()" class="btn btn-primary" >Register</button>
  <button ng-if="!register_user.reset_password_token" ng-click="change_password()" class="btn btn-primary" >Change Password</button>
  <button ng-if="register_user.reset_password_token" ng-click="reset_password()" class="btn btn-primary" >Change Password</button>
</div>

The registration controller src/app/register/register.js recognises the reset token, and calls the various methods:

/**
 * Registeration module
 */
angular.module( 'league.register', [
  'ui.state'
])

/**
 * Define the route that this module relates to, and the page template and controller that is tied to that route
 */
.config(function config( $stateProvider ) {
  $stateProvider.state( 'register', {
    url: '/register?resetToken',
    views: {
      "main": {
        controller: 'RegisterCtrl',
        templateUrl: 'register/register.tpl.html'
      }
    },
    data:{ pageTitle: 'Register' }
  });
})

/**
 * And of course we define a controller for our route.
 */
.controller( 'RegisterCtrl', function RegisterController( $scope, $http, $state, $stateParams, errorService ) {
  
  $scope.reset_users = function() {
    $scope.register_user = {email: null, password: null, password_confirmation: null};
  };
  
  $scope.reset_users();

  if($stateParams.resetToken){
    $scope.errors = {message: 'Please enter a new password and confirmation', errors: {}};
    $scope.register_user.reset_password_token = $stateParams.resetToken;
  } else {
    $scope.errors = {message: null, errors: {}};
  }

  $scope.register = function() {
    $http({method: 'POST', 
           url: '../users.json',
           data: {user: {email: $scope.register_user.email,
                         password: $scope.register_user.password,
                         password_confirmation: $scope.register_user.password_confirmation}}})
      .success( function( data, status ) {
        errorService.success( data, status, 'Thank you for registering, you are logged in', $scope);
      })
      .error( function( data, status ) {
        errorService.failure( data, status, $scope);
      });
  };

  $scope.change_password = function() {
    $http({method: 'PUT', 
           url: '../users.json',
           data: {user: {email: $scope.register_user.email,
                         password: $scope.register_user.password,
                         password_confirmation: $scope.register_user.password_confirmation,
                         current_password: $scope.register_user.current_password }}})
      .success( function( data, status ) {
        errorService.success( data, status, 'Your password has been changed', $scope);
      })
      .error( function( data, status ) {
        errorService.failure( data, status, $scope);
      });
  };

  $scope.reset_password = function() {
    $http({method: 'PUT', 
           url: '../users/password.json',
           data: {user: {email: $scope.register_user.email,
                         password: $scope.register_user.password,
                         password_confirmation: $scope.register_user.password_confirmation,
                         reset_password_token: $scope.register_user.reset_password_token }}})
      .success( function( data, status ) {
        errorService.success( data, status, 'Your password has been changed, you are logged in', $scope);
      })
      .error( function( data, status ) {
        errorService.failure( data, status, $scope);
      });
  };

  
})
;

We also update src/app/app.js to recognise the register module:

angular.module( 'league', [
  'templates-app',
  'templates-common',
  'common.errorHandling',
  'common.errorService',
  'league.home',
  'league.about',
  'league.club',
  'league.team',
  'league.login',
  'league.register',
  'ui.state',
  'ui.route'
])

and src/index.html to have a link to the register page:

                <li id="registerLink" ui-route="/register" ng-class="{active:$uiRoute}">
                  <a href="#/register">
                    Register
                  </a>
                </li>

For item 3 from the introduction, we want to detect that a user is not logged in, and redirect that user to the logon page, rather than just returning an error on every server interaction.  In thinking about this, it’s important to distinguish between someone who is not logged in and someone who is logged in but doesn’t have authority to perform the action they requested.  We’ve reviewed information on this, and we see that there are two important error codes that your application server can return:

  • 401 Unauthorised.  This is used for authentication error, that is to say, the user is not logged in.  These would typically be returned from Devise, and a user can resolve a 401 error by logging in properly.
  • 403 Forbidden.  This means that we know who you are, but you don’t have access to this resource.  This would typically be an authorisation error, and therefore typically returned from Cancan.

We also notice that the way http describes things doesn’t match normal security practice.  In the security space we tend to talk about authentication and authorisation.  Authentication is proving who you are (authenticating your identity), authorisation is typically about role-based access control (are you authorised to perform this action).  The http error response for 401 should logically be 401 Unauthenticated, but I presume that since it was defined in about 1990 that we can’t change it.  🙂  A more detailed explanation of the differences can be found on stackoverflow: 403 vs 401.

Our logic will focus on identifying 401 responses, which imply you’re not logged in, and redirecting you to the logon page when that happens.  We are using base code from jes.al: authentication with devise and angular, and updating it for the new syntax suggested in the$http interceptor documentation, which requires us only to provide a function for the specific intercept we want (in this case, the response error interceptor).

We want to detect the 401 error and redirect to the login page. To do this we create a global http interceptor, which we put in src/common/authentication/authentication.js. This common module will intercept every error http response and look at it to see whether it returned a 401 error. If it did, it redirects to the login page:

/**
 * Authentication module, redirects to homepage if not logged in 
 */

angular.module('common.authentication', [])

.config(function($httpProvider){
  // Intercepts every http request.  If the response is success, pass it through.  If the response is an
  // error, and that error is 401 (unauthorised) then the user isn't logged in, redirect to the login page 
  var interceptor = function($q, $location, $rootScope) {
    return {
      'responseError': function(rejection) {
        if (rejection.status == 401) {
          $rootScope.$broadcast('event:unauthorized');
          $location.path('/login');
          return rejection;
        }
        return $q.reject(rejection);        
      }
    };
  };
  $httpProvider.interceptors.push(interceptor);
});

We also need to include this module into app.js:

angular.module( 'league', [
  'templates-app',
  'templates-common',
  'common.errorHandling',
  'common.errorService',
  'common.authentication',
  'league.home',
  'league.about',
  'league.club',
  'league.team',
  'league.login',
  'league.register',
  'ui.state',
  'ui.route'
])

This should have the impact of redirecting you to the login page any time you visit a page that requires authentication – try this by grunt build, then visit the clubs page whilst not signed in.

If all this has gone well, you should now have register and login functionality working. The code for this tutorial can be found at github: PaulL : tutorial_10

Advertisements

11 thoughts on “10: Adding devise integration – logon and security

  1. I think I get the joke PaulL,with the J curve .
    I hope your wife and family well, and the reason you are so clever is because you born in NZ. We had a rough few days I think 46% Nat and the other 5% take us over the line

  2. Pingback: AngularJS and Rails 4 CRUD application using ng-boilerplate and twitter bootstrap 3: Tutorial Index | technpol

  3. Pingback: AngularJS and Devise – authentication with a rails server | technpol

  4. Pingback: AngularJS and Rails: CSRF protection | technpol

  5. Hello. I’ve downloaded your project from https://github.com/PaulL1/league-tutorial-rails4/ and I can’t authenticate a user account through the angular app… On client side i get “[HOST] is not allowed by Access-Control-Allow-Origin error”, even though i enabled CORS. On the server side, i get ‘Can’t verify CSRF token authenticity’ and ‘Unprocessable Entity’ errors. Can you help me understanding what’s happening? I’m stuck with this for days.

  6. On the client side – what is the host/url that it’s giving the error on? I’d guess that it’s on one of the fonts or something, and therefore probably something you could ignore.

    On the server side, the CSRF comes a bit later in the tutorial, but you’re perhaps getting unlucky and having it mess things up for you. And easy way around CSRF is to modify the application controller to modify the “protect_from_forgery” statement. Documentation here: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection/ClassMethods.html, I’d try perhaps setting to null_session (if it isn’t already), or use the skip_before_action. I’d note that this isn’t safe in production, but is something you could do to prove or disprove that the CSRF is a problem.

    Finally, on the unprocessable entity, which entity and when do you get that? Unprocessable entity is a 422 I think, and usually means that there’s a missing or invalid field – but also usually tells you in the response exactly which field it is that is invalid.

  7. Thanks for your reply PaulL. I took your tutorial as an example, but in the meantime I tried to do a sample app with users only on my own.
    Now I don’t have any CORS problems, but I get the same CSRF token error and unprocessable entity as well.
    When I set protect_from_forgery to null_session, the errors disappear and I can sign in (rails still complains about csrf authenticity), but when I access users list (requires authentication), the browser’s console says unauthorized.
    I posted full description of my problem in stackoverflow (http://stackoverflow.com/questions/25767692/csrf-token-authenticity-on-angularjsrails-app). If you have time, please take a look at it, since you seem very experienced in this subject. I would appreciate any help. This is getting me mad :S

  8. This tutorial was great for implementing the login procedure and the unauthorized interceptor. Thanks for it.

    I’m wondering if you have any input on an issue I’ve been struggling with that is the next step from what the tutorial covers. Whenever a user is logged in, my site shows their name and some attributes throughout the layout. My login controller uses a method inherited from the “application” controller (which all the other controllers inherit from) to set a currentUser on the application controller’s scope using what is essentially just an API wrapper for devise’s current_user method.

    This seems to work, but I’ve run into a number of problems with trying to make sure the user is updated when actions are taken that affect the data.

    What’s your approach for this issue? Most Rails apps with authentication need access to user data in much if not all of the app… Someone suggested to me trying to store the current user on the User service (something I also use for showing user profiles, user index, etc.), but I haven’t really been able to figure that out.

    Any suggestions would be much appreciated.

  9. In my case I basically did as people have suggested.

    I created a new service that I called userCache, I put the logic for getting the current user in there. I put in a listener for an event “userUpdate”, and anywhere that is capable of updating the user I put in a broadcast for “userUpdate”. When that happens I just call getCurrentUser again.

    (Actually, I had a few other things I cached, such as current project and some ref data, so I had a small family of caches and events that were triggered when any of these were updated. Not to mention a cookie that remembered the last selected project).

  10. I’ve changed rails version to 4.2.5 and got: No route matches [GET] “/UI/index.html”
    Tell us more about /UI concept

  11. The UI/index.html should be in the public folder of your rails application. Usually rails first looks to see if the path it’s been given matches a static resource that’s in public, if it does it serves it. If it doesn’t then it uses routes.rb to try to serve it from rails itself.

    Most likely you don’t have public/UI/index.html existing (or you have a symlink that’s busted in there), or something in rails has changed this behaviour in later versions.

    To be clear, public/UI should be a symlink to the angular bin or angular build directory. You also need to have run the angular build process.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s