Building robust user authentication is a fundamental requirement for most web applications. In this guide, we’ll walk through setting up a complete authentication system in Rails using Devise, integrating it seamlessly with Hotwire for a modern SPA-like experience, enhancing our forms with Simple Form, and implementing user profile images with Active Storage.
By the end of this guide, you’ll have a fully functional authentication system that handles user registration, login, profile management, and image uploads—all working smoothly with Turbo.
Table of Contents
1. Devise Setup
Devise is the de facto standard for authentication in Rails applications. It provides a complete MVC solution with modules for password recovery, session management, email confirmation, and more.
Installation
bundle add devise
rails generate devise:install
rails generate devise:views
rails generate devise User
After running the generator, Devise will create a migration file in db/migrate/. Before running the migration, let’s add some custom fields to our User model.
Adding Custom Fields
Open the generated migration file and add the following fields within the create_table block:
# db/migrate/XXXXXX_devise_create_users.rb
def change
create_table :users do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
# ... other Devise fields ...
## Custom Fields
t.string :name_surname, null: false, default: ""
t.string :gsm
t.date :date_of_birth
t.timestamps null: false
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
end
Configuring Strong Parameters
When adding custom fields, we need to permit them in Devise’s strong parameters. First, update your routes to use a custom registrations controller:
# config/routes.rb
Rails.application.routes.draw do
devise_for :users, controllers: { registrations: "users/registrations" }
# ... other routes ...
end
Then create the custom controller:
# app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
before_action :configure_account_update_params, only: [:update]
protected
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:name_surname, :gsm, :date_of_birth])
end
def configure_account_update_params
devise_parameter_sanitizer.permit(:account_update, keys: [:name_surname, :gsm, :date_of_birth])
end
end
Now run the migration:
rails db:migrate
2. Hotwire Integration
Hotwire (HTML Over The Wire) is Rails’ answer to building reactive applications without writing custom JavaScript. However, Devise was built before Hotwire existed, so we need to make a few adjustments to ensure they work together smoothly.
The Problem
By default, when Devise encounters an authentication error (invalid credentials, unauthorized access, etc.), it responds with HTTP status codes that Turbo doesn’t handle gracefully. This can result in broken redirects or missing flash messages.
Creating a Custom Failure App
To handle authentication failures properly with Turbo, create a custom failure app:
# lib/turbo_failure_app.rb
class TurboFailureApp < Devise::FailureApp
def respond
if request_format == :turbo_stream
redirect
else
super
end
end
def skip_format?
%w[html turbo_stream */*].include?(request_format.to_s)
end
end
Configuring Devise for Turbo
Update your Devise initializer to use the custom failure app:
# config/initializers/devise.rb
# Ensure the custom failure app is loaded
require "turbo_failure_app"
Devise.setup do |config|
# ... other configurations ...
# Add turbo_stream to navigational formats
config.navigational_formats = ["*/*", :html, :turbo_stream]
# Configure Warden to use our custom failure app
config.warden do |manager|
manager.failure_app = TurboFailureApp
end
end
How It Works
| Component | Purpose |
|---|---|
TurboFailureApp |
Intercepts authentication failures and ensures proper redirect behavior for Turbo Stream requests |
skip_format? |
Allows the failure app to handle HTML, Turbo Stream, and wildcard formats |
navigational_formats |
Tells Devise which response formats should trigger redirects instead of 401 responses |
With this configuration, your Devise authentication will work seamlessly with Turbo Drive and Turbo Frames.
3. Simple Form Configuration
Simple Form is a powerful form builder that reduces boilerplate and integrates beautifully with CSS frameworks like Bootstrap and Tailwind.
Installation
bundle add simple_form
rails generate simple_form:install
# For Bootstrap projects:
rails generate simple_form:install --bootstrap
Updating Devise Views
Let’s refactor the Devise login page to use Simple Form with Tailwind CSS styling:
<%# app/views/devise/sessions/new.html.erb %>
<div class="max-w-md mx-auto bg-white shadow-lg rounded-lg p-8 border border-gray-300">
<h2 class="text-3xl font-bold text-center mb-8 text-gray-800">Log in</h2>
<%= simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div class="space-y-4">
<%= f.input :email,
label: "Email",
required: true,
autofocus: true,
input_html: {
autocomplete: "email",
class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
} %>
<%= f.input :password,
label: "Password",
required: true,
input_html: {
autocomplete: "current-password",
class: "mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
} %>
<% if devise_mapping.rememberable? %>
<%= f.input :remember_me,
as: :boolean,
label: "Remember me",
wrapper_html: { class: "flex items-center" },
input_html: { class: "h-4 w-4 text-blue-600 border-gray-300 rounded" } %>
<% end %>
<%= f.button :submit,
"Log in",
class: "w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition duration-150" %>
</div>
<% end %>
<div class="mt-6 text-center text-sm text-gray-600">
<%= render "devise/shared/links" %>
</div>
</div>
4. Active Storage & Image Processing
Active Storage provides a simple way to attach files to Active Record models. Combined with the image_processing gem, we can handle user profile images with validation and transformations.
Installation
First, uncomment the image_processing gem in your Gemfile:
# Gemfile
gem "image_processing", "~> 1.2"
Then install and set up Active Storage:
bundle install
rails active_storage:install
rails db:migrate
Attaching Images to Users
Update the User model to accept profile images:
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_one_attached :profile_image
validate :acceptable_image
private
def acceptable_image
return unless profile_image.attached?
# Validate file size (max 10MB)
if profile_image.byte_size > 10.megabytes
errors.add(:profile_image, I18n.t("errors.messages.profile_image_too_large", default: "is too large (maximum is 10MB)"))
end
# Validate content type
acceptable_types = ["image/jpeg", "image/jpg", "image/png", "image/webp"]
unless acceptable_types.include?(profile_image.content_type)
errors.add(:profile_image, I18n.t("errors.messages.profile_image_invalid_format", default: "must be a JPEG, PNG, or WebP image"))
end
end
end
Updating the Registration Form
Add the file input to your edit registration view:
<%# app/views/devise/registrations/edit.html.erb %>
<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, multipart: true }) do |f| %>
<%# ... other fields ... %>
<div class="space-y-2">
<% if resource.profile_image.attached? %>
<div class="mb-4">
<%= image_tag resource.profile_image.variant(resize_to_limit: [150, 150]),
class: "rounded-full border-2 border-gray-200" %>
</div>
<% end %>
<%= f.input :profile_image,
as: :file,
label: "Profile Image",
hint: "Accepted formats: JPEG, PNG, WebP. Maximum size: 10MB",
input_html: {
accept: "image/jpeg,image/png,image/jpg,image/webp",
class: "block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100"
} %>
</div>
<%# ... submit button ... %>
<% end %>
Updating Strong Parameters
Don’t forget to permit the profile_image parameter in your registrations controller:
# app/controllers/users/registrations_controller.rb
def configure_account_update_params
devise_parameter_sanitizer.permit(:account_update, keys: [:name_surname, :gsm, :date_of_birth, :profile_image])
end
Conclusion
We’ve built a complete, modern authentication system that combines the reliability of Devise with the reactivity of Hotwire. Here’s what we accomplished:
- Devise: Handles all authentication logic with custom user fields
- Hotwire: Provides seamless page updates without full reloads
- Simple Form: Creates clean, maintainable forms with minimal code
- Active Storage: Manages user profile images with proper validation
This setup provides a solid foundation that you can extend with additional features like OAuth providers, two-factor authentication, or email confirmation as your application grows.
