Simple, secure authentication for Rails 8+ applications. Built on Rails primitives with no magic.
Coming from Devise? Read our detailed comparison.
- Email/Password authentication - secure session-based auth
- Magic link authentication - passwordless sign-in via email
- Email confirmation - verify user email addresses
- Password reset - secure password recovery flow
- OAuth support - Google, GitHub, and more
- Temporary users - guest accounts that convert to permanent
- Rate limiting - built-in protection on all endpoints
- Session tracking - IP and user agent logging
- Customizable styling - CSS variables for easy theming
- Custom mailers - use your own branded email templates
Add to your Gemfile:
gem "rails_simple_auth"Then run:
bundle installRun the installer:
rails generate rails_simple_auth:install
rails db:migrateAdd authentication to your User model:
class User < ApplicationRecord
authenticates_with :confirmable, :magic_linkable, :oauth, :temporary
# Your custom fields and validations
validates :company_name, presence: true
endAvailable modules:
:confirmable- Email confirmation for new accounts:magic_linkable- Passwordless sign-in via email:oauth- OAuth provider support (Google, GitHub, etc.):temporary- Guest accounts that convert to permanent
For basic email/password auth only:
class User < ApplicationRecord
authenticates_with
endProtect your routes:
class ApplicationController < ActionController::Base
before_action :require_authentication
endThe gem doesn't own your User model—you do. Add any custom fields:
# db/migrate/xxx_create_users.rb
class CreateUsers < ActiveRecord::Migration[8.0]
def change
create_table :users do |t|
# Required by gem
t.string :email, null: false
t.string :password_digest, null: false
t.datetime :confirmed_at # if using Confirmable
# Your custom fields
t.string :name
t.string :company_name
t.boolean :admin, default: false
t.string :oauth_provider
t.string :oauth_uid
t.timestamps
end
add_index :users, :email, unique: true
end
endThe gem ships with no CSS by default (Option B). Generate base styles:
rails generate rails_simple_auth:cssThen customize by overriding CSS variables:
/* In your application.css */
:root {
--rsa-color-primary: #22c55e; /* Your brand color */
--rsa-color-background-form: #f0fdf4; /* Form background */
--rsa-color-text: #166534; /* Text color */
}Or edit rails_simple_auth.css directly for complete control.
| Variable | Default | Description |
|---|---|---|
--rsa-color-primary |
#4f46e5 |
Primary button/link color |
--rsa-color-primary-hover |
#4338ca |
Primary hover state |
--rsa-color-background-form |
#ffffff |
Form container background |
--rsa-color-text |
#475569 |
Main text color |
--rsa-color-text-muted |
#64748b |
Secondary text color |
--rsa-color-border |
#e2e8f0 |
Border color |
--rsa-color-danger |
#dc2626 |
Error message color |
Copy views for full customization:
rails generate rails_simple_auth:views
# Or specific views only
rails generate rails_simple_auth:views --only sessions passwordsViews use BEM naming: .rsa-auth-form, .rsa-auth-form__input, etc.
# config/initializers/rails_simple_auth.rb
RailsSimpleAuth.configure do |config|
# Features
config.magic_link_enabled = true
config.email_confirmation_enabled = true
config.enable_oauth(:google, :github)
# Token expiration
config.magic_link_expiry = 15.minutes
config.password_reset_expiry = 15.minutes
config.confirmation_expiry = 24.hours
# Paths (symbol, string, or proc)
config.after_sign_in_path = :dashboard_path
config.after_sign_out_path = -> { new_session_path }
# Layout
config.layout = "auth" # Use a custom layout
# Mailer
config.mailer_sender = "auth@myapp.com"
# config.mailer_class = "UserMailer" # Use custom mailer (optional)
# Password requirements
config.password_minimum_length = 12
# Callbacks
config.after_sign_in_callback = ->(user, controller) {
Analytics.track("sign_in", user_id: user.id)
}
end- Enable providers:
RailsSimpleAuth.configure do |config|
config.enable_oauth(:google, :github)
end- Configure OmniAuth:
# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
provider :google_oauth2, ENV["GOOGLE_CLIENT_ID"], ENV["GOOGLE_CLIENT_SECRET"]
provider :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"]
end- Optionally map OAuth fields:
class User < ApplicationRecord
authenticates_with :oauth
def assign_oauth_attributes(auth_hash)
self.name = auth_hash.dig("info", "name")
self.avatar_url = auth_hash.dig("info", "image")
self.oauth_provider = auth_hash["provider"]
self.oauth_uid = auth_hash["uid"]
end
endTemporary users allow visitors to try your app without creating an account. They get a real user record with full functionality, then can convert to a permanent account later by providing email and password.
Reduce friction: Let users experience your app's value before asking them to sign up. This is especially useful for:
- E-commerce: Users can add items to cart, save preferences, then checkout as guest or create account
- Productivity apps: Users can create documents, try features, then save their work by signing up
- Games: Users can start playing immediately, then create account to save progress
- Collaboration tools: Users can join a shared workspace via link, then register to keep access
Preserve data: Unlike anonymous sessions, temporary users have real database records. When they convert, all their data (orders, documents, settings) stays linked to their account.
- Generate the migration:
rails generate rails_simple_auth:temporary_users
rails db:migrate- Add the
:temporarymodule to your User model:
class User < ApplicationRecord
authenticates_with :confirmable, :temporary
end- Enable in configuration:
RailsSimpleAuth.configure do |config|
config.temporary_users_enabled = true
config.temporary_user_cleanup_days = 7 # Auto-cleanup after 7 days
endCreate a temporary user when someone needs to use your app without signing up:
# In your controller
def try_without_account
user = User.create!(
email: "temp_#{SecureRandom.hex(8)}@temporary.local",
password: SecureRandom.hex(32),
temporary: true
)
# Sign them in
create_session_for(user)
redirect_to dashboard_path
endOr create via an invite link:
def accept_invite
# Create temporary user to access shared content
user = User.create!(temporary: true, ...)
create_session_for(user)
redirect_to shared_workspace_path(params[:workspace_id])
endWhen a temporary user is ready to create a real account:
# In your controller
def convert_account
if current_user.convert_to_permanent!(
email: params[:email],
password: params[:password]
)
redirect_to dashboard_path, notice: "Account created! Please check your email to confirm."
else
# Validation failed (email taken, password blank, etc.)
render :convert_form, status: :unprocessable_entity
end
endThe conversion:
- Updates email and password
- Sets
temporary: false - Resets
confirmed_at(requires email confirmation for new address) - Invalidates all existing sessions (security measure)
- Sends confirmation email automatically
When a temporary user signs in with a different account (or signs up), the temporary user is automatically destroyed:
Temporary User (browsing) → Signs in with existing account → Temp user deleted
Temporary User (browsing) → Creates new account → Temp user deleted
Temporary User (browsing) → Converts their temp account → Keeps same user record
This prevents orphaned temporary records and ensures clean data.
User.temporary # All temporary users
User.permanent # All permanent users
User.temporary_expired # Temporary users older than cleanup_days
current_user.temporary? # Is this a guest?
current_user.permanent? # Is this a real account?Temporary users are automatically eligible for cleanup after temporary_user_cleanup_days. Run cleanup manually or via scheduled job:
# In a rake task or background job
User.cleanup_expired_temporary!
# With custom retention period
User.cleanup_expired_temporary!(days: 14)Add to your scheduler (e.g., config/recurring.yml for Solid Queue):
cleanup_temporary_users:
schedule: every day at 3am
class: CleanupTemporaryUsersJobclass CleanupTemporaryUsersJob < ApplicationJob
def perform
count = User.cleanup_expired_temporary!
Rails.logger.info "Cleaned up #{count} expired temporary users"
end
endSubclass controllers for custom behavior:
# app/controllers/sessions_controller.rb
class SessionsController < RailsSimpleAuth::SessionsController
def after_sign_in(user)
track_login(user)
super
end
endUpdate routes to use your controller:
rails_simple_auth_routes(sessions_controller: "sessions")The gem includes a built-in mailer (RailsSimpleAuth::AuthMailer) with email templates that work out of the box. No configuration required.
| Purpose | |
|---|---|
confirmation |
Email confirmation when user signs up |
magic_link |
Passwordless sign-in link |
password_reset |
Password recovery link |
RailsSimpleAuth.configure do |config|
# Sender address for all auth emails (required)
config.mailer_sender = "auth@myapp.com"
# Or use environment variable
config.mailer_sender = ENV.fetch("MAILER_FROM", "noreply@example.com")
endFor branded emails with your own design, use a custom mailer:
# config/initializers/rails_simple_auth.rb
RailsSimpleAuth.configure do |config|
config.mailer_class = "UserMailer"
config.mailer_sender = "hello@myapp.com"
endYour custom mailer must implement these methods:
# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def confirmation(user, token)
@user = user
@confirmation_url = edit_confirmation_url(token: token)
mail(to: user.email, subject: "Confirm your email")
end
def magic_link(user, token)
@user = user
@magic_link_url = magic_link_login_url(token: token)
mail(to: user.email, subject: "Your sign-in link")
end
def password_reset(user, token)
@user = user
@reset_url = edit_password_url(token: token)
mail(to: user.email, subject: "Reset your password")
end
endCreate corresponding views in app/views/user_mailer/:
app/views/user_mailer/
├── confirmation.html.erb
├── magic_link.html.erb
└── password_reset.html.erb
Available in controllers and views:
current_user # The signed-in user (or nil)
user_signed_in? # Boolean
require_authentication # Redirects if not signed inAccess anywhere via:
RailsSimpleAuth::Current.userThe gem adds these routes:
| Method | Path | Description |
|---|---|---|
| GET | /session/new |
Sign in form |
| POST | /session |
Create session |
| DELETE | /session |
Sign out |
| GET | /sign_up |
Sign up form |
| POST | /sign_up |
Create account |
| GET | /passwords/new |
Password reset form |
| POST | /passwords |
Send reset email |
| GET | /passwords/:token/edit |
New password form |
| PATCH | /passwords/:token |
Update password |
| GET | /confirmations/new |
Resend confirmation |
| POST | /confirmations |
Send confirmation |
| GET | /confirmations/:token |
Confirm email |
| GET | /magic_link_form |
Magic link form |
| POST | /request_magic_link |
Send magic link |
| GET | /magic_link |
Login via magic link |
All authentication endpoints are rate limited using Rails 8's rate_limit DSL to prevent brute force attacks.
| Action | Limit | Period | Scope |
|---|---|---|---|
| Sign in | 5 requests | 15 minutes | per IP |
| Sign up | 5 requests | 1 hour | per IP |
| Magic link request | 3 requests | 10 minutes | per email |
| Password reset | 3 requests | 1 hour | per IP |
| Email confirmation | 3 requests | 1 hour | per IP |
RailsSimpleAuth.configure do |config|
config.rate_limits = {
sign_in: { limit: 10, period: 30.minutes },
sign_up: { limit: 3, period: 1.hour },
magic_link: { limit: 5, period: 15.minutes },
password_reset: { limit: 5, period: 1.hour },
confirmation: { limit: 5, period: 1.hour }
}
endTo disable rate limiting for a specific action, set it to nil:
config.rate_limits = {
sign_in: nil, # No rate limiting on sign in
sign_up: { limit: 5, period: 1.hour }
}When rate limited, users see a "Too many requests" error and must wait for the period to expire.
Sessions track user authentication state with IP address and user agent for security auditing.
Each session stores:
- user_id - The authenticated user
- ip_address - Client IP at sign-in time
- user_agent - Browser/device information
- created_at - When the session was created
Sessions expire after 30 days by default:
RailsSimpleAuth.configure do |config|
config.session_expiry = 30.days # Default
# config.session_expiry = 7.days # Shorter sessions
end# All sessions for a user
current_user.sessions
# Recent sessions first
current_user.sessions.recent
# Active sessions (not expired)
current_user.sessions.active
# Expired sessions
current_user.sessions.expiredExpired sessions can be cleaned up manually or via scheduled job:
# Clean up all expired sessions
RailsSimpleAuth::Session.cleanup_expired!Add to your scheduler:
class CleanupExpiredSessionsJob < ApplicationJob
def perform
count = RailsSimpleAuth::Session.cleanup_expired!
Rails.logger.info "Cleaned up #{count} expired sessions"
end
end- Password change: All sessions are invalidated when a user changes their password
- Account conversion: All sessions are invalidated when a temporary user converts to permanent
- Sign out: Only the current session is destroyed (other devices stay signed in)
RailsSimpleAuth uses a single table with role-based access — the Rails way. No separate admin models or authentication flows needed.
Add an admin column to your users table:
# Migration
add_column :users, :admin, :boolean, default: falseAdd a helper method to your model:
class User < ApplicationRecord
authenticates_with :confirmable
def admin?
admin == true
end
endclass AdminController < ApplicationController
before_action :require_admin
private
def require_admin
redirect_to root_path, alert: "Not authorized" unless current_user&.admin?
end
end
# Or as a concern
module AdminAuthentication
extend ActiveSupport::Concern
included do
before_action :require_admin
end
private
def require_admin
redirect_to root_path, alert: "Not authorized" unless current_user&.admin?
end
end# Console
User.find_by(email: "admin@example.com").update!(admin: true)
# Seeds
User.create!(email: "admin@example.com", password: "secure123", admin: true)For more complex role systems, consider adding a role enum or using an authorization gem like Pundit.
- BCrypt password hashing with salts
- Constant-time comparison prevents timing attacks
- Signed tokens for all email links
- Rate limiting on all auth endpoints
- HttpOnly cookies for session tokens
- SameSite=Lax CSRF protection
- Session invalidation on password change
- IP and user agent tracking for audit
MIT License