In this post I am explaining how to add authentication to a Rails application using the popular Devise gem.
Add the library
Use bundle add
to add Devise to the Gemfile
and install it in the
bundle:
$ bundle add devise
Next, run the Rails generator to add files that Devise needs with this command:
$ bin/rails g devise:install
create config/initializers/devise.rb
create config/locales/devise.en.yml
...
The generator will create the Devise initializer and other required files.
It will also print out some further instructions that need to be followed in order to set up Devise correctly.
Manual setup
Devise sends emails when certain events occur in the application. For example, when a new registration is created, or a password is reset, an email is sent with links back to the application.
It's important that a default mailer configuration is present so Devise can create the correct URLs for these links.
In the development environment I add a line similar to this to the configuration file:
# config/environments/development.rb
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
The host
and port
match what is actually used in development for
this specific application.
In the same way, in the production environment, I add the actual host for production, like so:
# config/environments/production.rb
config.action_mailer.default_url_options = {host: "ferrariwebdevelopment.com"}
Next, I need to make sure I have a root
route configured in my routes file.
Any route will do, as long as it's a root
route. For example, it could look
like this:
# config/routes.rb
root "articles#index"
I also need to add flash
messages in the application layout so Devise
can display its various messages to the user.
Initially, I can simply add this code to the layout:
# app/views/layouts/application.html.erb
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
As the application design evolves, I will replace this code with a Rails partial with
proper css
styles.
Lastly, if I need to customize the default Devise views, which happens in almost every application I set up, I can copy them from the gem into the application by running this generator:
$ bin/rails generate devise:views
The generator, that can be run at any time during development, will place the
Devise views in the app/views/devise
directory like so:
app/views/devise/
├── confirmations
│ └── new.html.erb
├── mailer
│ ├── confirmation_instructions.html.erb
│ ├── email_changed.html.erb
│ ├── password_change.html.erb
│ ├── reset_password_instructions.html.erb
│ └── unlock_instructions.html.erb
├── passwords
│ ├── edit.html.erb
│ └── new.html.erb
├── registrations
│ ├── edit.html.erb
│ └── new.html.erb
├── sessions
│ └── new.html.erb
├── shared
│ ├── _error_messages.html.erb
│ └── _links.html.erb
└── unlocks
└── new.html.erb
Creating the User model
The next step is to create the User
model, if I don't already have one.
This is the resource that will be authenticated with Devise. The model doesn't
have to be called User
, it can be called Admin
, Member
, or anything else that's
appropriate for the application.
For this purpose I use a Rails generator like so:
$ bin/rails generate devise User
invoke active_record
create db/migrate/20240216232702_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
This generator adds a migration that's already set up with the default database fields expected by Devise. Some sections of the migration file will be commented out. The reason for this is that Devise uses a number of modules that provide different functionalities.
These modules can be activated or deactivated through the model, and since inactive modules don't need to have fields in the database, they can be ignored in the migration.
So, before running the migration, I look at the model file and activate the Devise modules needed by my application. I also make sure that in the migration file the database fields match the corresponding active modules.
Once all this is set up, I can run the migration with:
$ bin/rails db:migrate
At this point, if the application is running I need to restart it, so it can pick up the Devise initializer.
Adding the Registration and Sign in links
Once Devise is configured it will have created a series of routes that we can use for signing up, or signing in, a user.
To allow a new user to register, we can add this link in the site navigation:
# app/views/layouts/application.html.erb
<%= link_to "Sign up", new_user_registration_path %>
Clicking on the link will take the user to the "new registration" screen. From here, the user can sign up by entering email and password.
After the registration, the user is automatically redirected to the home page. A "Welcome" message is also displayed.
In the same way, we should add the "Sign in" link to allow an already registered user to sign in:
# app/views/layouts/application.html.erb
<%= link_to "Sign in", new_user_session_path %>
Checking if a user is signed in
Devise provides a method to check if the user is signed in. This method is
called user_signed_in?
and we can use it in an if
statement to change the
links in the UI according to the user status.
If the user is signed in, we add a sign out link that will send a DELETE
request to destroy the user session.
If the user is not signed in, we will provide the "Sign up" and "Sign in" links.
If the user is signed in we will also provide a link for the user to edit their
own registration. To do that, we take advantage of the current_user
method
provided by Devise. The current_user.email
method call returns the user email.
The route to the edit screen for the user registration is provided by the
edit_user_registration_path
method.
<% if user_signed_in? %>
<%= link_to current_user.email, edit_user_registration_path %>
<%= link_to "Sign out", destroy_user_session_path, data: {turbo_method: :delete} %>
<% else %>
<%= link_to "Sign up", new_user_registration_path %>
<%= link_to "Sign in", new_user_session_path %>
<% end %>
Note that these Devise methods are named according to the resource that is
referenced. In this case, the resource is User
, so methods are called
current_user
, user_signed_in?
, new_user_registration_path
, and so on.
If we had a different resource, like Admin
for example, we would have methods
defined as current_admin
, admin_signed_in?
, or
new_admin_registration_path
.
Devise allows us to use multiple resources in the same application as well, so
we could have an Admin
and also a User
resource at the same time.
Authenticating the user
I am ready now to authenticate the user.
If I want to restrict access to some actions in a controller, I just add a call
to the before_action
method passing a symbol with a reference to the Devise
method to call for authentication, which is authenticate_user!
in my case.
Remember that this method takes its name from the resource we have to
authenticate. If the resource was Admin
the method would be
called authenticate_admin!
.
Since I only want to authenticate the user on certain actions in the controller,
I also pass a only:
option with an array listing the restricted actions.
# app/controllers/articles_controller.rb
before_action :authenticate_user!, only: [:new, :create, :edit, :update, :destroy]
Conclusion
This article explained how to authenticate a user with the popular Devise gem. It only touched the bases of authentication. Further articles will go into more details examining different aspects of authentication with Devise.
Photo by Life Of Pix