AngularJS and Devise – authentication with a rails server

Having spent some time building a sample app that integrates rails with AngularJS, I’m now looking at some of the other features to make that functionality more enterprise-ready.  The framework I’ve been working on uses devise, cancan and rolify to control access to applications.

In this configuration devise provides the authentication, and cancan/rolify provide role based access to functions within the application.  I’ll deal with cancan and rolify in a later post, as it’s necessary for Angular to know what accesses you have in deciding what links and buttons to show you.  In this post, we’ll deal with using Devise for logon with an AngularJS frontend.

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

For this discussion we’re going to use the league application that was built in my tutorial series.  I’ll try to avoid this being incomprehensible to people who didn’t work through that series, but if you want to follow along with the code you’ll need that code base, which you can find on github:PaulL.  There is also a version of this post in my newer Rails 4 tutorial, that post can be found here.

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: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.  With paranoid, it just accepts and silently fails if the username is wrong
  • config.allow_unconfirmed_access_for = 2.days    # allow people to use the application for 2 days without verifying their e-mail address
  • we probably want to change other things, come back to this

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), and to comment out the rememberable module. Then:

rake db:migrate

We open the user.rb model, and add the confirmable, lockable and timeoutable modules, and remove the rememberable module.  We also remove remember_me from the accessible attributes.  I’m not using the rememberable module as this essentially implements “keep me logged in on this computer” logic.  I’m planning to use the session logic to keep someone logged in for 30-60 minutes, but not to allow them to stay logged in for multiple days.

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 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.

I’ve reviewed a number of different integration guides on the web, many of them have had difficulty integrating using json, and have written a custom session controller for devise to allow this.  I’ve based my thinking heavily on the logic from jes.al: authentication with devise and angular, but having got this working I’ve found that the existing devise controllers seem to handle json just fine, whereas jes.al seem to have had problems with redirects.  I think this change added relatively recently to devise, within the devise github page we can review the controller code itself for the session controller and see that there is logic for “is_navigational_format” – which I think means it doesn’t do redirects when the request format is json.  So, whilst we’re using jes.al for inspiration, we’ve written our own code that should require no changes to native devise at the rails end.

We’ll build this in elements to illuminate exactly how it’s put together.  First we’ll create the login and logout functions with no error handling to demonstrate how it works.  We’ll then factor out the common code, and create all the ancillary methods.  We’re choosing to put both login and register on the same page – the idea is someone hits the login page and they need to either login (if a current user) or register (if they’re not a current user).  This may be better split into separate pages, but from an integration viewpoint it makes little difference, so we’ll do it this way.

So, first we create a new <code>src/app/login</code> folder (following the ng-boilerplate pattern), and create a <code>src/app/login/login.tpl.html</code> file to provide our UI.  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 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:

/**
 * Club 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.  Ideally we’d use a resource rather than the native http service, but I have not found a way to do that.  Devise splits these actions across a number of controllers, but each of those controllers appear to expect a “user” object to be passed to them.  When we don’t manually create the object it doesn’t end up tagged as a user object – and rails/devise appear not to accept it.  So the native $http service it is.

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 to http://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).

Moving forward, we’d like to refactor the code in a couple of ways.  First, we would like to add error handling.  Second, we’d like to put all our http calls into a single service so that the error handling and other functions can be handled centrally.  This changes src/app/login/login.js as follows:

/**
 * Club 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_error = {message: null, errors: {}};    

  $scope.login = function() {
    $scope.submit({method: 'POST', 
                   url: '../users/sign_in.json',
                   data: {user: {email: $scope.login_user.email, password: $scope.login_user.password}},
                   success_message: "You have been logged in.",
                   error_entity: $scope.login_error});
  };

  $scope.logout = function() {
    $scope.submit({method: 'DELETE', 
                   url: '../users/sign_out.json',
                   success_message: "You have been logged out.",
                   error_entity: $scope.login_error});
  };

  $scope.submit = function(parameters) {
    $scope.reset_messages();

    $http({method: parameters.method,
           url: parameters.url,
           data: parameters.data})
      .success(function(data, status){
        if (status == 201 || status == 204){
          parameters.error_entity.message = parameters.success_message;
          $scope.reset_users();
        } else {
          if (data.error) {
            parameters.error_entity.message = data.error;
          } else {
            // note that JSON.stringify is not supported in some older browsers, we're ignoring that
            parameters.error_entity.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);
          }
        }
      })
      .error(function(data, status){
        if (status == 422) {
          parameters.error_entity.errors = data.errors;          
        } else {
          if (data.error) {
            parameters.error_entity.message = data.error;
          } else {
            // note that JSON.stringify is not supported in some older browsers, we're ignoring that
            parameters.error_entity.message = "Unexplained error, potentially a server error, please report via support channels as this indicates a code defect.  Server response was: " + JSON.stringify(data);
          }
        }
      });
  };

  $scope.reset_messages = function() {
    $scope.login_error.message = null;
    $scope.login_error.errors = {};
  };

  $scope.reset_users = function() {
    $scope.login_user.email = null;
    $scope.login_user.password = null;
  };
})
;

Walking through the logic here:

  • We declare a new model component – login_error.  This has a message component – which we’ll display at the top of the page for errors that relate to the whole transaction – and an errors component, which holds field level errors that are returned by rails.  Shortly we’ll show where we bind that in the view
  • We factor out our http calls into a submit method, which takes in the HTTP verb (GET, POST, PUT etc), the URL we want to hit, the body of the request we want to send, a message we’d like to display if we’re successful, and the model we want to put the errors into (if there are any)
  • When we call submit, we first reset any messages that are there from a previous call.  We then call the $http service with the parameters we received, and check for errors.  The errors are a bit tricky, because some of the things that AngularJS considers to be success we consider to be a failure:
    • 201 and 204 are our success codes (no doubt there are other codes that are also arguably success, I’m ignoring them for now until I see them).  We put the success message into the login_error.message model, and we blank out the email/password fields (you’re now logged in)
    • Everything else we treat as an error, and we put the data.error content (which should be the error message) into login_error.message
    • On the failure side, if it’s a 422 that means we have field level validation errors.  We take those errors and put them into login_error.errors model
    • Otherwise, for any other failure we assume we have a data.error element, and we put that into login_error.message

We need to wire this content to our view, and we’re using the same pattern we did in the wider tutorial for showing errors at the field level.  Create an error class in src/less/main.less if there isn’t already one there:

.error {
  color: red;
}

And we update our src/app/login/login.tmp.html view to include some error objects to display our returned errors:

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

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

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

This should give you a functional page where login and logout work, and errors and success messages are correctly handled.  It’s also created the framework to now add our other features.

We’re now going to create a second section on the page for registration (and password change), as these require a password and a confirmation password.  We’ll define a second model for this, as if we use the same model we end up with a situation where if you type in one e-mail field the data appears in both.  We could build this up slowly and explain it along the way, but since you’re so smart we’ll jump straight to the answer. src/app/login/login.js now looks like:

/**
 * Club 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_error = {message: null, errors: {}};    
  $scope.register_user = {email: null, password: null, password_confirmation: null};
  $scope.register_error = {message: null, errors: {}};

  $scope.login = function() {
    $scope.submit({method: 'POST', 
                   url: '../users/sign_in.json',
                   data: {user: {email: $scope.login_user.email, password: $scope.login_user.password}},
                   success_message: "You have been logged in.",
                   error_entity: $scope.login_error});
  };

  $scope.logout = function() {
    $scope.submit({method: 'DELETE', 
                   url: '../users/sign_out.json',
                   success_message: "You have been logged out.",
                   error_entity: $scope.login_error});
  };

  $scope.password_reset = function () {
    $scope.submit({method: 'POST', 
                   url: '../users/password.json',
                   data: {user: {email: $scope.login_user.email}},
                   success_message: "Reset instructions have been sent to your e-mail address.",
                   error_entity: $scope.login_error});
  };

  $scope.unlock = function () {
    $scope.submit({method: 'POST', 
                   url: '../users/unlock.json',
                   data: {user: {email: $scope.login_user.email}},
                   success_message: "An unlock e-mail has been sent to your e-mail address.",
                   error_entity: $scope.login_error});
  };

  $scope.confirm = function () {
    $scope.submit({method: 'POST', 
                   url: '../users/confirmation.json',
                   data: {user: {email: $scope.login_user.email}},
                   success_message: "A new confirmation link has been sent to your e-mail address.",
                   error_entity: $scope.login_error});
  };

  $scope.register = function() {
    $scope.submit({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_message: "You have been registered and logged in.  A confirmation e-mail has been sent to your e-mail address, your access will terminate in 2 days if you do not use the link in that e-mail.",
                   error_entity: $scope.register_error});
  };

  $scope.change_password = function() {
    $scope.submit({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}},
                   success_message: "Your password has been updated.",
                   error_entity: $scope.register_error});
  };

  $scope.submit = function(parameters) {
    $scope.reset_messages();

    $http({method: parameters.method,
           url: parameters.url,
           data: parameters.data})
      .success(function(data, status){
        if (status == 201 || status == 204){
          parameters.error_entity.message = parameters.success_message;
          $scope.reset_users();
        } else {
          if (data.error) {
            parameters.error_entity.message = data.error;
          } else {
            // note that JSON.stringify is not supported in some older browsers, we're ignoring that
            parameters.error_entity.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);
          }
        }
      })
      .error(function(data, status){
        if (status == 422) {
          parameters.error_entity.errors = data.errors;          
        } else {
          if (data.error) {
            parameters.error_entity.message = data.error;
          } else {
            // note that JSON.stringify is not supported in some older browsers, we're ignoring that
            parameters.error_entity.message = "Unexplained error, potentially a server error, please report via support channels as this indicates a code defect.  Server response was: " + JSON.stringify(data);
          }
        }
      });
  };

  $scope.reset_messages = function() {
    $scope.login_error.message = null;
    $scope.login_error.errors = {};
    $scope.register_error.message = null;
    $scope.register_error.errors = {};    
  };

  $scope.reset_users = function() {
    $scope.login_user.email = null;
    $scope.login_user.password = null;
    $scope.register_user.email = null;
    $scope.register_user.password = null;
    $scope.register_user.password_confirmation = null;
  };
})
;

We’ve added methods to call the base devise methods for most of the functions on offer:

  • password_reset, this requires a username, and sends reset instructions to your e-mail address
  • unlock, this requires a username, and sends unlock instructions to your e-mail address
  • confirm, this requires a username and sends a new confirmation token to your e-mail address
  • register, this takes username, password and password_confirmation, and both registers you and sends a confirmation e-mail to your e-mail address
  • change_password, this uses the register section of the page, and does an online password change.  I note that it doesn’t you to re-enter your old password first, which is sort of unusual.  Something to be careful about

And the page now looks like:

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

<div ng-show="login_error.message" class="error">
  <p>{{login_error.message}}</p>
</div>

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

  <div ng-class="{error: login_error.errors.password}">Password: <input type="password" ng-model="login_user.password" />
    <div ng-show="login_error.errors.password">
      <div ng-repeat="field_error in login_error.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>
  <button ng-click="password_reset()" class="btn btn-primary" >Reset Password</button>
  <button ng-click="unlock()" class="btn btn-primary" >Unlock</button>
  <button ng-click="confirm()" class="btn btn-primary" >Confirm</button>
</div>

<div>
  <h1>Register / Password Change</h1>
</div>

<div ng-show="register_error.message" class="error">
  <p>{{register_error.message}}</p>
</div>

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

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

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

  <button ng-click="register()" class="btn btn-primary" >Register</button>
  <button ng-click="change_password()" class="btn btn-primary" >Change Password</button>
</div>

This creates two blocks on the page, login or register/password reset.  In my mental picture this should have login as a block to the left, and to the right a register block.  So a user visiting your page either logs in using an existing userid, or registers.  There are lots of ways to lay this out, including separate pages for these two functions, I’m treating this as a proof of concept, not as an html design tutorial.

Run grunt build, and take a look at the result.

Having completed this work, we’ve covered points 1 and 2 from our original introduction.

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).

The first step is to set all our controllers to authorise, as at the moment (despite our new login logic) there is nothing stopping people from viewing our clubs and teams without being signed in.  At the top of each rails controller enter:

  before_filter :authenticate_user!

Now when you visit clubs or teams without being signed in, you should get an error on your rails console (401 unauthorised) and a list page with no data shown.

Next we want to detect this 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.authentication',
  'league.home',
  'league.about',
  'league.club',
  'league.team',
  'league.login',
  '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.

This is also the point where we’ll sort out our root or index page. Currently if you visit http://localhost:300 you’ll get 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. If the index page requires something from the server that will in turn redirect you to the login page, if it doesn’t then you’ll stop at the index page, and when you click clubs or teams, it will prompt for a login.

We’ve created a substantial unit test suite for this setup, it’s a large file so easier to view directly on github: login_spec.js.  This provides a set of detailed tests for the submit method and the associated error handling, and then tests each of the individual methods for correct html format.

The code for this is branched from tutorial 7, and can be found on github:devise_integration.

As a supplementary note, you should also be careful with devise and csrf.  In summary you need to override the sessions controller, and make sure a new csrf token is set on login and logout, a full post on this topic is here.

Advertisements

21 thoughts on “AngularJS and Devise – authentication with a rails server

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

  2. Pingback: AngularJS and Rails Tutorial: part 7 form error handling, datepicker | technpol

  3. Thank you very much for the tutorial. It is very useful tutorial for me.
    Could you me tell me when the next parts of this tutorial (using cancan, rolify to set permission for specific user) is ready?

    Again, many thanks!!!

  4. Quang,

    Thanks for the feedback. Rolify and cancan are coming, I haven’t yet worked out how I want to handle the client side so that I enable/disable functions based on cancan. If you follow the blog you’ll get an e-mail when the next update comes.

    Paul

  5. Pingback: Adding translation using angular-translate to an angularjs app | technpol

  6. Ok, I’ve followed all the steps and when I’m on /login, I get 201. When I click on /clubs I get 401. What could the issue be?

  7. I have occasionally had an issue, again related to CSRF, where you login successfully, but still get 401 on subsequent pages. But if you refresh your browser it works. I believe it’s to do with an authenticity token, and I suspect that handling has changed in Rails 4.

  8. Paul, would you share an thoughts if you have come up with any ideas to handle the client side with cancan?

    Again, thank you very much for sharing your knowledge. Your blog posts’ series are really helpful to me and some of my colleagues.

  9. I’ve been thinking on CanCan, but haven’t written it yet.

    I think I’m coming down to the logic that I’m only using a subset of CanCan – so I use it in two ways:
    1. Users have access to perform actions on a particular class – e.g. a given user can index teams, but maybe not create teams
    2. A small set of classes I control at the object level, so for argument’s sake I grant users access to particular clubs – a user can update _this_ club, but not other clubs

    I’m then planning to work in two ways. When I return a list of clubs, I’ll use accessible_by in the controller to only return those the user can see. I’ll also put flags against each item in the result set from the rails controller, so can_edit, can_view etc, and use these to determine what buttons I show, I haven’t worked out quite how I do this, but I can imagine a couple of ugly ways (just not sure whether I can imagine elegant ways yet). This isn’t 100%, but the rails end will duplicate validate what the user can do, so this isn’t a security risk, just a usability risk

    2. Once a user is inside a club, then their access to sub-entities of the club is based on the class they’re accessing such as team (and the club), not on the individual instances. So a given user can or cannot edit teams. So I’m planning to create a service client side that holds the user’s permissions, and enable buttons, links etc based on the accesses they have. That access would be passed from a special method on my server, and I have to work out whether I just reprocess the current_ability to get it, or whether I write a long series of “can xxxx” calls to fill in a structure that I pass back to Angular.

    A bit murky as a description, I plan to write a post on it once I get around to actually implementing it.

  10. Very good stuff, kinda solved one of the problems I was having. I do have a question, the code on the controller shouldn’t be on a Service, such as user.js where registration, login and currentUser methods were implemented?

  11. Yes, I think that’s definitely an option.

    I’ve found that there’s a lot of flexibility in what you make a service and what you make a controller. Ultimately I decided that the logon functionality is only used in one place – on the login/registration pages, and therefore there wasn’t benefit in moving it to a service. But it perhaps would be cleaner if I did so.

    In my real application I moved a lot of the server handling functionality into services, particularly the error handling and displaying of error messages on the screen. But I still have the login/registration functionality baked into the login controller. 🙂

  12. Usually usernames are stored in the user or users table in the database, and can be found there. Passwords, however, need to be dealt with through password reset, you can’t recover out of the database.

    I don’t believe there is a devise feature that easily allows retrieval of username, since username (or at least e-mail, which I use for username) is the key that it uses – without that, it has no idea who you’re talking about.

    If you’re custom building a username retrieve, be very careful about security implications.

  13. Great post, I have an example where the login / logout code is moved into services. Happy to share. I also have a ‘hack’ that integrates Cancancan with Angular/Rails/Devise. Would you be interested in collaborate on this. It could be your next post.

  14. Glad it was useful to you. I’d be interested in the Cancan integration, I’ve done a subset for my real app, and haven’t done anything in the tutorial because it feels very specific to my needs and not so generally useful to everyone. If you have something more general I’d be interested to take a look.

  15. Paul, please write me at dj@fitbird.com and let’s discuss. I’ll fork my sample app and share it with you. I think it would benefit the community if we can add cancancan to use your sample app. I would also to see “remember me” and “password reset” added. I have started this as well.

  16. Pingback: [RAILS]: Для прочтения | shabanovblog

  17. Paul, thanks for an excellent ‘how-to’! Maybe you have an idea why I’m getting a below pasted error. I have both 401 and 500 error. 401 I supose I’ll handle easily (CSRF). But why am I getting undefined method `users_url’?
    Regards!

    Started POST “/users/sign_in.json” for 127.0.0.1 at 2014-10-27 14:24:49 +0100
    Processing by Devise::SessionsController#create as JSON
    Parameters: {“user”=>{“email”=>nil, “password”=>”[FILTERED]”}, “session”=>{“user”=>{“email”=>nil,
    “password”=>”[FILTERED]”}}}
    Completed 401 Unauthorized in 1ms
    Processing by Devise::SessionsController#new as JSON
    Parameters: {“user”=>{“email”=>nil, “password”=>”[FILTERED]”}, “session”=>{“user”=>{“email”=>nil,
    “password”=>”[FILTERED]”}}}
    Completed 500 Internal Server Error in 16ms
    NoMethodError (undefined method `users_url’ for #):

  18. Apologies for wrong question, my mistake. 401 is a ‘Unauthorised’ error, it has nothing to do with CSRF. Error 500 I was getting because I had no resources: users in my Rails routes.rb file.
    So I’ve rectified everything.
    However, I don’t know why I’m getting the 201 response if I try to log in with nonexisting user? I have server log below. We see it doesn’t finds the user, gives error 401 (which is ok) and then goes to process the request with ‘SessionsController#new as JSON’, sending the success 201 response?
    Regards.

    Started POST “/users/sign_in.json” for 127.0.0.1 at 2014-10-28 10:41:09 +0100
    Processing by Devise::SessionsController#create as JSON
    Parameters: {“user”=>{“email”=>”nonexisting@user.com”, “password”=>”[FILTERED]”}, “session”=>{“use
    r”=>{“email”=>”nonexisting@user.com”, “password”=>”[FILTERED]”}}}
    User Load (3.0ms) SELECT “users”.* FROM “users” WHERE “users”.”email” = ‘nonexisting@user.com’ ORDER BY “users”.”id” ASC LIMIT 1
    Completed 401 Unauthorized in 6ms
    Processing by Devise::SessionsController#new as JSON
    Parameters: {“user”=>{“email”=>”nonexisting@user.com”, “password”=>”[FILTERED]”}, “session”=>{“use
    r”=>{“email”=>”nonexisting@user.com”, “password”=>”[FILTERED]”}}}
    Completed 201 Created in 78ms (Views: 1.0ms | ActiveRecord: 0.0ms)

  19. typo

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

  20. Hi Paul, since we spoke last, I have replaced Cancancan with Pundit. The code is most cleaner and easier to maintain. I can recommend to into this as alternative to authorization.

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