# Rails 8 Authentication

If you've dealt with authentication in a Rails app before, you've probably used a gem like `devise` to add the feature in a couple of hours and move on to the next thing.

However, the [release of Rails 8](https://avohq.io/blog/rails-8-whats-new) came with many changes that philosophically nudge the framework to developer empowerment to reduce the need for third-party code.

That's why Rails 8 comes with an authentication generator to help us add the feature to any application pretty quickly while following Rails conventions and owning the authentication code.

In this article, we will **add authentication to a Rails app using the generator** and some custom code on top of it to improve the default provided auth.

## What we will build
We will build a simple job listing application where listing details can only be accessed by authenticated users.

Beyond e-mail & password authentication, **we will also add an email and password sign up flow**

Even though this is not as feature-complete as devise, it is a step closer to showing that we can add as many features as we require as long as we build upon the foundations and concepts provided by the auth generator.

The final result should look something like this:

## Application set up
The first step is to create a new Rails app using Tailwind:

```bash
$ rails new joblist --css=tailwind
```

Next, we run the db setup and migrations:

```bash
$ bin/rails db:setup db:migrate
```

We will have a single model called `JobListing` so we add it:

```bash
$ bin/rails generate scaffold JobListing name description:text
```

Next, we migrate the database:

```bash
$ bin/rails db:migrate
```

After running this, and adding some custom styling, we end up with something like this:

![Rails 8 authentication job listing app home page](/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6Mzc4LCJwdXIiOiJibG9iX2lkIn19--5e21ce16446ea8b0493e377309b884b9a8a3bb6b/rails-8-authentication-homepage.webp)

Of course, this is just a placeholder for the home page. We will be adding the rest of the functionality as we progress with the tutorial.

Now, we will analyze the generated code to better understand the auth flow. You can <a href="#sign-in-flow" data-behavior="soft-scroll">skip to the tutorial itself </a> if you already know about the code.

## Understanding the generator
The first thing to understand is that Rails 8 authentication is not meant to be a stand-in replacement for something like devise, at least not for now.

On the contrary, it's meant to kickstart our app's authentication and leave us with a solid foundation to build starting from some solid architectural choices.

Out of the box, it comes with two main features: **user sign in and password recovery.**

This leaves us with the task of building our sign-up flow and any other feature we want like [social login](https://avohq.io/blog/social-login-rails-google-github), [magic links login](https://avohq.io/blog/magic-link-authentication-with-rails), phone authentication, [account confirmation](https://avohq.io/blog/user-confirmation-rails-auth), account locking, etc.

If this is a dealbreaker for you, you might consider using a gem. 

But, consider that 'owning' the authentication flow means that customization will be easier, and believe me: it's _very probable_ that your auth flows will require customization down the road.

Now that we have that out of the way, let's focus on the architecture:

## The generator's architecture
When we run the `bin/rails generate authentication` command, Rails generates quite a bit of code for us: controllers, controller concerns, views, mailers, and even a `Connection` channel to use with ActionCable.

However, to understand how everything works we must focus on the models it generates: `User`, `Session` and `Current`. The first two are db-backed:

![User and session models database schema](/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6Mzc5LCJwdXIiOiJibG9iX2lkIn19--e6e41f41727111025e0004c8c0daf021448f74a9/database-models.webp)

Out of the box, the `users` table stores an e-mail address and a `password_digest` which is used to compare the password provided at sign-in with the password used to create the user.

On the other hand, the `sessions` table belongs to a `User` and, out of the box, stores the `ip_address` and `user_agent` information for every session started by the `User` which is useful by itself but also allows us to build upon this concept in the future if we need to.

The `Current` model is a singleton that inherits from `ActiveSupport::CurrentAttributes` that lets us access the user associated with the session present in a given request cookies:

```ruby
class Current < ActiveSupport::CurrentAttributes
  attribute :session
  delegate :user, to: :session, allow_nil: true
end
```

Whenever we call `Current.user` we access the `User` instance associated with the session via the `user_id` foreign key in the `sessions` table.

This is very similar to the view-accessible `current_user` global method defined by `devise` so we can use it in our views or controllers whenever we need to check whether a request comes from a logged-in user or not.

{{info}}
One advantage the Rails 8 auth generator has over `devise` is that it keeps track of sessions history instead of just storing the latest session information like the `trackable` module does. This allows for a more granular control of user sessions and possibly a more secure application in general.
{{/info}}

### The Authentication concern
The heart of the authentication flow with the Rails 8 generator is definitely the `Authentication` controller concern.

This module, which is 52 lines of code, adds everything we need to authenticate users within our application.

It defines two important methods, which are `require_authentication` and the helper method `authenticated?`. 

As its name suggests, the `require_authentication` method makes sure the request has a signed cookie that defines the `session_id` which is used to retrieve a session or return the `Current.session` in case the session was previously defined in the request lifecycle.

On the other hand, the `authenticated?` method is an alias to the `resume_session` method, which returns the current session or finds a session by cookie.

There's also a private `terminate_session` method which basically destroys the `Current.session` and deletes the `session_id` cookie to make sure the user is currently signed out of our application.

One thing to consider with this concern is that it handles the typical redirect after sign in: it keeps track of the URL that generated the sign in request in the `return_to_after_authenticating` cookie and saves us from manually defining that common redirection flow.

{{warning}}
Because the `Authentication` concern is included in the `ApplicationController` by default, authentication is required for every endpoint of our application that inherits from it. If we want to skip authentication for a controller action we need to add an `allow_unauthenticated_access only: [:action]` and the `before_action :resume_session` calls to every controller action we need to skip authentication for, or we can add the `allow_unauthenticated_access` call after the `include Authentication` in the `ApplicationController` and manually define the routes we want to authenticate.
{{/warning}}

### The sessions controller
This controller is responsible for creating and destroying sessions. In other words: it's responsible for signing users in and out of our application.

It defines three actions:

- The `new` action, which is responsible for rendering the sign-in form.
- The `create` action, which authenticates a user and persists the session in the database.
- The `destroy` action, which is responsible for terminating a session whenever a user wants to sign out of our application.

Note that, to make this all work, a single `resource :session` is defined in the routes file which defines 6 endpoints. However, the `show` and `edit` actions are not defined in the controller, so we can modify the route to `resource :session, only: [:new, :create, :destroy]` to keep our routes cleaner.

### The passwords controller
This controller is responsible for the password recovery flow.

It defines four actions:
- The `new` action: responsible for rendering the password recovery form.
- The `create` action: responsible for sending the password reset email using the `PasswordsMailer.reset`. 
- The `edit` action: renders the password edit form if the correct `token` is present in the URL. The token is a signed JWT which contains the user's ID, and it has a 15-minute expiration date. If the token is not present or is invalid, the user gets redirected to the `new` action.
- The `update` action is where the actual update happens given the password and its confirmation match.

{{info}}
If we want to customize the password reset e-mail appearance, we can do it by changing `app/views/passwords_mailer/reset.html.erb`.
{{/info}}

### The routes
The generator adds the following to our `routes.rb` file:

```ruby
# config/routes.rb
resource :session, only: %i[new create destroy]
resources :passwords, param: :token
```

This generates the necessary routes for the sign-in and password reset flows.

However, to remove unused routes, we can modify the `passwords` routes:

```ruby
resources :passwords, only: %i[new create edit update], param: :token
```

We don't really need to access the index, show or destroy actions for this resource in particular.

### The views
The generator creates the necessary views for the sign-in and password recovery flows.

Namely, it generates views for the sign in at `app/views/sessions/new.html.erb` and another for the password recover at `app/views/passwords/new.html.erb`.

After minimal styling, adding a padding, borders and centering them, is applied, they look like this:

![The generated sign-in view](/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzgwLCJwdXIiOiJibG9iX2lkIn19--c3a14eb70934076f32325bb5db08f610333e3212/sign-in-view.webp)

![The generated password recovery view](/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzgxLCJwdXIiOiJibG9iX2lkIn19--5ad28bb529589e0eaf7fcc9662232a186db8d7ed/password-recovery-view.webp)

We also get a reset mailer view generated at `app/views/password_mailer/reset.html.erb` which might not the prettiest, but it does its job:

![The generated reset mailer view](/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MzgyLCJwdXIiOiJibG9iX2lkIn19--63f82910757bcebeb061fb9179a1e6fb665b12fb/reset-mailer-view.webp)

## Sign in flow
The first step to add authentication with the Rails generator is to actually run it:

```bash
$ bin/rails generate authentication
```

This will generate all the necessary code and database migrations to add a sign-in feature to our application.

Note that if we created our app with the `--css=tailwind` argument, the views will be decently styled to kickstart the process.

The command will create a `User` and `Session` models and a `Current` class to access the current user whenever we need it.

{{warning}}
Bear in mind that the generator produces a `User` model and that, at least for the moment, we're unable to customize which model to use. If you would like to use another model for authentication, you would have to change the references to users in the generated code and add the `has_many :sessions` association and the `has_secure_password` call to the desired model to implement password-related logic.
{{/warning}}

It will also add endpoints, controllers and their corresponding views to handle session creation (sign-in) and the password recovery flows.

Unless we need to modify the custom sign-in flow with the addition of extra fields or any other personalization, we're basically good to go: users can log in to our application using their existing account information.

However, consider that, by default, every route in your application will be protected, and you might not want that.

To allow a specific endpoint or controller to be accessed without authentication, we can add the following to the controller file:

```ruby
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
  allow_unauthenticated_access only: [:home]
  before_action :resume_session, only: [:home]

  def home
    @job_listings = JobListing.all
  end
end
```

Notice that the `resume_session` method call is necessary for this to work because it's the method required to retrieve the current session. Otherwise, even if we were signed in the view wouldn't be able to access the `Current.user`.

To add a sign-out action we need to add a button that makes a `DELETE` request to the `/session` endpoint.

Somewhere in our app, we should add something like this:

```erb
<% if Current.user.present? %>
  <%= button_to "Sign out", session_path, method: :delete, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-black underline focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
<% else %>
  <%= link_to "Sign in", new_session_path, class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" %>
<% end %>
```

Notice that we have to add the `button_to` helper for it to work correctly.

### Flash messages
Even though some actions like `sessions#create` include a flash message for the scenario where no user is found, there are a couple of places were flash messages can help our users.

The first place is in the `request_authentication` method that resides in the `Authentication` concern:

```ruby
# app/controllers/concerns/authentication.rb
def request_authentication
  session[:return_to_after_authenticating] = request.url
  flash[:alert] = "You must be logged in to access this page"
  redirect_to new_session_path
end
```

We add the flash message to notify our users that they must be logged to access a given page.

The other place is in the `destroy` action of the `SessionsController`:

```ruby
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def destroy
    terminate_session
    flash[:notice] = "You have been signed out."
    redirect_to new_session_path
  end
end
```

We then add a partial to display flash messages to give better feedback to our users.

After adding the flash messages, we should have a working authentication flow:

{{gumlet video_id="67861de161e6f9f4a6a9c509" width="2742" height="1444" /}}

## Password recovery flow
The password reset flow also comes out of the box with the Rails 8 auth generator.

To test it in development, we can add the `letter_opener` gem to our Gemfile and configure ActionMailer to intercept emails:

```ruby
# Gemfile
group :development do
  gem "letter_opener"
end
```

```ruby
# config/development.rb
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.perform_deliveries = true
```

We can access the password reset screen at `/passwords/new` and a link to it is actually present in the views provided by the generator.

When a user clicks in the link received in the password reset email, which contains a signed and expiring token, the password edit form gets rendered, and it allows the user to update the password.

The password flow looks like the following:

{{gumlet video_id="6786273d61e6f9f4a6aa0ef3" width="2742" height="1388" /}}

## Sign up flow
The Rails authenticator doesn't include a sign-up flow, mostly because it's a very application-dependent flow.

But, adding it to our app is straightforward.

Before anything, we need to add validations to our `User` model, considering that the default auth generator doesn't any.

By default, we need an `email_address` and a `password` to sign in to our application, so we need to make sure both of those fields are required and valid.

```ruby
class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy

  validates :email_address, presence: true, uniqueness: true, format: { with: URI::MailTo::EMAIL }
  validates :password, presence: true

  normalizes :email_address, with: ->(e) { e.strip.downcase }
end
```

 Then, we add a `registration` endpoint:

```ruby
# config/routes.rb
resource :registrations, only: [:new, :create]
```

Next, we define a `RegistrationsController` and define an action to render the sign-up form:

```ruby
# app/controllers/registrations_controller.rb
class RegistrationsController < ApplicationController
  allow_unauthenticated_access only: %i[new create]
  resume_session only: :new
  
  def new
    @user = User.new
  end
end
```

We then add a view to handle user sign up, we will have fields for email, password, and password confirmation:

```erb
# app/views/registrations/new.html.erb
<div class="mt-24">
  <div class="max-w-2xl mx-auto md:w-2/3 w-full">
    <div class="px-6 py-4 border border-gray-300 rounded-xl">
      <h1 class="font-bold text-2xl">Sign up</h1>
      <p class="text-base text-gray-500">
        Sign up to create your account and start posting jobs.
      </p>

      <%= form_with url: registrations_url, model: @user, method: :post, class: "contents" do |form| %>
        <div class="my-5">
          <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], class: "block shadow rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
        </div>

        <div class="my-5">
          <%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72, class: "block shadow rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
        </div>

        <div class="my-5">
          <%= form.password_field :password_confirmation, required: true, autocomplete: "current-password", placeholder: "Password confirmation", maxlength: 72, class: "block shadow rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %>
        </div>

        <div class="col-span-6 sm:flex sm:items-center sm:gap-4">
          <div class="flex w-full items-center justify-between">
            <div class="inline">
              <%= form.submit "Sign up", class: "rounded-lg py-2 px-6 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
            </div>

            <div class="mt-4 text-sm text-gray-500 sm:mt-0">
              <%= link_to "Forgot password?", new_password_path, class: "text-gray-700 underline" %>
            </div>
          </div>
        </div>
      <% end %>
    </div>
  </div>
</div>
```

Then, we add the sign-up logic to the `create` action of the registrations controller:

```ruby
class RegistrationsController < ApplicationController
  allow_unauthenticated_access only: %i[new create]
  before_action :resume_session, only: %i[new create]

  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      start_new_session_for @user
      redirect_to root_path, notice: "You've successfully signed up to Joblister. Welcome!"
    else
      flash[:alert] = @user.errors.full_messages.join(", ")
      render :new, status: :unprocessable_entity
    end
  end
  
  private

  def user_params
    params.expect(user: [:email_address, :password, :password_confirmation])
  end
end
```

Now, if we go through the sign-up flow, we can register users within our application. 

Please note that adding `has_secure_password` to the `User` model sets a `password_confirmation` attribute that is validated when we try to create a new user. 

So, because we're passing that attribute in the `user_params` hash, if the confirmation doesn't match the password, the `errors` object will actually have the appropriate message.

Of course, this flow can be vastly different depending on your application's requirements, I just wanted to demonstrate how a basic devise-like sign-up flow would work.

You can modify the view or the controller action as you wish in case your application calls for more fields or a different sign-up flow.

If you've been following, the sign-up flow should look something like this:

{{gumlet video_id="6786e5e81ca67b92b082ac14" width="1444" height="704" /}}

### Checking against data breaches

An extra step to improve our sign-up flow is to check the chosen password against a known leaked password using the `pwned` gem, which implements the “Have I been pwned?” API.

To add this feature to our app, we start by adding and installing the gem:

```console
bundle add pwned && bundle install
```

Next, we just have to add a validation to the `User` model:

```ruby
class User < ApplicationRecord
  validates :password, not_pwned: true
end
```

Now, if we try signing up with a known breached password like “seinfeld”, we should get a validation error with the following message: **“Password has previously appeared in a data breach and should not be used”.**

We can customize the message by replacing the boolean with an object with the message:

```ruby
class User < ApplicationRecord
  validates :password, not_pwned: { message: "has appeared in %{count} data breaches. Please choose another." }
end
```

There are many possible customizations we can add, check the [gem's documentation](https://github.com/philnash/pwned) to explore them all.

{{warning}}
If you're hesitant about sending user's passwords to an API that could, paradoxically, be breached itself or act maliciously, you might be relieved to learn that the API, and the `pwned` gem, uses the [k-anonymity](https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2/#cloudflareprivacyandkanonymity) property which, in this case, takes a piece of the password's SHA-1 hash (the first 5 characters) and compares it against the list of leaked passwords. If it's present, we can safely conclude the password was leaked without actually revealing it.
{{/warning}}

## TL;DR
Rails 8 added an authentication generator that, in principle, allows us to get rid of gems like `devise` and build auth by our own.

It generates two db-backed models: `User` and `Session` which help us keep track of the actual users and their session

It also generates a `Current` class that inherits from `ActiveSupport::CurrentAttributes` that allows us to access the currently logged-in user using the `Current.user` method.

Besides the models, it also adds an `Authentication` concern that handles most of the logic and the corresponding controllers, endpoints, and views to handle the basic sign-in and password recovery flows.

After we run the generator, we should be able to sign in or reset passwords for any existing user in our application. 

To have a better UX, we should also add flash messages to the `request_authentication` method in the `Authentication` concern and to the `destroy` session endpoint.

To add a sign-up flow, we just need to create a `RegistrationsController` with its corresponding endpoint and view to render the form where users can input their data and sign-up to our application.

After creating the user, we just call the `start_new_session_for` method and pass the `User` instance to log the user in.

As you can see, the Rails 8 auth generator produces a practical way to handle user authentication and allows us to replace gems like `devise` which can be pretty heavy, especially if our auth needs are not that advanced.

Having to manage the code by ourselves can seem daunting at first but, eventually, it probably results in a more maintainable application because we are forced to understand what's happening behind the scenes.

If you liked this post, you can probably enjoy other posts that we wrote that use the authenticator like: [social login Rails 8 auth](https://avohq.io/blog/social-login-auth-generator) or [user confirmation with Rails 8 auth](https://avohq.io/blog/user-confirmation-rails-auth)

I hope this article helped you understand and improve your authentication code. Stay in tune for more tutorials like this.

Have a good one and happy coding!
