Rails 8 Authentication
Built in lightweight authentication for Ruby on Rails.
authentication
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]
endWe 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.