Delve deeply into Devise - Introduce all the Modules
Tags: ruby, rails, tutorial, authentication
Published: 2023-06-24 14:29:55

## Introduction
If you're a Rails developer, I bet you have already heard of or used `devise`. If you really don't know it, I guess you're reading this article in the far future 😅. `devise` is a very comprehensive **user authentication** library for Rails and it's still true in 2023.

`devise` is very powerful but it's also notorious for being difficult to learn. It has too many features and there isn't a entry point to cut in. Despite that, I'd like to introduce `devise` starting from its 10 modules. These modules encapsulate 10 common features an authentication system may have. In addition, almost all functionalities of `devise`  are built around these 10 modules. I believe by knowing more about them, it will easier for anyone to harness `devise`.

In this article, I will enable all 10 modules on the `User` model one by one. I suggest reading this article sequentially and also following the codes in each chapter. You will build an application with the 100% released `devise`. If you're totally new to `devise` or even Rails, you can take a look at my other article to setup a simplest environment and play around with it first. {% embed https://dev.to/kevinluo201/how-to-setup-very-basic-devise-in-rails-7-55ia %}.

## Devise Modules
Let's have a brief view of all 10 modules.

1. [Database Authenticatable](#database_authenticatable): storing user's password as hash digest and allowing users to sign in
2. [Registerable](#Registerable): allowing new users to register
3. [Confirmable](#Confirmable): new users need to click the confirmation link in a confirmation email to activate their accounts.
4. [Validatable](#Validatable): check the username and password validity
5. [Recoverable](#Recoverable): "Forgot Password?" feature
6. [Rememberable](#Rememberable): the user session will be remembered so the users don't need to log in again after restarting the browser
7. [Timeoutable](#Timeoutable): the user session will expire after a period of time
8. [Lockable](#Lockable): lock/unlock users
9. [Trackable](#Trackable): track how users use the system, such as how many times a user has signed in
10. [Omniauthable](#Omniauthable): add Omniauth support

## Preparation
### Environment
* Ruby 3.1.2p20
* Rails 7.0.5
* devise 4.9.2

### Installation
Create a new rails project and install `devise`. Then execute the command below to initialize `devise`.
```bash
rails generate devise:install
```

### Add Mailer's default_url_options
Many features will send mails out and this is a required configuration.
```ruby
# config/environments/development.rb
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
```

### Add User model
We will use `User` model for authentication and we want to use the `email` field as the **identifier**.

To create the table in the database, execute:
```
rails g migration create_users email:index
```
It will create a migration file in `db/migrations`, please open that file and edit it like below to add some extra constraints like `NOT NULL` and `UNIQUE`:
```ruby
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :email, null: false
      t.timestamps
    end
    add_index :users, :email, unique: true
  end
end
```
Run `rails db:migrate` to make the changes.

Add the `User` model:
```ruby
# app/models/user.rb
class User < ApplicationRecord
end
```

#### Warning
Please be aware that in practice you should use
```bash
rails generate devise User
```
to create `User` with a basic devise configuration. However, this article's purpose is to introduce `devise`'s modules and enable them progressively.

### Add a root page
Let's add a root page so it can make testing more smooth.

1. Add a `PagesController` with an `index` action
```ruby
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
  def index
  end
end
```
2. Add the view for `index`
```html
<!-- app/views/pages/index.html.erb -->
<h1>Pages#index</h1>
<% if user_signed_in? %>
  <%= link_to 'Sign out', destroy_user_session_path, data: { turbo_method: :delete }
<% else %>
  <%= link_to 'Sign in', new_user_session_path %>
<% end %>
```
3. Modify the `<body>` of the layout so it can show flash messages
```html
<!-- app/views/layout/application.html.erb -->
<body>
  <p class="notice"><%= notice %></p>
  <p class="alert"><%= alert %></p>

  <%= yield %>
</body>
```

### Routes
Modify the routes like below.
```ruby
# config/routes.rb
Rails.application.routes.draw do
  root "pages#index"
  devise_for :users
end
```
`devise_for :users` will generate route paths for `devise` in the scope of `users`. It doen't do anything right now because we haven't enabled any module yet. We'll see more examples while introducing each module.

### Export Devise views
The last step is to export devise views. This is usually for customization.  In fact, we don't have to do this now because we're not going to modify them. It could be helpful to see those pages in code for better understanding.
```bash
rails generate devise:views
```

We have done all the prerequisites. At this point, if you start the rails server and go to `http://localhost:3000`, it should be a crashed page 😆. There's nothing to concern about. Let's start enabling those modules on `User`!

## Database Authenticatable <a name="database_authenticatable"></a>
The `:database_authenticatable` module provides 2 main functionalities to the `User` model:
1. Store the users' passwords in a **hash digest** format in the column, `encrypted_password`.
2. Verify whether a user's input matches the password digest stored in the database.

It should be a **required** module. To be honest, I don't know what will happen if you don't include this module but others. Why do you want to do that? If you really need to not include this module, you'd better think twice before using `devise`. 

### Enable the module
To enable `:database_authenticatable`, we need to add one column to the `users` table, `encrypted_password`. Execute
```
rails g migration add_encrypted_password_to_users encrypted_password
```
It will create a new migration file in `db/migrations`. Although it's a magic command, it's not magical enough😅 Please open the file and add `null: false` for the database `NOT NULL` constraint.
```ruby
class AddEncryptedPasswordToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :encrypted_password, :string, null: false
  end
end
```
Run `rails db:migrate` to make the changes

We can enable our first module in `User` by using the `devise` method:
```ruby
# app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable
end
```

### Usage
#### Headless usage
`:database_authenticatable` will introduce a method `#password=` for setting users password. Open the rails console:
```ruby
user = User.new(email: 'kevin@taiwan.com')
user.password = '123456'
user.save!
```
If you check `user.encrypted_password`, it'll be a gibberish string which is exactly the **hash digest** of `bcrypt` so no one knows the true password except you.
```ruby
user = User.last
user.encrypted_password
# "$2a$12$j.tv091dn9OQPV4seF74Z.PIlohxesFxMGuQh0l39hH4mFS5XyDTi"
```
We can use the method `#valid_password?` to verify if a password is correct.
```ruby
user.valid_password? '123456'
# => true
user.valid_password? 'abcde'
# => false
```

#### Testing in browsers
`:database_authenticatable` module will **unlock** 3 paths for users to log in and log out. (It really feels like playing a game🎮) You can check them by executing `rails routes`
```
    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
```
- `devise/sessions#new` is for the sign-in page
- `devise/sessions#create` is create the login session
- `devise/sessions#destroy` is for logging out

1. Now, you can start the rails server by `rails server` and go to `http://localhost:3000/` on a browser. You'll find the page is fixed now and can display successfully.
  ![home page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oo5lqjcnkv1f1umvh8nw.png)
2. Click the **Sign In** link and you will be led to `http://localhost:3000/users/sign_in` and you should see a Login page provided by `devise`
  ![Sign-in page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mvt9mjwtt8iy8bf86arn.png)
3. You can then input the user's credentials you just created in the rails console. You should log in successfully and be redirected to the root page.
  ![signed in](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ldmbt971crl2blgv16za.png)
4. You can try to click **Sign Out** and sign out.

### Customization
1. modify settings in `config/initializers/devise.rb`
2. modify the exported `app/views/devise/sessions/new.html.erb` which is for displaying the login page.
3. execute `rails generate devise:controllers users -c sessions` to export `SessionsController`. 

### Checkpoint - encrypted_password
`encrypted_password` is a misleading name because it actually uses `bcrypt` to calculate the **hash digest** of the password and stores that digest. I didn't check the git log but I guess it's a historical result. 

Again, it's okay if you don't understand what the previous paragraph means. I don't think a web developer has to know how the hash algorithm is implemented. However, knowing the difference between an encrypted password and a hashed password is more important. A very easy way to understand them is: you can decrypt an encrypted password to the original password but you cannot revert a hashed digest of a password. As a result, we usually think hashing a password is more secure.

## Registerable <a name="Registerable"></a>
The `:registerable` module is for new users to sign up and manage their data. I think this one is the most confusing one. It sounds like you need to include this module to create users but the fact is that you don't need this module to do that. We just created a new user in the previous chapter, right?

`:registerable` only adds some extra **routing endpoints** and makes some views show the links to go to the **signup page**. Even though we **include** this module in `User`, it doesn't add any feature to the model itself. 

`:registerable` can save you some time by preventing you from building a similar workflow again.

### Enable the module
Unlike `:database_authenticatable`, we can just enable `:registerable` module directly.
```ruby
# app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable
end
```

### Usage
After enabling `:registerable`, a series of `user_registration` paths will be generated:
```
cancel_user_registration GET    /users/cancel(.:format)  devise/registrations#cancel
   new_user_registration GET    /users/sign_up(.:format) devise/registrations#new
  edit_user_registration GET    /users/edit(.:format)    devise/registrations#edit
       user_registration PATCH  /users(.:format)         devise/registrations#update
                         PUT    /users(.:format)         devise/registrations#update
                         DELETE /users(.:format)         devise/registrations#destroy
                         POST   /users(.:format)         devise/registrations#create
```
I think showing examples is easier for you to understand what these paths do.

#### Create a user
If you go to `http://localhost:3000/users/sign_up`, you can see a Sign-Up page provided by `devise`:

![Sign-up page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1f9d028gtmcd4kq3ct51.png)

You can create a new user if you submit the form with valid input values.

#### Update or Delete a user
If you **logged in**, you can go to `http://localhost:3000/users/edit` to edit your information.

![Edit user info](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5errzmh4uj0ldym3rwv.png)

### Customization
If you want to customize it:
1. modify settings in `config/initializers/devise.rb`
2. modify the exported views of `app/views/devise/registration/*.html.erb`
3. execute `rails generate devise:controllers users -c registrations` to export `RegistrationsController`. 

### Checkpoint - links on pages
If you go to the Log-in page `http://localhost:3000/users/sign_in` now, you'll find a **Sign up** link that wasn't there.

![Sign-in page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7vbgoula8sxqdnf0dhak.png)

It feels like magic. What's happening? This is because the partial, `app/views/devise/shared/_links.html.erb` checks which modules are enabled and displays the required links. You can check it out to see the details.

## Confirmable <a name="Confirmable"></a>
I think `:confirmable` is the second most frequently used module of `devise`. It provides a very common behaviour: users need to validate their email addresses by clicking a confirmation **link** sent with a confirmation mail. It can prevent malicious users from signing up with fake emails.

### Enable the module
We need to add some required columns on `users` table for this module.
Execute
```bash
rails g migration add_confirmation_to_users confirmation_token:index confirmed_at:datetime confirmation_sent_at:datetime unconfirmed_email
```
and it should create a migration file like below
```ruby
class AddConfirmationToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :confirmation_token, :string
    add_index :users, :confirmation_token, unique: true
    add_column :users, :confirmed_at, :datetime
    add_column :users, :confirmation_sent_at, :datetime
    add_column :users, :unconfirmed_email, :string
  end
end
```
It will add 4 columns:
- `confirmation_token`: A randomly generated token will be stored in this column and it will be embedded in the confirmation link sent to the user.
- `confirmation_sent_at`: it records the time the confirmation mail was sent
- `confirmed_at`: it records the time after a user clicks the confirmation link and is confirmed. **A user has been confirmed if this column is not NULL**.
- `unconfirmed_email`: If a user wants to update his/her email address, it will store the new email address.

Run `rails db:migrate` to make the changes. Then add `:confirmable` in `User` to enable the module:
```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :confirmable
end
```

### Usage
#### Headless
`:confirmable` adds an array of methods. We're going to try some of them. Open the console:
```ruby
user = User.first
# check if the user is confirmed
user.confirmed?
# => false
```

You can call `#send_confirmation_instructions`
```ruby
user.send_confirmation_instructions
```
A confirmation mail with the confirmation link will be sent to the user's email address:
```
Date: Mon, 19 Jun 2023 12:37:09 -0400
From: please-change-me-at-config-initializers-devise@example.com
Reply-To: please-change-me-at-config-initializers-devise@example.com
To: kevin@taiwan.com
Message-ID: <6490843547bc2_f396c1c90348@F2XWD4WR0C.mail>
Subject: Confirmation instructions
Mime-Version: 1.0
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

<p>Welcome kevin@taiwan.com!</p>

<p>You can confirm your account email through the link below:</p>

<p><a href="http://localhost:3000/users/confirmation?confirmation_token=uyMSehFApDiMs_-yjcT7">Confirm my account</a></p>
```
Check the user's attributes now:
```ruby
user.confirmation_token
# => "uyMSehFApDiMs_-yjcT7"
user.confirmation_sent_at
# => Mon, 19 Jun 2023 16:37:09.185202000 UTC +00:00
user.confirmed_at
# => nil
user.unconfirmed_email
# => nil
```
1. `user.confirmation_token` matches the `confirmation_token` embedded in the email's confirmation link.
2. `user.confirmation_sent_at` was filled
3. `user.confirmed_at` is `nil` because the user hasn't clicked the confirmation link yet
4. `user.unconfirmed_email` is `nil` because we're not doing a reconfirmation

You can confirm the user by calling `#confirm`:
```ruby
user.confirm
user.confirmed?
# => true
```

If you want to do the reconfirmation for the user, you can save the new email in the `unconfirmed_email` first.
```ruby
user.email
# => "kevin@taiwan.com"
user.update! unconfirmed_email: 'kevin@taipei.tw'
user.pending_reconfirmation?
# => true
user.send_reconfirmation_instructions
# an email with the reconfirmation link will be sent
user.confirm
user.email
# => 'kevin@taipei.tw'
user.unconfirmed_email
# => nil
```

#### Use in browsers
1. You will not be able to log in as an unconfirmed user. You'll be rejected with a message saying you need to confirm the email first.
  ![Sign-in page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1elfa6zi2k4wwvfg954g.png)
2. If the user wants the system to send the confirmation mail again, they can click **"Didn't receive confirmation instruction?"** which will lead them to `http://localhost:3000/users/confirmation/new` to resend the mail.
  ![Resend confirmation mail](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bpkvfrsizqby8adoxxhl.png)
3. You can log in as normal after you click the confirmation link in the mail 

#### Customization
You can customize `:confirmable` by
1. modify settings in `config/initializers/devise.rb`
2. modify the mail template `app/views/devise/mailer/confirmation_instructions.html.erb`
3. modify the resend mail page, `app/views/devise/confirmations/new.html.erb`
4. run `rails generate devise:controllers users -c confirmations` to export `ConfirmationsController`

## Validatable <a name="Validatable"></a>
`:validatable` module provides validations for:
* `email`: check if the input email complies with the email's regex
* `password`: check the length of the input password is 6~128

That's it. It's a very simple module.

### Enable the module
`:validatable`  adds model validations for `email` and `password` so `users` needs to have both **methods**. We've already done that when enabling `:database_authenticatable` so we can just enable the module in `User`.
```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :confirmable, :validatable
end
```

### Usage
This is a very straightforward module. Please check the example below:
```ruby
user = User.new email: 'test-user'
user.password = '1234'
user.save
# => false
user.errors.count
# => 2
user.errors.each { |error| puts error.full_message }
# Email is invalid
# Password is too short (minimum is 6 characters)
```

### Customization
Please forgive me but I personally think it's **meaningless** to customize this module 😆. Adding validations is way too easy in Rails. For example, the code below can do exactly the same thing as `:validatable` does.
```ruby
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, length: { in: 6..128 }
```

If you want to customize the `:validatable`, I suggest you just stop using it. Use Rails validation instead and customize it by yourself.

## Recoverable <a name="Recoverable"></a>
`:recoverable` is another module that I use frequently in different projects. In a nutshell, it's the **"Forget Password?"** feature. Users can reset their passwords via a reset link sent to their email addresses.

### Enable the module
`:recoverable` needs some required columns. Execute
```bash
rails g migration add_recoverable_to_users reset_password_token:index reset_password_sent_at:datetime
```
to create a migration file:
```ruby
class AddRecoverableToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :reset_password_token, :string
    add_index :users, :reset_password_token, unique: true
    add_column :users, :reset_password_sent_at, :datetime
  end
end
```
It's pretty similar with `:confirmable`. When a user wants to reset a password, a randomly generated token will be stored in `reset_password_token` and it will be embedded in the link in the reset password mail sent to the user's email address. `reset_password_sent_at` will record the time the mail is sent.

We can enable `:recoverable` now:
```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :confirmable, :validatable, 
         :recoverable
end
```

### Usage
#### Headless
You can call `#send_reset_password_instructions` to create a `reset_password_token` and it will be embedded in the reset password mail.
```ruby
user.send_reset_password_instructions
# => 's5ZkHwPHQGAc7DSGCpR_'
```
Below is a sample of reset password mail. You can find the `reset_password_token` embedded in the link.
```
Date: Mon, 19 Jun 2023 21:21:18 -0400
From: please-change-me-at-config-initializers-devise@example.com
Reply-To: please-change-me-at-config-initializers-devise@example.com
To: kevin@taipei.tw
Message-ID: <6490ff0e7179_df83c1c193c8@F2XWD4WR0C.mail>
Subject: Reset password instructions
Mime-Version: 1.0
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

<p>Hello kevin@taipei.tw!</p>

<p>Someone has requested a link to change your password. You can do this through the link below.</p>

<p><a href="http://localhost:3000/users/password/edit?reset_password_token=s5ZkHwPHQGAc7DSGCpR_">Change my password</a></p>

<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>
```

#### Use in browsers
After enabling `:recoverable`, you'll get new series of routes endpoints for `/passwords`
```
new_user_password  GET    /users/password/new(.:format)  devise/passwords#new
edit_user_password GET    /users/password/edit(.:format) devise/passwords#edit
     user_password PATCH  /users/password(.:format)      devise/passwords#update
                   PUT    /users/password(.:format)      devise/passwords#update
                   POST   /users/password(.:format)      devise/passwords#create
```

We can open a browser to check what these paths do. 
1. Go to `http://localhost:3000/users/sign_in`, and you'll find a new link shows up, **Forgot your password?**
  ![Sign-in page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7m2fowrwdlevb9p1kk2u.png)
2. Click that link and you'll be led to `http://localhost:3000/users/password/new`
  ![Forgot password](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zu8dgigmt8vy9lixqh5o.png)
3. If you input a valid email of the users you created in the system and submit the form. A `POST` request to `http://localhost:3000/users/password` will be made. Then a **reset password mail** as described in the previous section will be sent out. You can find that mail in the terminal running `rails server`. If you have a gem like [letter_opener](https://github.com/ryanb/letter_opener), you can see that mail shown in your browser. 
4. Click or copy-paste the reset password link to open the reset password page `http://localhost:3000/users/password/edit?reset_password_token=xxxxxxxxxx`
  ![Reset Password](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mz0b8zv85mi6ykyhbdqx.png)
5. Input a new password and submit the form. A `PATCH/PUT` request will be made toward `http://localhost:3000/users/password`. It will then check the `reset_password_token` and then update the user's password.

### Customization
To customize the pages and workflow:
1. modify settings in `config/initializers/devise.rb`
2. Modify the views in `app/views/devise/passwords/*.html.erb`
3. Modify `app/views/devise/mailer/reset_password_instructions.html.erb` for the reset password mail
4. Execute `rails generate devise:controllers users -c passwords` to export `PasswordsController`

### Checkpoint - reset password token
If you're a scrupulous person, you might find one interesting thing. I mentioned above that the token will be stored in `reset_password_token` and will be embedded in the reset password link. However, if you really check their values, you'll find they're different. Take the example we used in this section, 
- the token embedded in the reset password link is `s5ZkHwPHQGAc7DSGCpR_`
- but if you check `user.reset_password_token`, it'll be `aab3acb2a7dc13e835394c85a14c37417bbd5837692ee67853961bbc6863ee01`

Why? How can `devise` verify the token if they are totally different? 

The answer is that the value stored in `reset_password_token` is also a **hash digest**, like how `devise` stores passwords. Furthermore, it will use **HMAC-SHA256** hash algorithm to calculate the hash digest. You can run the commands below to check the result.
```ruby
salt = Devise.token_generator.send(:key_for, :reset_password_token)
OpenSSL::HMAC.hexdigest('SHA256', salt, 's5ZkHwPHQGAc7DSGCpR_')
# => "aab3acb2a7dc13e835394c85a14c37417bbd5837692ee67853961bbc6863ee01"
```

Why does `:database_authenticatable` use **bcrypt** and `:recoverable` use **HMAC-SHA256**? The usage is pretty similar. It may be because of the speed, `bcrypt` is the most secure way to do hashing. 

To be frank, the true answer is I don't know. I don't think speed is that critical for resetting passwords. I guess this inconsistency may be common in an open-source project. Different groups of volunteers chose different approaches.

## Rememberable <a name="Rememberable"></a>
If you've logged in to the system, try restarting your browser. You'll be logged out. It's inconvenient. To solve this issue, `:rememberable` is introduced. This module provides the ability to keep the user session even if the browser is closed.

How does it do that? **Cookies**! Generally speaking, `:rememberable` records the current user's id in a cookie field when a user logs into the system. For instance, if a user with id `10` is logging in, it will record `user_id => '10'` in the cookie. When the system detects a cookie with `user_id => '10'`, it will assume that the user with id `10` is logged in. How long can it last? It depends on the cookie's **expiration time**. If the cookie expires after 1 week, then the user's session can be kept for 1 week.

Of course, we don't use a cookie just like that because anyone can modify a cookie's value. You don't want a user to change that `user_id` to an admin's id and suddenly become an admin. Don't worry. `:rememberable` also consider how to make it **tamperproof** and **verify** the cookie's value.

### Enable the module
`:recoverable` requires one more column. Execute
```
rails g migration add_rememberable_to_users remember_created_at:datetime
```
to create a migration file:
```ruby
class AddRememberableToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :remember_created_at, :datetime
  end
end
```
`remember_created_at` is used for storing the time a user logging with the "**Remember me**" checkbox checked.

We can enable `:rememberable` now:
```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :confirmable, :validatable,
         :recoverable, :rememberable
end
```

### Usage
`:rememberable` utilizes cookies to realize its functionality. Although it does add many methods for the `User` model, it doesn't make sense if we only call those methods in the rails console. Therefore, I only demonstrate how it works in browsers in this article. 

1. Go to `http://localhost:3000/users/sign_in`, you'll find there's a new **Remember me** checkbox
  ![sign-in page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xy110dqb3sveukzjtlbh.png)
2. Logging in with the checkbox checked, a parameter `rememeber_me` will be sent with the form and that will trigger `:rememberable` mechanism to remember your session in the cookie.
```ruby
"remember_me"=>"1"
```
3. After logging in, open your browser's devtools (I'm using Chrome) and you can find there a new key, `remember_user_token`, is added to the cookie. It should expire in 2 weeks because that's the default setting. The cookie value seems gibberish. It's actually a **signed message** and it's the **key** of the whole `:rememberable` functionality. It records the currently logged-in user's id and other helpful information. The rails system can take advantage of that information to **remember** the user's session.
  ![Cookie](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p225dhjvmb5upl3ubc1h.png)
4. With this key-value pair, you can try close and start your browser again. You'll find your session is kept. Nice! 🎉

### Customization
You can change the configuration of `:rememberable` in the initializer, for example, changing the longevity of the cookie to another time. The default is 2 weeks and you can change it to any length you want.
```ruby
# config/initializers/devise.rb
config.remember_for = 1.weeks
```

Even though there is some stuff `devise` allows us to customize `:rememberable`, I don't think it's meaningful to do that. The whole logic of `:rememberable` is encapsulated well and I can't see the need to change them. 

### Checkpoint1 - Signed message
For a curious mind like you, I think you must want to know what kind of message is really stored in `remember_user_token`. First, we copy-paste the gibberish cookie value from our previous example:
```
eyJfcmFpbHMiOnsibWVzc2FnZSI6Ilcxc3hYU3dpSkRKaEpERXlKRVpWT0VwWVRFOWxPVXh6TG5KRU1qbFBkME5HVFdVaUxDSXhOamczTXpVMU56VXlMakEzTXpBM05TSmQiLCJleHAiOiIyMDIzLTA3LTA1VDEzOjU1OjUyLjA3M1oiLCJwdXIiOiJjb29raWUucmVtZW1iZXJfdXNlcl90b2tlbiJ9fQ==--1c344cce076954bc63f16c7ae6213be0d39ceb6d
```

Let's demystify it. This message can be split into 2 parts, **the true message** and **the signature**, and they're separated by `--` in the string. The string before `--` is the message itself and the string after `--` is the signature. You can try to stop the program by `binding.break` (this is a functionality from `debug` gem)
```ruby
def index
  binding.break
end
```
The message is not encrypted, it's just encoded by base64 **twice** so we can decode it easily.
```ruby
cookie_value = cookies['remember_user_token']
#=> eyJfcmFpbHMiOnsibWVzc2FnZSI6Ilcxc3hYU3dpSkRKaEpERXlKRVpWT0VwWVRFOWxPVXh6TG5KRU1qbFBkME5HVFdVaUxDSXhOamczTXpVMU56VXlMakEzTXpBM05TSmQiLCJleHAiOiIyMDIzLTA3LTA1VDEzOjU1OjUyLjA3M1oiLCJwdXIiOiJjb29raWUucmVtZW1iZXJfdXNlcl90b2tlbiJ9fQ==--1c344cce076954bc63f16c7ae6213be0d39ceb6d
msg, signature = cookie_value.split('--')
rememberable_msg = Base64.decode64(msg)
# => "{\"_rails\":{\"message\":\"W1sxXSwiJDJhJDEyJEZVOEpYTE9lOUxzLnJEMjlPd0NGTWUiLCIxNjg3MzU1NzUyLjA3MzA3NSJd\",\"exp\":\"2023-07-05T13:55:52.073Z\",\"pur\":\"cookie.remember_user_token\"}}"
Base64.decode64(JSON.parse(rememberable_msg)['_rails']['message'])
# => "[[1],\"$2a$12$FU8JXLOe9Ls.rD29OwCFMe\",\"1687355752.073075\"]"
```
The result 
```ruby
[[1],"$2a$12$FU8JXLOe9Ls.rD29OwCFMe","1687355752.073075"]
```
is exact the
```ruby
[user_id, salt_of_password_hash_digest, timestamp]
```
My user's id was `1` so it recorded `1` in the first element of the message.

In fact, this signed cookie message is a functionality provided by Rails. You can get the value directly by `cookies.signed[]`. It parses the message correctly and also verifies the signature.
```ruby
cookies.signed['remember_user_token']
# => [[1], "$2a$12$FU8JXLOe9Ls.rD29OwCFMe", "1687355752.073075"]
```
The signature is generated based on the message's content. If anyone tries to change the message but doesn't know how to create the corresponding signature, the verification will be failed and it will return `nil`
```ruby
cookies['remember_user_token'] = cookies['remember_user_token'][2..-1]
cookies.signed['remember_user_token']
# => nil
```

### Checkpoint2 - SessionCookie
Did you notice there's also another cookie value, `_devise_modules_session`? That's used for storing the **session data**. It feels strange. Why do we need another cookie value to remember the session? Isn't the session already kept by that cookie value?

You can check the **Expires** column and it says `_devise_modules_session` is a **Session Cookie**. A session cookie will be destroyed after you close the browser.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#session_cookie

It's designed to be like this so developers can choose to enable `:rememeberable` depending on the requirements.

## Timeoutable <a name="Timeoutable"></a>
Users can keep their session forever if they don't close their browsers. That's a problem for some systems. `:timeoutable` will revoke a user's session if the user doesn't do anything for quite a while. For example, if a user doesn't do anything in the application for 30 minutes, he'll be signed out and redirected to the sign-in page if it's needed.

I felt difficult to imagine how to realize this feature. Surprisingly, it turned out to be simple to do. For every request, `devise`  records the current time in cookies. Therefore, when Rails gets a new request, it can compare the last request's time and the current time to see if the difference is over the threshold. If it's over, then just revoke the user's session.

### Enable the module
`:timeoutable`  doesn't need any database migration either because it only uses cookies to realize the feature. Thus, we can enable it directly.
```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :confirmable, :validatable,
         :recoverable, :rememberable, :timeoutable
end
```

There aren't many things you can customize for this module. The only one may be the **timeout time**. The default timeout time is 30 minutes. You can change it in the initializer. Later for testing, I'm going to change it to 1 minute.
```ruby
# config/initializers/devise.rb
# config.timeout_in = 30.minutes
config.timeout_in = 1.minute
```

### Usage
1. Log into the system in a browser. 
2. You can print `session` and you should see there's a new key called `warden.user.user.session`  with a nested JSON  `last_request_at` recording a timestamp of the request's time.
```ruby
puts session.to_json
{
  # ignore other keys
  "warden.user.user.session": {
    "last_request_at": 1687446153
  }
}
```
3. Wait for 1 minute and refresh the page, you will be redirected to the root page.

In my opinion, this module conflicts with `:rememberable`, for example, even if the "remember me time" is 2 weeks long, if the "timeout time" is 30 minutes, you'll lose your session after 30 minutes. The only benefit it has when you enable both modules is that **you can keep the session if you restart your browser within the timeout time** but it's not practical 😅.

## Lockable <a name="Lockable"></a>
`:lockable` implements methods for you to lock and unlock users. A locked user cannot log into the system. There are some reasons for you to want to lock users:
- You can lock a user if he/she enters the wrong password several times. It could be a malicious user trying to hack the account
- You want to lock a user because that person already left your organization
- ~~based on your mood~~

### Lockable strategies
`:lockable` has already designed some strategies. You can choose under what kind of situation should you lock or unlock a user. You can configure them in the `config/initializers/devise.rb`

#### Locking strategies
1. `:failed_attempts`: a user will be locked if the wrong password was entered several times
2. `:none`: you have to lock the user manually
The default is `:failed_attempts`

#### Unlocking strategies
1. `:email`: Send an unlock link to the user's email
2. `:time`: A locked user will be unlocked automatically after a certain amount of time
3. `:both`: Enables both strategies
4. `:none`: You have to unlock a user manually
The default is `:both`

### Eanble the module
`:lockable` requires some more columns. Execute
```
rails g migration add_lockable_to_users failed_attempts:integer unlock_token locked_at:datetime
```
to create a migration file:
```ruby
class AddLockableToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :failed_attempts, :integer, default: 0
    add_column :users, :unlock_token, :string
    add_column :users, :locked_at, :datetime
  end
end
```
- `locked_at` stores the time a user is locked. A user isn't locked when it's NULL.
- `failed_attempts` default value is 0, it will increase 1 whenever a user try to log in with wrong password
- `unlock_token` is a token embedded in the unlock link sent to the user if you allow users to unlock by themself.

We can enable `:lockable` now:
```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :confirmable, :validatable,
         :recoverable, :rememberable, :lockable
end
```

### Usage
#### Headless
We can check how to lock a user manually in the rails console.
```ruby
user = User.first
user.lock_access!
# => 'ZxmxUFVX579zNgpBJftN' This is the unlock_token
user.access_locked?
# => true
```
When you do that, an unlock mail with a unlock link will be sent to the user's email
```
Date: Thu, 22 Jun 2023 13:14:42 -0400
From: please-change-me-at-config-initializers-devise@example.com
Reply-To: please-change-me-at-config-initializers-devise@example.com
To: kevin@taipei.tw
Message-ID: <64948182289b0_eb89c1c317fd@F2XWD4WR0C.mail>
Subject: Unlock instructions
Mime-Version: 1.0
Content-Type: text/html;
 charset=UTF-8
Content-Transfer-Encoding: 7bit

<p>Hello kevin@taipei.tw!</p>

<p>Your account has been locked due to an excessive number of unsuccessful sign in attempts.</p>

<p>Click the link below to unlock your account:</p>

<p><a href="http://localhost:3000/users/unlock?unlock_token=ZxmxUFVX579zNgpBJftN">Unlock my account</a></p>
```
`unlock_token` will be embedded in the link. By the way, just like `:recoverable`, the token in the link is the real token and `user.unlock_token` stores its HMAC digest.

You can unlock the user by
```ruby
user.unlock_access!
user.access_locked?
# => true
user.locked_at
# => nil
user.unlock_token
# => nil
```

#### Use in browsers
`:lockable` will add 3 more paths:
```
new_user_unlock GET    /users/unlock/new(.:format) devise/unlocks#new
user_unlock     GET    /users/unlock(.:format)     devise/unlocks#show
                POST   /users/unlock(.:format)     devise/unlocks#create
```
- `devise/unlocks#show` is for the unlock link
- `devise/unlocks#new` and `devise/unlock#create` are for asking for resending the unlock mail

We can try entering the password several times to see how this really works. Before the test, let's modify the threshold to 3 times so we don't need to do that too many times.
```ruby
# config/initializers/devise.rb
config.maximum_attempts = 3
```
1. Go to `http://localhost:3000/users/sign_in` and try to log in with the wrong password for 3 times
2. Your user account will be locked and you cannot log in even if you enter the correct password.
  ![locked](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/466rr3itee7pxmxs9hmp.png)
3. You can find the unlock link in the unlock mail displayed in your rails server's logs. If you can't find it. You can also click the new link, **"Didn't receive unlock instructions?"**, which leads you to `/users/unlock/new` to ask for a new unlock mail.
![Sign-in page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/84b4ug9xxlafdcm15ah2.png)
4. Before you unlock your user account, you can go to the rails console and you'll find `user.failed_attempts` has increased.
```ruby
user.failed_attempts
# => 4
```
5. After you click the unlock link, your user will be unlocked. All related columns will be reset
```ruby
user.locked_at
# => nil
user.unlock_token
# => nil
user.failed_attempts
# => 0
```

### Customization
You can customize `:lockable` by
1. You can choose lock/unlock strategies and customize other options in `config/initializers/devise.rb`
2. modifying the mail template `app/views/devise/mailer/unlock_instructions.html.erb`
3. modify the resend instructions page, `app/views/devise/unlocks/new.html.erb`
4. run `rails generate devise:controllers users -c unlocks` to export `UnlocksController`

## Trackable <a name="Trackable"></a>
`:trackable` provides you the ability to track some information related to the use of `devise`. I think it'll be very straightforward if you just see the columns required by this module which are also the information that will be tracked. There are 5 of them:
- `sign_in_count`: how many times this user has signed in
- `current_sign_in_at`: the time the **current** session began
- `current_sign_in_ip`: the IP address of the user when the **current** session created
- `last_sign_in_at`: the time the **last** session began
- `last_sign_in_ip`: the IP address of the user when the **last** session created

You can use that information to detect suspicious actions or see statistics for the use of the system.

### Enable the module
We need to add those columns first for `:trackable`. Execute
```
rails g migration add_trackable_to_users sign_in_count:integer current_sign_in_at:datetime current_sign_in_ip last_sign_in_at:datetime last_sign_in_ip
```
and
```ruby
class AddTrackableToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :sign_in_count, :integer, default: 0
    add_column :users, :current_sign_in_at, :datetime
    add_column :users, :current_sign_in_ip, :string
    add_column :users, :last_sign_in_at, :datetime
    add_column :users, :last_sign_in_ip, :string
  end
end
```

After `db:migrate`, we can enable `:trackable`:
```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :confirmable, :validatable,
         :recoverable, :rememberable, :timeoutable, :lockable, :trackable
end
```

### Usage
1. Before opening your browser, open the rails console and check the current information
```ruby
user.sign_in_count
#=> 0
user.current_sign_in_at
#=> nil
user.current_sign_in_ip
#=> nil
user.last_sign_in_at
#=> nil
user.last_sign_in_ip
#=> nil
```
2. Open your browser and sign in. Back to the rails console to check the data. You'll find the current_sign_in_x and last_sign_in_x are identical. The IP address is `::1` because you connect to the server locally.
```ruby
user.sign_in_count
#=> 1
user.current_sign_in_at
#=> Thu, 22 Jun 2023 22:02:03.818027000 UTC +00:00
user.current_sign_in_ip
#=> "::1"
user.last_sign_in_at
#=> Thu, 22 Jun 2023 22:02:03.818027000 UTC +00:00
user.last_sign_in_ip
#=> "::1"
```
3. If you want to see different IP addresses being recorded, you can use your mobile or other laptops to connect to your development server. You can do that by starting the server with `-b` argument
```shell
rails s -b 0.0.0.0
```
4. Sign in on your other devices and then go back to the console to check. `user.sign_in_count` increased by 1 and `current_sign_in_x` information is also updated.
```ruby
user.sign_in_count
#=> 2
user.current_sign_in_at
#=> Thu, 22 Jun 2023 22:06:57.822208000 UTC +00:00
user.current_sign_in_ip
#=> "192.168.1.107"
user.last_sign_in_at
#=> Thu, 22 Jun 2023 22:02:03.818027000 UTC +00:00
user.last_sign_in_ip
#=> "::1"
```

### Checkpoint - IP
How do we know the origin IP address of a request? It's actually a Rails feature provided by `Actionpack`. It uses `ActionDispatch::Request#remote_ip`. `#remote_ip` is smart enough that it will ignore the proxy server's IP addresses and find the original IP address of the request.
```ruby
request.remote_ip
```

## Omniauthable <a name="Omniauthable"></a>
`:omniauthable` is a special module in `devise` but it's also in charge of a very common feature: letting users log in by using a user's session from another website, e.g. Facebook, Google, Twitter, Github, etc. It's kind of delegating authentication work to those big tech companies. Nowadays, most companies follow **OAuth**'s standards to build the authentication workflow (OAuth always means **OAuth 2.0** in this article). However, each company may have different **dialects** when you communicate via OAuth. This module is called `:omniauthable` because `devise` has integrated with the gem [omniauth](https://github.com/omniauth/omniauth), which provides a unified interface to realize the login process via OAuth.

The root logic of `:omniauthable` is totally different from traditional authentication, which makes it feel like a social outcast in `devise`. Therefore, the steps to enable `:omniauthable` are pretty different. If you aren't familiar with OAuth, don't worry, it's fine to read this chapter. I'll make changes step by step.

I'll use **Facebook** as the OAuth provider. The main steps we're going to go through are:
1. [[#Setup the Omniauth environment]]
2. [[#Connect to Facebook to grant the access]]
3. [[#Callback from Facebook]]
4. [[#Use returned user's information to create/find the user and sign him/her in]]

### Setup the Omniauth environment
#### Install assistance gems
Although we know `devise` has integrated with the gem `omniauth`, it just defines a unified interface without any functionalities. It will be easier if we just use other gems that implement the customized OAuth workflow for specific providers. For example, I'm going to use Facebook's OAuth login so I add the gems below in the Gemfile and do `bundle install`:
```ruby
# Gemfile
gem 'omniauth-facebook', '~> 9.0'
gem 'omniauth-rails_csrf_protection', '~> 1.0.1'
```
You can find gems for other platforms by googling gems name `omniauth-[Provider's name]`

#### Database migration
`:omniauthable` needs 2 additional string columns:
1. `provider`: it stores provider's name, `facebook`, `google`,
2. `uid`: it stores the user's **unique identifier** on the provider's website. For instance, you can see it as the id of your Facebook account

Execute
```
rails g migration AddOmniauthToUsers provider uid:index
```
and it will produce the migration file like below:
```ruby
class AddOmniauthToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :provider, :string
    add_column :users, :uid, :string
    add_index :users, :uid
  end
end
```
Run `rails db:migrate` to make the changes.

#### Enable modules
I assume you already have an App on Facebook for performing OAuth. If you don't know how to create an App on Facebook, you can check out their documentation: https://developers.facebook.com/docs/development/create-an-appso 
You can put the corresponding App's ID and App's secret in the `devise` initializer. 
```ruby
# config/initializers/devise.rb
config.omniauth :facebook, "APP_ID", "APP_SECRET"
```
Anyway, I think we'd better not commit confidential information in git. I suggest using the Rails credentials to do that. First, edit the credentials YAML:
```shell
EDITOR=vim rails credentials:edit
```
Add credentials:
```yaml
facebook:
  app_id: "APP_ID"
  app_secret: "APP_SECRET"
```
We can access the values via `Rails.application.credentials` in the initializer.
```ruby
# config/initializers/devise.rb
config.omniauth :facebook, Rails.application.credentials.facebook.app_id, Rails.application.credentials.facebook.app_secret
```

We can enable the module now. There's a special attribute `omniauth_providers` to configure, too.
```ruby
class User < ApplicationRecord
  devise :database_authenticatable, :registerable, :confirmable, :validatable,
         :recoverable, :rememberable, :timeoutable, :lockable, :trackable,
         :omniauthable, omniauth_providers: [:facebook]
end
```

### Connect to Facebook to grant the access
The first step of OAuth is to tell Facebook that you, a Facebook user, agree this web application is going to access your information on Facebook. Let's add the following code:
```ruby
# config/routes.rb
Rails.application.routes.draw do
  root "pages#index"
  devise_for :users, omniauth_providers: %i[facebook]
end
```
This will add 2 more routing endpoints:
```
user_facebook_omniauth_authorize GET|POST /users/auth/facebook(.:format)          devise/omniauth_callbacks#passthru
 user_facebook_omniauth_callback GET|POST /users/auth/facebook/callback(.:format) devise/omniauth_callbacks#facebook
```
- `/users/auth/facebook` is used to redirect the user to Facebook to grant the access
- `/users/auth/facebook/callback` is used to receive the callback request from Facebook with the asked information. In our case, that should be data including the user's email on Facebook.

Add a link to the `/users/auth/facebook` which is the `user_facebook_omniauth_authorize_path`. We use `button_to` because we want to trigger a form submission to send a POST request. We also add `turbo: false` to disable Turbo because we'd like to have a full-page reload
```html
<h1>Pages#index</h1>
<% if user_signed_in? %>
  <%= link_to 'Sign out', destroy_user_session_path, data: { turbo_method: :delete } %>
<% else %>
  <%= link_to 'Sign in', new_user_session_path %>
  <%= button_to "Sign in with Facebook", user_facebook_omniauth_authorize_path, data: { turbo: :false } %>
<% end %>
```

Maybe it's worth mentioning that `button_to` is a convenient way to create a form with only a submit button. It's almost identical to this:
```html
<%= form_with url: user_facebook_omniauth_authorize_path, data: { turbo: false } do |f| %>
  <%= f.submit "Sign in with Facebook" %>
<% end %>
```

### Callback from Facebook
If you grant access, Facebook will send back the requested information. I didn't mention that but the URL of the path, `/users/auth/facebook/callback`,  was embedded in the previous request. Facebook will redirect the users to that callback URL with the requested information.

If you take a look at `rails routes` again, you'll find that it's `devise/omniauth_callbacks#facebook` handling `/users/auth/facebook/callback`.  For other modules, exporting controllers is part of the customization. However, we have to export the `OmniauthController` for enabling `:omniauthable`. Execute
```shell
rails generate devise:controllers users -c omniauth_callbacks
```
and make `devise` use of this exported controller in routes:
```ruby
# config/routes.rb
Rails.application.routes.draw do
  root "pages#index"
  devise_for :users, omniauth_providers: %i[facebook], controllers: { omniauth_callbacks: 'users/omniauth_callbacks' }
end
```

You should add the action `#facebook` in the controller because it doesn't exist yet.
```ruby
# app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    # you will get the requested information from Facebook here
  end
end
```

### Use returned user's information to create/find the user and sign him/her in
We now know Facebook will return the requested information, a.k.a the user's data, to `Users::OmniauthCallbacksController#facebook` so we can use that information to create or find the user in the rails application. How do we do that?

It's where the `omniauth` gem shines. It defines a **shared schema** for all providers. You can check it at https://github.com/omniauth/omniauth/wiki/Auth-Hash-Schema. `omniauth` will put all the information in a specific header and you can access that via `request.env["omniauth.auth"]`. By looking up the schema page, we know this is the data schema
```json
{
  provider: "facebook",
  uid: "uid of user's Facebook account"
  info: {
    email: "user's email"
  }
  // Other fields are ignored
}
```

The next step is to create or find the user based on that information in `#facebook` action. We can utilize `User.find_or_create_by` to do that and then sign in the user by `sign_in_and_redirect`
```ruby
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    auth = request.env["omniauth.auth"]
    @user = User.find_or_create_by(provider: auth.provider, uid: auth.uid) do |user|
      user.email = auth.info.email
      user.password = Devise.friendly_token[0, 20] # give it a random password
    end
    @user.confirm unless @user.confirmed? # add this line if :confirmable is enabled
    sign_in_and_redirect root_path, event: :authentication # :event is for warden to execute callbacks for :authentication
    set_flash_message(:notice, :success, kind: "Facebook")
  end
end
```
I usually see people encapsulate the logic of user creation and finding in a class method called `.from_omniauth` so it can be used  by multiple providers in the future.
```ruby
# app/models/user.rb
def self.from_omniauth(auth)
  find_or_create_by(provider: auth.provider, uid: auth.uid) do |user|
    user.email = auth.info.email
    user.password = Devise.friendly_token[0, 20] # give it a random password
  end
end
```
so the `#facebook` can be refactored into
```ruby
# app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    @user = User.from_omniauth(request.env["omniauth.auth"])
    @user.confirm unless @user.confirmed? # add this line if :confirmable is enabled
    sign_in_and_redirect @user
    set_flash_message(:notice, :success, kind: "Facebook")
  end
end
```

### Test
1. Go to the root page, you'll see the new button, **Sign in with Facebook**
  ![Home page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a278xstbjq825168c3re.png)
2. Click the button, and you'll be redirected to Facebook granting access page. My Facebook app name is "develop only" so it says "develop only is requesting access to:"
  ![grant facebook access](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ooc0rjqenc2wn8hzmi51.png)
3. After granting access, you will find you have already logged in
  ![root page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bp06ioai5zna0ie9chr4.png)
4. Open the rails console, you'll find a new user is created and `provider` and `uid` are filled
```ruby
user = User.last
user.provider
#=> 'facebook'
user.uid.present?
#=> true
```

I only showed the general idea here. You can find more information on `devise`'s wiki page for OmniAuth: https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview. You can read [OAuth RFC](https://datatracker.ietf.org/doc/html/rfc6749) if you are eager to know the details.

## Conclusion
What a journey! We made it 😭! To be honest, I didn't expect I would spend so much time on this article because I've used every module more or less in the past. I thought I was pretty familiar with `devise`. It turned out there are still many things that are different from what I imagined when starting digging deeper. 

I tried to write this article as a tutorial for beginners to enable each module. Therefore, I demonstrate them step by step. In practice, you should execute the macro
```bash
rails generate devise User
```
and all migrations and related configurations will be there for you (except `:omniauthable` so I say it's an outcast😆). You can then opt-in to what you want. Whatever, I think it's still valuable if you just took over an existing project that uses `devise`.

Thank you for reading this article. I hope this article makes you understand more about `devise` and the authentication of web applications🙏. Maybe after reading this article, you may find you don't need `devise` at all due to its complexity. If that's true, you can read my other article, building your own authentication system in Rails. {% embed https://dev.to/kevinluo201/building-a-simple-authentication-in-rails-7-from-scratch-2dhb %}

If you think this article is helpful, you can buy me a coffee to encourage me 😉
[![Buy Me A Coffee](https://cdn.buymeacoffee.com/buttons/default-black.png)](https://www.buymeacoffee.com/kevinluo)

## Code Repo
https://github.com/kevinluo201/devise_modules

## References
* https://github.com/heartcombo/devise
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#session_cookie