Post navigation

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:

Provision of a registration page that matches the Devise expectations and calls the Devise register method and password change method

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

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

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

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:

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.

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:

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.

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:

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:

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:

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:

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:

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:

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:

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:

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

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

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.

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.

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

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.

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

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.