Menu
+

Rails 8 Authentication
Built in lightweight authentication for Ruby on Rails.
authentication
David Gim
Saturday, 25 January 25




Lightweight and Built-in Authentication Generator in Rails 8


With the release of Rails 8, several exciting features were introduced, including Solid Queue, Kamal 2, and more. Among these additions was a new Authentication Generator. While this isn’t a full-fledged authentication system like Devise, it provides enough functionality to handle basic authentication tasks such as sessions, password authentication, and password reset emails.

Let’s dive in and explore how it works, and compare it to the elephant in the room—Devise.


The setup  


First, let’s create a new Rails app and generate the authentication scaffolding. You can name the app whatever you like.

rails new app postings
cd postings
rails g authentication
rails db:migrate

After running the migrations, take a look at your schema.rb file. You’ll notice two new tables: sessions and users.

Unlike Devise, the Rails authentication generator doesn’t create a registration controller or model. This is a step you’ll need to implement yourself, as the generator is designed to be lightweight and provide just enough functionality to get you started.


Lets first add routes for new registeration process:

 resource :registration, only: %i[new create] # routes for new and create 

And lets create registeration controller with follow methods:
class RegisterationsController < ApplicationController
  allow_unauthenticated_access

  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: "Successfully registered!"
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:email_address, :passworrd, :password_confirmation)
  end
end


Here, we’re using two new methods provided by the Rails authentication module:

  • start_new_session_for: creates new session for the @user
  • allow_unauthenticated_access: allows user registeration. 

These methods come from the Authentication module, which is located in app/controllers/concerns/authentication.rb.

Now, let’s create a view so users can register. Save this file as app/views/registrations/new.html.erb:

<div class="registeration">
    <% if alert = flash[:alert] %>
        <p><%= alert %></p>
    <% end %>
    <% if notice = flash[:notice] %>
        <p><%= notice %></p>
    <% end %>

    <%= form_with model: @user, url: registeration_path, class: "registeration-form" do |form| %>
        <div class="form-group">
            <%= form.label :email_address %>
            <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], class: "form-control" %>
        </div>
        <div class="form-group">
            <%= form.label :password %>
            <%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72, class: "form-control" %>
        </div>
        <div class="form-group">
            <%= form.label :password_confirmation %>
            <%= form.password_field :password_confirmation, required: true, autocomplete: "current-password", placeholder: "Confirm your password", maxlength: 72, class: "form-control" %>
        </div>
        <%= form.submit "Register", class: "btn btn-primary" %>
    <% end %>
</div>

Now if you run the server you'll see something root page of your app. 

Testing 


Testing the new Action

test "GET #new renders the new partial" do
  get new_registration_path
  assert_response :success
  assert_template partial: "registrations/_new"
end

test "GET #new assigns a new user" do
  get new_registration_path
  assert assigns(:user).is_a?(User)
  assert assigns(:user).new_record?
end

Testing the create Action

setup do
  @valid_params = {
    user: {
      email_address: "test@example.com",
      password: "password123",
      password_confirmation: "password123"
    }
  }

  @invalid_params = {
    user: {
      email_address: "",
      password: "password123",
      password_confirmation: "wrongpassword"
    }
  }
end

test "POST #create with valid parameters creates a new user" do
  assert_difference("User.count", 1) do
    post registration_path, params: @valid_params
  end

  assert_redirected_to root_path
  assert_equal "Successfully registered!", flash[:notice]
end

test "POST #create with invalid parameters does not create a user" do
  assert_no_difference("User.count") do
    post registration_path, params: @invalid_params
  end

  assert_response :unprocessable_entity
  assert_template partial: "registrations/_new"
  assert_equal "Failed to register!", flash[:alert]
end

We can run the test with following command

bin/rails test test/controllers/registrations_controller_test.rb

If everything is working and correct you'll get response like this 

Running 4 tests in a single process (parallelization threshold is 50)
Run options: --seed 12345

# Running:

....

Finished in 0.587130s, 6.8092 runs/s, 20.4276 assertions/s.
4 runs, 12 assertions, 0 failures, 0 errors, 0 skips

That's is all for testing, we could check things like email uniqueness or password length and so on but I don't it's necessary here. 

Lets take a look at authentication module


/controllers/concerns/authentication.rb
module Authentication
  extend ActiveSupport::Concern

  included do
    before_action :require_authentication
    helper_method :authenticated?
  end

  class_methods do
    def allow_unauthenticated_access(**options)
      skip_before_action :require_authentication, **options
    end
  end

  private
    def authenticated?
      resume_session
    end

    def require_authentication
      resume_session || request_authentication
    end

    def resume_session
      Current.session ||= find_session_by_cookie
    end

    def find_session_by_cookie
      Session.find_by(id: cookies.signed[:session_id]) if cookies.signed[:session_id]
    end

    def request_authentication
      session[:return_to_after_authenticating] = request.url
      redirect_to new_session_path
    end

    def after_authentication_url
      session.delete(:return_to_after_authenticating) || root_url
    end

    def start_new_session_for(user)
      user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
        Current.session = session
        cookies.signed.permanent[:session_id] = { value: session.id, httponly: true, same_site: :lax }
      end
    end

    def terminate_session
      Current.session.destroy
      cookies.delete(:session_id)
    end
end


Module actions ensures authentication by default using a before_action callback but allows exemptions with allow_unauthenticated_access. It manages sessions by storing them in the database and tracking them via signed cookies.

Key methods include resume_session to restore a user’s session, request_authentication to redirect unauthenticated users to a login page, and start_new_session_for to create a new session upon login or registration. It also supports secure features like httponly cookies and thread-safe session management via the Current class. 

Quick example: 
<% if authenticated? && (Current.user == @post.author) %>
  <div style="display: inline-flex; align-items: center;">
    <%= image_tag("edit.svg", class: "svg") %>
    <%= link_to "Edit", edit_post_path(@post), class: "btn edit" %>
    <%= image_tag("delete.svg", class: "svg") %>
    <%= link_to "Delete", post_path(@post, category: @category), method: :delete, data: { turbo_confirm: 'Are you sure?', turbo_method: "delete" }, class: "btn delete" %>
  </div>
<% end %>

Code above checks if user is authenticated and if current user is the author of the post. 

It's simple as that just create a new user and login and you are all set.