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:
- The working application can be compared to the non-working application to (hopefully) find the problem by comparison, and
- Problems in a minimal application are much easier to find and fix.
- 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:
- Generate a minimal Rails application
- Install Devise and generate a minimal model for user authentication
- Build a minimal set of views and controllers to handle routing
- Configure the mailer
- 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:
-
: routing error, No route matches [GET] “/”.http://localhost:3000/
-
: routing error, No route matches [GET] “/users”.http://localhost:3000/users
-
: renders sign up page.http://localhost:3000/users/sign_up
-
: renders sign in page.http://localhost:3000/users/sign_in
-
: routing error, No route matches [GET] “/users/sign_out”.http://localhost:3000/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:
- GET requests which are not now (but will later be) routed to a view, and
- 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:
- Recover passwords using email from my localhost development server, and
- 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:
-
config/routes.rb
-
config/application.rb
-
config/initializers/devise.rb
-
config/environments/development.rb
-
config/environments/production.rb
-
app/assets/stylesheets/application.css
-
app/views/home/index.html.erb
-
app/views/users/show.html.erb
-
app/views/layouts/application.html.erb
-
app/views/devise/registrations/new.html.erb
-
app/views/devise/registrations/edit.html.erb
-
app/views/devise/menu/_login_items.html.erb
-
app/views/devise/menu/_registration_items.html.erb
-
app/models/user.rb
-
app/controllers/home_controller.rb
-
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