-
Notifications
You must be signed in to change notification settings - Fork 0
23 Authentication
It's really starting to shape up now, but there's no commenting, and no voting. In order to do those things right, there's a big prerequisite we need to complete: Authentication!
Right now, no one can log in. Everyone is an anonymous user. When it comes to authentication, there are a variety of existing libraries, as well as an infinite number of custom solutions. In this project, we're going to use one of the most popular pre-rolled solutions: Devise.
Devise includes a whole bunch of options, and frankly, an awful lot of magic. While the magic aspects make it really easy to implement, it's not necessarily a complement. Magic becomes a bad thing when you need to stray from the beaten path, or—heaven forbid—troubleshoot.
Nonetheless, Devise works for many people most of the time. As a Rails developer, you're almost certain to encounter it.
The first step is, naturally, to add Devise to our Gemfile
.
Gemfile
gem 'devise'
After that, rerun bundle
.
$ bundle
Unlike the other gems we've used, Devise require that we run a further install task.
$ bin/rails generate devise:install
This will create an initializer that runs every time you start the server, and a locale file where you can edit the messages Devise uses.
It will also output some important configuration information.
1. Ensure you have defined default url options in your environments files. Here
is an example of default_url_options appropriate for a development environment
in config/environments/development.rb:
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
In production, :host should be set to the actual host of your application.
Go ahead and open config/environments/development.rb. Let's add the appropriate configuration just below the existing config.action_mailer
setting.
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
Next:
2. Ensure you have defined root_url to *something* in your config/routes.rb.
For example:
root to: "home#index"
We have that one taken care of already.
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>
Again, we've taken care of that.
4. If you are deploying on Heroku with Rails 3.2 only, you may want to set:
config.assets.initialize_on_precompile = false
On config/application.rb forcing your application to not access the DB
or load models when precompiling your assets.
This one isn't a problem under Rails 4.
5. You can copy Devise views (for customization) to your app by running:
rails g devise:views
Putting the built-in views someplace handy sounds like a good idea. Let's do it.
$ bin/rails g devise:views
The generator creates a whole bunch of views in app/views/devise. We won't be looking at all of them, but we're happy to have them there.
The next order of business is to take a look at the initializer that Devise's install task generated: config/initializers/devise.rb. It describes much of Devise's functionality and lets us change a variety of Devise-specific settings.
When you see the following line, go ahead and change the fake email address to your own:
config/initializers/devise.rb
config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
There are a bunch of other bits of configuration to be found. I encourage you to look into more of them on your own, and be sure to check out the Devise project on GitHub.
Having dealt with those initial configuration items, it's time to create our User
model. Devise includes a generator that makes it dead-simple. It is possible to have multiple models for different types of users, but we have no need to do that.
$ bin/rails g devise User
invoke active_record
create db/migrate/20141106044900_devise_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
insert app/models/user.rb
route devise_for :users
Make particular note of the following:
- It created the
User
model for us. - It created a migration to create the
users
table. - It added routes.
Let's peek at the new model.
app/models/user.rb
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
Devise includes quite a few modules by default, as well as showing us our other options. For now, let's keep the defaults. You're encouraged to read more about these and other modules on your own.
Next, take a look at the generated migration. I won't include its contents here, but notice the included columns. Also notice, the columns that are commented out. These correspond to the modules that are not included by default.
One thing worth pointing out: There is no username
column, nor a name
field of any kind by default. Let's go ahead and add a username
column to the migration. Like email, we'll make it required.
t.string :username, null: false, default: ""
Let's also add a unique constraint on username
.
add_index :users, :username, unique: true
With that done, we're ready to migrate.
$ bin/rake db:migrate
== 20141106044900 DeviseCreateUsers: migrating ================================
-- create_table(:users)
-> 0.0180s
-- add_index(:users, :email, {:unique=>true})
-> 0.0029s
-- add_index(:users, :reset_password_token, {:unique=>true})
-> 0.0016s
== 20141106044900 DeviseCreateUsers: migrated (0.0227s) =======================
You can restart your server anytime. Note that if you pull up your app and browse around right now, you won't see any difference. You'll see something if you visit /users/sign_in, however!
Go ahead and click "Sign up" to see the new user registration form. When you're done entering an email and password, you should see a flash message telling you that you've signed up successfully. Doesn't get much easier than that, does it?
As mentioned previously, Devise is incredibly easy to set up when you're happy with the default behavior. We'd like a form field for our custom username
attribute, so we're going to have to get our hands a little dirtier.
We'll put the username
field right under email
. If you'd like to make the form look consistent with the other forms in the app by using fieldsets and CSS classes on the submit button, now is a good time to do that as well.
app/views/devise/registrations/new.html.erb
<fieldset><%= f.label :email %><br />
<%= f.email_field :email, autofocus: true %></fieldset>
<fieldset><%= f.label :username %><br />
<%= f.text_field :username %></fieldset>
...
<div><%= f.submit "Sign up", class: 'btn btn-default' %></div>
Let's add username to the edit user form as well.
app/views/devise/registrations/edit.html.erb
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
<div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
<% end %>
<fieldset><%= f.label :username %><br />
<%= f.text_field :username %></fieldset>
In addition to adding classes to the submit button on the edit form, I'm going to add them to the "Cancel my account" button. In this case, I'll use btn-danger
, rather than btn-default
. This will make it look red and scary.
app/views/devise/registrations/edit.html.erb
<p>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger' %></p>
Strong parameters will keep username
values from being saved until we permit mass assignment in the controller. Frankly, that's a pain-in-the-neck with Devise's controllers. Here's a good explanation.
We're going to resolve this using what the Devise documentation calls "the lazy way™", using devise_parameter_sanitizer
. Edit ApplicationController
.
Near the top, we'll add a before_action
that gets called if devise_controller?
returns true.
app/controllers/application_controller.rb
before_action :configure_permitted_parameters, if: :devise_controller?
The action, configure_permitted_parameters
, we'll define as a protected
method.
app/controllers/application_controller.rb
protected
def configure_permitted_parameters
devise_parameter_sanitizer.for(:sign_up) << :username
devise_parameter_sanitizer.for(:account_update) << :username
end
Let's also make username
required.
app/models/user.rb
validates :username, presence: true
Having done that, register another new user. Look for your new user via the console, and double-check that the username
is assigned correctly.
Now, we need a link to the sign-in page, so the URL doesn't have to be entered manually. We also need a way to logout. Since we're signed in right now, let's go ahead and figure out how to add a logout link.
Let's look at all of the routes that devise added for us.
$ bin/rake routes | grep devise
new_user_session GET /users/sign_in(.:format) devise/sessions#new
user_session POST /users/sign_in(.:format) devise/sessions#create
destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy
user_password POST /users/password(.:format) devise/passwords#create
new_user_password GET /users/password/new(.:format) devise/passwords#new
edit_user_password GET /users/password/edit(.:format) devise/passwords#edit
PATCH /users/password(.:format) devise/passwords#update
PUT /users/password(.:format) devise/passwords#update
cancel_user_registration GET /users/cancel(.:format) devise/registrations#cancel
user_registration POST /users(.:format) devise/registrations#create
new_user_registration GET /users/sign_up(.:format) devise/registrations#new
edit_user_registration GET /users/edit(.:format) devise/registrations#edit
PATCH /users(.:format) devise/registrations#update
PUT /users(.:format) devise/registrations#update
DELETE /users(.:format) devise/registrations#destroy
Logging out in devise uses the controller action devise/sessions#destroy
. Given that's it's a destroy
, you may correctly guess that requests must be made using the HTTP verb DELETE
. Thankfully, we have experience with such things.
We'll add our logout link to the <header>
.
app/views/layouts/application.html.erb
<header>
<%= link_to 'Redly', posts_path %>
<div id="session_actions">
<%= link_to 'logout', destroy_user_session_path, method: :delete %>
</div>
</header>
And we'll add a little style.
app/assets/stylesheets/style.scss
#session_actions {
position: absolute;
right: 0px;
bottom: 0px;
background-color: #EFF7FF;
padding: 4px;
margin-bottom: 10px;
line-height: 12px;
border-top-left-radius: 7px;
font-size: x-small;
}
Give the new link a try! You should see a "Signed out successfully" flash message. You still see the logout link in the header, when you obviously ought to see "login" instead. You may have noticed in the output of rake routes
that the path helper for the sign in page is new_user_session_path
. Let's add a link to that as well.
app/views/layouts/application.html.erb
<div id="session-actions">
<%= link_to 'logout', destroy_user_session_path, method: :delete %>
<%= link_to 'login', new_user_session_path %>
</div>
That works, but it's silly to a "logout" link when you're already logged out, or a "login" link when you're already logged in. Thankfully, Devise gives us a user_signed_in?
helper.
app/views/layouts/application.html.erb
<div id="session-actions">
<% if user_signed_in? -%>
<%= link_to 'logout', destroy_user_session_path, method: :delete %>
<% else -%>
<%= link_to 'login', new_user_session_path %>
<% end -%>
</div>
Now you should only see one link at a time. But wouldn't it be swell to also offer logged out users a link straight to the new user registration page?
app/views/layouts/application.html.erb
<div id="session-actions">
<% if user_signed_in? -%>
<%= link_to 'logout', destroy_user_session_path, method: :delete %>
<% else -%>
want to join? <%= link_to 'login', new_user_session_path %>
or <%= link_to 'register', new_user_registration_path %>
<% end -%>
</div>
And, say, shouldn't well tell users who they're logged in as? Devise provides a current_user
helper that simplifies that task.
app/views/layouts/application.html.erb
<% if user_signed_in? -%>
<%= current_user.username %>
<span class="separator">|</span>
<%= link_to 'logout', destroy_user_session_path, method: :delete %>
<% else -%>
Even better, let's make that a link to the edit form.
app/views/layouts/application.html.erb
<% if user_signed_in? -%>
<%= link_to current_user.username, edit_user_registration_path(current_user) %>
<span class="separator">|</span>
<%= link_to 'logout', destroy_user_session_path, method: :delete %>
<% else -%>
Try all of these links out, and just generally play around with what we've implemented for authentication. When you're satisfied, commit your work.
$ git add .
$ git commit -m "Use Devise for authentication."