Rolling Windows and Sticky Bytes

<tagline type="clever"/>

Devise Authentication, Internationalization and Mailed URIs

on 2012-07-11

For those who aren’t familiar with it, Devise is an authentication framework for Ruby on Rails. I was initially hesitant to use a plug-in for authentication for a few reasons.

  1. I was worried that the various authentication frameworks represent a “kitchen sink” approach to authentication and I wasn’t sure if I needed all the features. More complexity means higher probability of coding errors.
  2. Rails 3 makes it easy to roll your own via ActiveModel::SecurePassword[1]. I had initially decided based on reading up on Rails authentication strategies that this would be the best approach since it would give me the most flexibility.

Eventually I changed my mind and went for Devise because of the 11 features provided by the Devise toolkit, my site required 5 of them (Database Authenticable, Token Authenticable, Recoverable, Rememberable, Validatable) and most of the rest would be “nice to have”. Rather than spending time and effort rolling my own version of each of these, it seemed prudent to re-use existing, well tested code.

Having made the leap to Devise, I figured it would be worth writing down my experience.

I was able to install the gem without issue by adding it to my Gemfile and using bundler in the usual way (as documented in the “Getting Started” section on the Devise github page). The “devise install” run went equally smoothly (n.b.: only because I followed the instructions closely).

At this point, the next step is to add Devise to your model and this is where I started to run into some difficulties. This wasn’t because of any problem with Devise itself but because in my original design, I had specified the database field for e-mail addresses as “email_address” instead of “email” (which is what Devise expects). When I created a migration to rename the database field, I found that the indexes on the table had names too long to rename!

Lesson Learned #1: Do not accept the generated index names for database migrations if you are indexing a lot of fields in a table.

After I determined that I would have to fix up the index names, I modified the migration for the database field rename so that it would drop the index, rename the email_address field to email, then re-add the index with a shorter name. This worked out reasonably well.

I had to futz with the generated model attributes a bit (editing some attr_accessible entries and customizing the devise features) but the modifications were minor.

The next step was to try to add an authentication call to a page. There are two (easy) ways to do this:

  1. Apply authentication to all methods in your controller. To do this, add before_filter :authenticate_user! to the controller (somewhere inside class UsersController < ApplicationController but outside of any individual controller method).
  2. Apply authentication to individual methods in your controller. To do this, add authenticate_user! to the controller method you want to protect.

Which strategy you use will depend on whether the whole object needs to be behind the “authentication wall” or if some actions on an object can be run by anyone (e.g. if your site has a “posts” controller, you may want the software to allow anybody to view a post but only allow authenticated users to comment on one).

Note that (as stated in the documentation), the name of the method will be authenticate_model!, so if you have devise on your users model, the call will be authenticate_user!, if you are using an “admins” model, it will be authenticate_admin!, if you are using a “commenters” model, it will be authenticate_commenter!, etc.

This bit me at least once during my experimentation, with the software throwing a missing method error.

Lesson Learned #2: Make sure you’ve specified the right model name in authenticate_model! calls

Once I had the dev site putting up an authentication wall, I moved on to getting a reset link sent out for a test user and getting the password set… and that’s when all hell broke loose.

I clicked the “reset password” link and the e-mail was sent. All good!

When I visited the generated reset link I was… forwarded to a login page. I spent a couple of hours trying to diagnose the problem, struggling with it until I checked the logs and remembered that my application is internationalized. This turned out to be important.

On each page visit, the site will do its best to make sure it serves you in the appropriate language. It does this by taking the following steps:

  1. Enumerate the available locales in the application
  2. Ask your web browser for your most preferred locale based on the list of available locales
  3. Set the locale to a) the locale specified on the URI (to ensure all URIs are RESTful), b) the preferred locale specified by your browser or if all else fails, c) the default locale
  4. If the locale has changed, redirect the browser to the current URI with the correct locale specified as a parameter

Naturally it’s important to make sure that the locale is set correctly on every internal link to avoid a constant barrage of phony redirects. Back in the early days of development on the application, I made sure this would happen by setting the default url options in my application controller to include the current locale like so:

def default_url_options(options={})
  { :locale => I18n.locale }
end

Essentially what was happening when a user was hitting the “reset” URI provided in the e-mail message was that they were reaching a page without a locale set and being redirected. Because the software strips single-use security tokens from redirect URIs, they were being forwarded to a login page instead of the password reset page.

So why didn’t the URIs being sent by Devise have the locale set? Essentially because ActionMailer (which is used to generate the mail messages) does not inherit from the application controller, so the default url options specified there do not apply.

Lesson Learned #3: You have to set the default locale for ActionMailer URIs if you have internationalized your application

To fix the locale problem in my e-mailed URIs, I had to add a line to set the locale to the ActionMailer settings for my environments:

config/environments/development.rb
config.action_mailer.default_url_options = { :host => 'dev.example.com:3000', :locale => I18n.locale }
config/environments/production.rb
config.action_mailer.default_url_options = { :host => 'example.com', :locale => I18n.locale }

Once I had set the ActionMailer default URL options and restarted the application, password reset links worked like a charm and I was able to reset the password for a test account and log into the site with it.


3 Responses to “Devise Authentication, Internationalization and Mailed URIs”

  1. Gábor says:

    hey,

    there’s a small problem with your mailer’s default_url_option.
    the I18n.locale in the environment files happens before you set the current locale in the controller. so it always will be the default locale.

    please tell me if I missed something 🙂

    are your devise url-s ok?
    i had to define the default_url_options as self.default_url_options or the failed sign in redirected badly: /users/sign_in instead of /en/users/sign_in.

    G.

    • ianderson says:

      You are absolutely right. I just tested it and mail is going out with the default locale links in the mail are going out with the default locale. I’ll have to fix that.

      The application URIs are working correctly (apart from using the wrong locale) but I’m using parameters for locale instead of paths. Maybe that’s the difference?

      Edit: Turns out the mailer views are being localized but the URIs inside have the default locale set.

    • ianderson says:

      Since I had to generate individual localized views for the devise mailer anyway, I ended up setting the locale manually on the link in each view. Maybe not the most elegant solution but I don’t have that many locales anyway.

Leave a Reply

Your email address will not be published. Required fields are marked *