天天看點

Devise Authentication Recover Password by Email Devise Authentication Recover Password by Email (Code Kata)

Devise Authentication Recover Password by Email (Code Kata)

[Updated: August 3, 2011]

Once in a while, you might get a long-running application problem which defeats all attempts to resolve. For example, perhaps your Devise-enabled application running on Heroku simply will not allow users to recover their passwords via email. Perhaps it even crashes when users attempt to recover their passwords.

And of course your application is sufficiently complex to derail simplistic debugging techniques.

There has to be a better way!

Instead of getting all bogged down in the gory details…

Working from a minimal known-good application provides three benefits:

  1. The working application can be compared to the non-working application to (hopefully) find the problem by comparison, and
  2. Problems in a minimal application are much easier to find and fix.
  3. It’s easier to get both the big picture on a tiny application, and understand how all the pieces fit together.

Sounds great!

Here’s the goal: A minimal Devise-enabled Rails 3.1 micro-application which allows users to recover passwords.

Here’s what we need to do:

  1. Generate a minimal Rails application
  2. Install Devise and generate a minimal model for user authentication
  3. Build a minimal set of views and controllers to handle routing
  4. Configure the mailer
  5. Test test test

Devise, minimal

First off, we’re doing this in Rails 3.1 and on Heroku’s Celadon Cedar stack. Heroku is making a concerted effort to ephemeralize their hosting by using as much off-the-shelf technology as possible, so if you are not using Heroku, it’s not difficult to replicate their stack for yourself.

You might want to read these articles first:

  • Rails on localhost Postgres
  • Migrating Rails from Bamboo to Cedar

Given you now have a minimal application running locally and on Heroku and using Postgres, let’s install Devise:

$ vi Gemfile  # add gem 'devise'
$ bundle install
$ rails generate devise:install
           

Follow the instructions after Devise installs itself, ensuring you have the environments url set correctly.

$ rails generate devise:install
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml

===============================================================================

Some setup you must do manually if you haven't yet:

  1. Setup default url options for your specific environment. Here is an
     example of development environment:

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

     This is a required Rails configuration. In production it must be the
     actual host of your application

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root :to => "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

===============================================================================
           

Let Devise handle users

Next, create the Devise model:

$ rails generate devise User
$ rake db:migrate # did you create the postgres role?
$ rake db:test:prepare
           

Important: Devise inverts the “natural” order of user model development, where the User model is created first, stocked with various attributes (Name, URL, etc.), then the authentication is added to the User model. In Devise, the user authentication is the model. If you attempted to create a Devise User after creating the application User, the migration will fail.

Instead of adding a large number of attributes to the User model, constrain User to authentication, authorization and session management.

Bonus for Riding Rails readers: I haven’t seen this anywhere else, but it turns out that if you would like to add a user name to the default Devise model, it’s easy:

$ rails generate devise User name:string
           

Spiffy! Beats creating another migration to add 

:name

.

Put all the other details into a Profile, where each user 

has_one :profile

 and each profile 

belongs_to :user

.

If you want to be evil, keep the profiles for marketing purposes when users delete their accounts. The benefit to the user, of course, is that when they realize their `mistake’ and create a new account, their profile is ready and waiting for their return.

Moving right along…

More configuration

For the next few sections, we’re going to lean heavily on Daniel Kehoe’s superb tutorial on Rails with Devise. In fact, that tutorial is worth working through a few times itself. What we’re doing here is similar, but constrained to Rails 3.1, and we’ll be doing more testing.

In 

config/application.rb

, add 

config.filter_parameters += [:password, :password_confirmation]

In 

app/models/user.rb

, add

attr_accessible :name, :email, :password, :password_confirmation, :remember_me

  validates :name,  :presence => true, :uniqueness => true
  validates :email, :presence => true, :uniqueness => true
           

Views

At this point, your application will not run “out-of-the-box” because it has no views. An easy way to get views is to Devise’s built-in views:

rails generate devise:views
           

This copies over Devise’s internal views so that you can modify them as you see fit. Go ahead and do that, then let’s take a look at the routes:

$ rake routes
        new_user_session GET    /users/sign_in(.:format)       {:action=>"new", :controller=>"devise/sessions"}
            user_session POST   /users/sign_in(.:format)       {:action=>"create", :controller=>"devise/sessions"}
    destroy_user_session DELETE /users/sign_out(.:format)      {:action=>"destroy", :controller=>"devise/sessions"}
           user_password POST   /users/password(.:format)      {:action=>"create", :controller=>"devise/passwords"}
       new_user_password GET    /users/password/new(.:format)  {:action=>"new", :controller=>"devise/passwords"}
      edit_user_password GET    /users/password/edit(.:format) {:action=>"edit", :controller=>"devise/passwords"}
                         PUT    /users/password(.:format)      {:action=>"update", :controller=>"devise/passwords"}
cancel_user_registration GET    /users/cancel(.:format)        {:action=>"cancel", :controller=>"devise/registrations"}
       user_registration POST   /users(.:format)               {:action=>"create", :controller=>"devise/registrations"}
   new_user_registration GET    /users/sign_up(.:format)       {:action=>"new", :controller=>"devise/registrations"}
  edit_user_registration GET    /users/edit(.:format)          {:action=>"edit", :controller=>"devise/registrations"}
                         PUT    /users(.:format)               {:action=>"update", :controller=>"devise/registrations"}
                         DELETE /users(.:format)               {:action=>"destroy", :controller=>"devise/registrations"}
                    root        /                              {:controller=>"users", :action=>"index"}
           

Yes, this code listing is truncated at the edge of the page. And yes, you should type this out in your terminal window to see the controllers. Narrow web page, wide terminal. Nice for a change, right? Think about it…

Let’s dig a little deeper into this routing.

Default routing in Devise

So far, we have installed a “default” Rails 3.1 application using Devise’s default configuration for authentication. Examining 

config/routes.rb

, we find

Pgtest::Application.routes.draw do
  devise_for :users
.
.
end
           

Assuming you’re running Rails on the default 

http://localhost:3000

, hit these links and see what happens:

  • http://localhost:3000/

    : routing error, No route matches [GET] “/”.
  • http://localhost:3000/users

    : routing error, No route matches [GET] “/users”.
  • http://localhost:3000/users/sign_up

    : renders sign up page.
  • http://localhost:3000/users/sign_in

    : renders sign in page.
  • http://localhost:3000/users/sign_out

    : routing error, No route matches [GET] “/users/sign_out”.

This is as it should be, Devise doing the heavy lifting, while staying out of our application’s domain.

Further, note the routing errors come in two flavors:

  1. GET requests which are not now (but will later be) routed to a view, and
  2. POST, DELETE and PUT requests which are not supposed to render. These requests – HTTP verbs in RESTful routing – get redirected somewhere sensible after the action is performed.

Unfortunately, we are going to have to do more work before we can investigate retrieving forgotten passwords.

Minimal views

We need a few views to drive the application. Let’s start with signing up, click here: http://localhost:3000/users/sign_up

Put in your details, click Sign Up, and get for your effort:

undefined local variable or method `root_path' for #<Devise::RegistrationsController:0x00000100fe4880>
           

Ok, this is boring, back to Daniel’s tutorial to fill out the application skeleton.

In 

app/views/devise/registrations/edit.html.erb

:

<p><%= f.label :name %><br />
    <%= f.text_field :name %></p>
           

In 

app/views/devise/registrations/new.html.erb

:

<p><%= f.label :name %><br />
    <%= f.text_field :name %></p>
           

Now issue 

$ rails generate controller home index

In 

config/routes.rb

 replace:

get "home/index"
           

with

root :to => "home#index"
           

Add to 

controllers/home_controller.rb

:

def index
  @users = User.all
end
           

Modify the file 

app/views/home/index.html.erb

 and add:

<h3>Home</h3>
    <% @users.each do |user| %>
    <p>User: <%= user.name %> </p>
    <% end %>
           

Create initialization file 

db/seeds.rb

 by adding:

puts 'SETTING UP DEFAULT USER LOGIN'
user = User.create! :name => 'First User', :email => '[email protected]', :password => 'please', :password_confirmation => 'please'
puts 'New user created: ' << user.name
           

Whence:

$ rake db:seed
           

If you need to, you can run 

$ rake db:reset

 to recreate everything from scratch.

Controllers

$ rails generate controller users show
           

Note that “users” is plural when you create the controller.

Open 

app/controllers/users_controller.rb

 and add:

before_filter :authenticate_user!

def show
  @user = User.find(params[:id])
end
           

The file 

config/routes.rb

 has already been modified to include:

get "users/show"
           

Remove that and change the routes to:

root :to => "home#index"
devise_for :users
resources :users, :only => :show
           

Open 

app/views/users/show.html.erb

 and add:

<p>
      User: <%= @user.name %>
    </p>
           

Now modify the file app/views/home/index.html.erb to look like this:

<h3>Home</h3>
    <% @users.each do |user| %>
    <p>User: <%=link_to user.name, user %></p>
    <% end %>
           

In the 

app/assets/stylesheets/application.css

 file:

ul.hmenu {
  list-style: none; 
  margin: 0 0 2em;
  padding: 0;
}
ul.hmenu li {
  display: inline;  
}
#flash_notice, #flash_alert {
  padding: 5px 8px;
  margin: 10px 0;
}
#flash_notice {
  background-color: #CFC;
  border: solid 1px #6C6;
}
#flash_alert {
  background-color: #FCC;
  border: solid 1px #C66;
}
           

Adding session management links.

We want our users to be able to sign in and sign out, so we’ll need to provide links. An easy way to do this is following the Devise documentation for signing in and out.

First, make a partials directory for convenience:

$ mkdir app/views/devise/menu/
           

Then open 

app/views/devise/menu/_login_items.html.erb

 and add:

<% if user_signed_in? %>
   <li>
   <%= link_to('Logout', destroy_user_session_path, :method => 'delete') %>        
   </li>
 <% else %>
   <li>
   <%= link_to('Login', new_user_session_path)  %>  
   </li>
 <% end %>
           

Create the file 

app/views/devise/menu/_registration_items.html.erb

 and add:

<% if user_signed_in? %>
   <li>
   <%= link_to('Edit account', edit_user_registration_path) %>
  </li>
 <% else %>
   <li>
   <%= link_to('Sign up', new_user_registration_path)  %>
   </li>
 <% end %>
           

Then use these partials in your 

app/views/layouts/application.html.erb

 file, like this:

<body>
     <ul class="hmenu">
     <%= render 'devise/menu/registration_items' %>
     <%= render 'devise/menu/login_items' %>
   </ul>
   <%- flash.each do |name, msg| -%>
     <%= content_tag :div, msg, :id => "flash_#{name}" if msg.is_a?(String) %>
   <%- end -%>
 <%= yield %>
 </body>
           

That’s it for the framework.

Getting email unscrewed

First configure Devise email in 

config/initializers/devise.rb

:

config.mailer_sender = "[email protected]"
           

If you continue to get an error message, did you reboot the server?

Localhost email testing

Do this in the shell you’re using to run the Rails server:

$ export GMAIL_SMTP_US[email protected]
$ export GMAIL_SMTP_PASSWORD=yourpassword
           

Add the following to 

config/environments/development.rb

:

config.action_mailer.default_url_options = { :host => 'localhost:3000' }
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
config.action_mailer.default :charset => "utf-8"


ActionMailer::Base.smtp_settings = {
  :address => "smtp.gmail.com",
  :port => 587,
  :authentication => :plain,
  :domain => ENV['GMAIL_SMTP_USER'],
  :user_name => ENV['GMAIL_SMTP_USER'],
  :password => ENV['GMAIL_SMTP_PASSWORD'],
}
           

Remote host

As recommended, you do have your application running on Heroku, right?

$ heroku config:add [email protected]
$ heroku config:add GMAIL_SMTP_PASSWORD=yourpassword
           

Add these lines to 

config/environments/production.rb

:

config.action_mailer.default_url_options = { :host => 'herokuapp.com' }
# ActionMailer Config
# Setup for production - deliveries, no errors raised
config.action_mailer.delivery_method = :smtp
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
config.action_mailer.default :charset => "utf-8"

ActionMailer::Base.smtp_settings = {
  :address => "smtp.gmail.com",
  :port => 587,
  :authentication => :plain,
  :domain => ENV['GMAIL_SMTP_USER'],
  :user_name => ENV['GMAIL_SMTP_USER'],
  :password => ENV['GMAIL_SMTP_PASSWORD'],
}
           

If these parameters look like they could be refactored, they probably could be. Good opportunity to extend and maintain this article.

At this point the application works for me. I can:

  1. Recover passwords using email from my localhost development server, and
  2. recover passwords from a production server running on Heroku.

Now, I’ll compare the configuration files of this application with the configuration files of the non-working application currently deployed on Heroku. It’s “the long way ’round,” but as a result, I also have a better understanding of how to use Devise effectively. If you work this whole article line-by-line, you will understand much better too.

Mission accomplished!

(Sort of. We didn’t do any testing…)

Testing

Technically, BDD/TDD requires the tests for all this functionality be written first, then the code written to pass the tests. This is all well and good when the programmer has a good grip on both the code which is to be written, and how to test that code.

When neither apply, as was the case for the author when this was first written, working code is developed in a “spike.”

Now that this code is working, a suite of relevant tests should be developed. In the best possible world, after the tests are developed, the entire spike should be trashed and all the code rewritten in red-green form. Since this is a code kata, either this article will be rewritten as test-first, or a companion article will be written to illustrate the test first principles in action. For now, let’s figure out what to test, and about how best to test it.

Actually, we’re at 1800 words, so let’s instead quickly recap and call this one finished. Watch another article in a series, or a rewrite of this to be more BDD/TDD oriented.

Recap

This was a longer piece of work than is comfortable in this format, with a lot of files being touched.

Here’s a list of the files which were modified or created manually:

  1. config/routes.rb

  2. config/application.rb

  3. config/initializers/devise.rb

  4. config/environments/development.rb

  5. config/environments/production.rb

  6. app/assets/stylesheets/application.css

  7. app/views/home/index.html.erb

  8. app/views/users/show.html.erb

  9. app/views/layouts/application.html.erb

  10. app/views/devise/registrations/new.html.erb

  11. app/views/devise/registrations/edit.html.erb

  12. app/views/devise/menu/_login_items.html.erb

  13. app/views/devise/menu/_registration_items.html.erb

  14. app/models/user.rb

  15. app/controllers/home_controller.rb

  16. app/controllers/users_controller.rb

(Please leave a comment if you find a file missing. Thanks.)

Here’s an interesting conundrum. It’s customary to start with a bottom-up approach in TDD by writing unit tests in a Fat Model Skinny Controller paradigm, without worrying overmuch about the view layer. Here, out of 16 files, there is only one model, and by itself it doesn’t much of anything interesting. We can (and will) write some model tests exercising the callbacks and validation, but that doesn’t much help test the behavior of the micro-application as a whole.

In this case, we’re working top down, which makes sense given the task at hand.

One last point: this example application isn’t minimal. It could be shortened by removing the name attribute and not adding the css or menu links.

From around the Rubysphere…

  • Here’s a great link: Devise with locking and confirmation