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