With over 20,000 GitHub stars and lots of integrations, the Devise gem is one of the most popular gems in the Ruby landscape. So why would we term it one of Ruby's "hidden" gems? Well, as popular as it is, most developers only scratch the surface of the library's capabilities.
In this two-part series, we'll take a deep dive into Devise.
In this first part, we'll learn some of the basics, including:
- What Devise is and why you should use it in the first place, including some situations where it's advisable not to use it.
- How to install Devise and use it in your project.
- How to customize the library for your project.
In part two, we'll look at more advanced usages of Devise, including:
- OmniAuth and using Devise for API-only applications.
- Integrating Devise with an authorization library.
Let's get started!
Pre-requisites
In this tutorial, we'll use a simple Ruby on Rails 7 application featuring users and tasks. Users can register, log in, and log out, with access to create
, read
, update
, and delete
actions for tasks depending on their assigned role.
We'll use this app to progressively build ever more complex features and, in the process, show off Devise's powerful features in action.
Let's kick off with a brief introduction to Devise.
Introducing Devise for Ruby
Devise is an authentication library built on top of Warden, a Rack-based authentication framework.
Warden handles user sessions using secure session strings to verify the identities of logged-in users. It also handles users who are not logged in to ensure they cannot access restricted resources.
But since Warden is purely Rack-based, it does not add controller actions, views, helpers, or any other configuration options necessary for building a proper user authentication solution. Devise, on the other hand, does.
Another notable feature of Devise is its modularity. The library comes with around 10 modules which allow you to specify exactly how you want authentication handled in your application. You don't need to use all 10 modules. Instead, you activate and use only what you need for your app. We'll go over these modules later, which include the Registerable
module, Omniauthable
, Trackable
, and others.
With that in mind, let's start building our Tasks app and install Devise.
Getting Started: Installing Devise
Generate a new Rails 7 app using bundle exec rails new tasks_app
. Alternatively, just pull our example app's code from the repo.
Ensure you're in the app's root directory, then run bundle add devise
. This will add the Devise gem to your app's Gemfile. After this, run bundle exec rails g devise:install
to install Devise and generate its initializer file (we'll take a closer look at this file when we go through Devise's modules).
When you run this command, the generator will also give you a few setup suggestions for Devise to work as expected. Just follow the given instructions, and you should be good to go.
Next, let's generate a user model that Devise will work with.
Generating a Devise User Model
Devise needs a user model. What you call this model is totally dependent on your preferences and your app's use case, but it's usually either User
or Admin
.
Go ahead and generate a Devise user model with bundle exec rails g devise User
. This will generate a User
model under app/models/user.rb
, a migration file to create the user table, and a route for accessing the user resource:
# app/config/routes.rb
Rails.application.routes.draw do
devise_for :users
root "home#index"
end
Run bundle exec rails db:migrate
to run the migration and set up the user table. With that done, we now have a user model we can use for all things authentication.
Next, let's set up a Task model to use moving forward.
Generating a Task Model
Remember, using Devise, our simple app needs to:
- Enable a user to register.
- Enable a user to sign in and out of the app.
- Enable a user to create a Task that will belong to them.
- Allow (or disallow) a user from interacting with a Task, depending on their role.
With that in mind, let's now generate the Task model that users will interact with:
bundle exec rails g scaffold Task user:references title body:text status:integer
Now we should have a Task model associated with the user who created it, a title, a body, and a status (showing whether it's done or not).
After that, run bundle exec rails db:migrate
to create the tasks table. Also ensure that you relate a Task to a User:
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :tasks, dependent: :destroy # add this line
end
When you run the scaffold generator, the relation belongs_to :users
will automatically be added to the Task model. All the same, edit the Task model to use an enum
for the status as shown below:
# app/models/task.rb
class Task < ApplicationRecord
belongs_to :user
enum :status, { draft: 0, underway: 1, done: 2, archived: 3 }
end
With that, we are now ready to dive deeper into Devise. Let's start with a brief overview of Devise's modules.
Modules in Devise for Ruby
As we mentioned in the introduction, one of Devise's key features is its modularity. But what does "modularity" actually mean, in this case? It simply means separating the authentication process into different parts to be managed independently.
The latest version of Devise (at the time of writing) contains 10 modules:
- Database authenticatable - This module will take a user-supplied password and convert it into a secure hash which is then stored in the database. The module also handles verification when a user signs in.
- Omniauthable - Enables OmniAuth authentication.
- Lockable - This module will lock an account depending on the number of failed logins. The account is made accessible again after a certain time period or via email.
- Trackable - Tracks the number of logins an account has made, the IP addresses used, and the login timestamps.
- Confirmable - Sends an email with confirmation instructions when an account is registered and will also check if a user's account is confirmed when they log in.
- Registerable - Allows users to register an account on your app, and handles account editing and destruction.
- Recoverable - Handles password resets and account recovery.
- Timeoutable - Expires user sessions after a specified amount of time has elapsed.
- Validatable - This lets you define custom validation rules for user-supplied emails and passwords during account creation.
- Rememberable - Uses a cookie to remember a user during authentication.
For more information, Devise's documentation on modules will serve you well. For now, we'll switch to Devise helpers and filters.
Devise Helpers and Filters
One of the reasons to use an authentication library like Devise is to manage access to controller resources and associated views. Devise comes loaded with a set of convenient helpers, which include:
-
user_signed_in?
- Checks whether there is a currently logged-in user. -
current_user
- Allows you to reference the currently logged-in user. For example, you can use a code snippet likecurrent_user.email
to fetch the currently logged-in user's email. -
user_session
- For the currently logged-in user's session. -
destroy_user_session_path
- Destroys a logged-in user's session and redirects to a specified path or the root path. -
new_user_session_path
- Responds with a user login view. -
edit_user_registration_path
- Gives the currently logged-in user access to a view to edit their registration details. -
new_user_registration_path
- Responds with a view that has a registration form for registering new users.
That's about it for the helpers. To manage controller access, Devise gives you a nifty before_action
filter you can use like this:
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :authenticate_user! # if your model is called "User"
before_action :authenticate_member! # if your model is called "Member"
...
end
With that, let's now shift focus to how Devise integrates with Rails' strong parameters.
Devise and Ruby on Rails' Strong Parameters
Strong parameters is a well-known Ruby on Rails feature that prevents the mass assignment of request parameters to objects. Strong parameters require that request parameters be explicitly declared before any assignment is done, usually at the controller level.
Devise also mirrors this functionality by requiring parameters to be explicitly defined under appropriate Devise-specific controller actions:
-
sign_up
- By default, this is found in the Devise controllerDevise::RegistrationsController#create
. The keys allowed by default areemail
,password
, andpassword_confirmation
. -
sign_in
- This is found in the controllerDevise::SessionsController#new
, with the default allowed authentication keysemail
andpassword
. -
account_update
- Found inDevise::Registrations#update
— the authentication keysemail
,current_password
,password
, andpassword_confirmation
are allowed.
The most convenient way to add your own authentication keys is to use a before_action
filter in ApplicationController
, like here, where we add a username
key that is required when a user signs up:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
end
end
An interesting use case for Devise's strong parameters is when you need to pass an array of keys to the Devise sanitizer. For example, let's say we have a freelancer marketplace app where a user can be a "contractor" for hire, or an "employer" who can hire other contractors, or a contractor and employer at the same time.
Without going into too many technical details, we could achieve this functionality by allowing users to update their account using two select boxes for the respective roles — "contractor" and "employer" — so that a user can choose either or both options.
A quick way of achieving this is to use an array with the roles as keys, but Rails' strong parameters only allow for the following scalar values: String
, Symbol
, NilClass
, Numeric
, TrueClass
, FalseClass
, Date
, Time
, DateTime
, StringIO
, IO
, ActionDispatch::Http::UploadedFile
, and Rack::Test::UploadedFile
. As you can see, arrays, hashes, and other objects are, by default, not permitted.
To use an array that permits multiple roles for our user, we can modify our code as shown below:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :configure_permitted_parameters, if: :devise_controller?
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:account_update) do |user_params|
user_params.permit({ roles: [] },
:email, :password, :password_confirmation, :current_password)
end
end
end
The Devise documentation on helpers covers the topic in more detail.
Next, we'll learn how to customize Devise's views and controllers.
Customizing Devise Views
Because Devise is built as a Rails engine, almost all of its components are pre-packaged. Good examples of this include the views for logging in, signing up, updating account details, resetting your password, etc. At some point in your journey to build an app, you may need to customize these built-in views to suit your needs.
The first step is to generate views using the command bundle exec rails g devise:views
. When you do that, the respective views are generated and placed in the app/views/devise
folder:
Let's use a practical example of customizing Devise's views. In the previous section, we learned how to add extra authentication keys to the Devise sanitizer. Let's now extend that example to add a username
field to the user registration view.
Since the username
field is not available to our default user model, we'll add it using a bundle exec rails g migration add_column_username_to_user username
migration. Run the command bundle exec rails db:migrate
to start the migration and add the column to the user table.
Next, open up the new user registration file and edit it as follows:
# app/devise/registrations/new.html.erb
<h2>Sign up</h2>
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
...
<!-- username field added -->
<div class="field">
<%= f.label :username %><br />
<%= f.text_field :username, autofocus: true %>
</div>
...
<% end %>
Generating Devise views this way is fine if you're working with all views. But say you only want to customize a couple of views. How can you do this? Well, you simply pass the generator command a list of the views you want by indicating the view flag bundle exec rails g devise:views -v sessions registrations
. In this case, this only generates views associated with the login/logout process and those that handle sign-ups.
Moving on, let's dig further into Devise customization by learning how to customize the library's controllers and routes.
Customizing Devise Controllers and Routes
Customizing Devise's views can only get you so far. If you want real customization, you need to go into the library's controllers and routes.
Let's say we want to send an email to our admin notifying them of every new user registration on the app. (This is not the ideal way to go about things, but let's go with this example to illustrate what we want).
To begin with, we need to modify the signup action of the Devise::RegistrationsController
. Normally, Devise's controllers are not directly accessible for editing, so, just like we did with the views, we can generate them like so:
bundle exec rails generate devise:controllers users
The last bit of that command is the scope - in this case, users
. If you want it to be something else, you can change it accordingly. When you are done, the generated controllers should be in a structure similar to this:
Next, open up the routes file and modify Devise's routes to reflect the changes in the controller structure:
# config/routes.rb
Rails.application.routes.draw do
resources :tasks
devise_for :users, controllers: {
sessions: 'users/sessions',
registrations: 'users/registrations'
}
root "tasks#index"
end
Now open up the newly generated Users::RegistrationsController
and modify it to send an email to the admin whenever a new user signs up:
# app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
...
def create
super do |resource|
# use a custom mailer to send an email to the admin
AdminNotifierMailer.user_signup_notification(resource).deliver_later
end
...
end
As you can see, with access to Devise's controllers and actions, you can modify your app's authentication flow in any way you want.
Coming Up Next: Advanced Use of Devise
In the first part of this two-part series, we covered the basics of the Devise gem. We learned about the Devise's different modules, as well as how to customize its views and controllers.
In part two, we'll dive into more advanced usages of Devise, including API authentication and how to use OmniAuth with Devise.
Until then, happy coding!
P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!
Top comments (0)