35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Rails 8 Authentication: The Built-in Generator That Replaces Devise

Rails 8 Authentication: The Built-in Generator That Replaces Devise

TTB Software
rails
How to set up authentication in Rails 8 using the new built-in generator. Step-by-step guide covering sessions, password resets, and when you still need Devise.

Rails 8 ships with bin/rails generate authentication, a built-in authentication generator that creates session-based auth without any gems. If you’ve been reaching for Devise on every new project, this changes the calculus.

The generator produces a User model, a Session model, sign-in/sign-out controllers, password reset flow, and the Current object wiring — all in about 200 lines of code you own completely.

Running the Generator

rails new myapp
cd myapp
bin/rails generate authentication
bin/rails db:migrate

That’s it. You now have:

  • User model with has_secure_password and email normalization
  • Session model tracking active sessions per user
  • SessionsController for sign-in and sign-out
  • PasswordsController for reset requests
  • Authentication concern you include in ApplicationController
  • Rate limiting on login attempts via rate_limit

What the Generated Code Actually Does

The Authentication concern is the core. It provides require_authentication as a before action and exposes Current.user throughout your app:

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

  included do
    before_action :require_authentication
  end

  private

  def require_authentication
    resume_session || request_authentication
  end

  def resume_session
    if session_record = find_session_by_cookie
      set_current_session(session_record)
    end
  end

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

  def request_authentication
    redirect_to new_session_path
  end

  def set_current_session(session_record)
    Current.session = session_record
  end
end

No magic. No 50-module deep stack trace when something breaks. You can read the entire auth flow in five minutes.

Session Management

Each sign-in creates a Session record tied to the user. The session token is stored in a signed cookie. This means you can list active sessions, revoke specific ones, and track login history — features that took custom work with Devise.

class SessionsController < ApplicationController
  allow_unauthenticated_access only: [:new, :create]
  rate_limit to: 10, within: 3.minutes, only: :create

  def create
    if user = User.authenticate_by(
      email: params[:email],
      password: params[:password]
    )
      start_new_session_for(user)
      redirect_to root_path
    else
      redirect_to new_session_path,
        alert: "Invalid email or password."
    end
  end

  def destroy
    terminate_session
    redirect_to new_session_path
  end
end

The rate_limit macro is new in Rails 8. It uses the request’s IP by default and returns HTTP 429 when exceeded. No Rack::Attack configuration needed for basic login throttling.

Password Resets

The generator creates a token-based password reset flow using generates_token_for:

class User < ApplicationRecord
  has_secure_password

  generates_token_for :password_reset, expires_in: 15.minutes do
    password_salt&.last(10)
  end

  normalizes :email, with: ->(email) { email.strip.downcase }
end

generates_token_for was added in Rails 7.1. The token automatically invalidates when the password changes (because password_salt changes), and it expires after 15 minutes. No separate token column in the database. No cleanup cron jobs.

The PasswordsController handles the reset flow:

class PasswordsController < ApplicationController
  allow_unauthenticated_access

  def create
    if user = User.find_by(email: params[:email])
      PasswordMailer.reset(user).deliver_later
    end
    redirect_to new_session_path,
      notice: "Check your email for reset instructions."
  end

  def update
    if user = User.find_by_token_for(:password_reset, params[:token])
      user.update!(password: params[:password])
      redirect_to new_session_path,
        notice: "Password updated. Please sign in."
    else
      redirect_to new_password_path,
        alert: "Invalid or expired token."
    end
  end
end

Notice the constant-time response on create — it redirects with the same message whether the email exists or not. No user enumeration out of the box.

Customizing the Generated Code

Since you own all the code, customization is straightforward. Here are patterns I’ve used in production:

Adding Remember Me

# In SessionsController#create, after start_new_session_for(user):
if params[:remember_me] == "1"
  cookies.signed.permanent[:session_id] = {
    value: Current.session.id,
    httponly: true,
    same_site: :lax
  }
end

Multi-device Session Management

The Session model makes this natural:

class SessionsController < ApplicationController
  def index
    @sessions = Current.user.sessions.order(created_at: :desc)
  end

  def destroy_other
    Current.user.sessions.where.not(id: Current.session.id).destroy_all
    redirect_to sessions_path, notice: "Other sessions revoked."
  end
end

Adding User Registration

The generator deliberately omits registration. Add it yourself:

class RegistrationsController < 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
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation)
  end
end

When You Still Need Devise

The built-in generator covers the 80% case. You’ll want Devise (or another gem) when you need:

  • OAuth/OmniAuth integration — social logins require more plumbing than the generator provides
  • Confirmable emails — the generator has no email verification flow
  • Lockable accounts — beyond rate limiting, if you need account lockout after N failures
  • Complex role-based access — Devise + Pundit/CanCanCan is a well-tested stack

For API-only apps, you’ll also want to swap the cookie-based sessions for token auth. The generator’s session model gives you a good foundation, but you’ll need to add token generation and header-based authentication yourself.

Performance Considerations

Each authenticated request hits the sessions table to load the session record. On a busy app, add a database index (the migration already creates one on user_id) and consider caching:

def find_session_by_cookie
  Rails.cache.fetch("session:#{cookies.signed[:session_id]}", expires_in: 5.minutes) do
    Session.find_by(id: cookies.signed[:session_id])
  end
end

Invalidate the cache on sign-out. For most apps under 10K concurrent users, the database lookup without caching is fine — it’s a primary key lookup.

Migration from Devise

If you’re moving an existing Devise app to built-in auth:

  1. Generate the authentication files
  2. Keep your existing users table — just ensure password_digest exists (Devise uses encrypted_password, so you’ll need a migration to rename or add the column)
  3. Users must reset their passwords — Devise’s bcrypt format is compatible with has_secure_password, but the column name differs
  4. Replace authenticate_user! with require_authentication in your controllers
  5. Swap current_user references to Current.user
  6. Move any Devise callback logic into your new controllers

This is a gradual process. Run both systems in parallel during migration by checking both authentication methods in your concern. I’ve done this migration on a Rails app with multi-tenancy and it took about two days for a medium-sized codebase.

Frequently Asked Questions

Does Rails 8 authentication work with API-only applications?

Not directly. The generator creates cookie-based session auth designed for server-rendered apps. For APIs, you’ll need to modify the Authentication concern to read tokens from the Authorization header instead of cookies. The Session model still works — just generate a token on creation and authenticate via header rather than signed cookie.

Can I use the Rails 8 auth generator with an existing User model?

Yes. The generator checks for an existing User model and skips creating a new one. It will add has_secure_password and the email normalization if missing. Run bin/rails generate authentication and review the generated migration — it only adds columns that don’t exist yet.

How does the built-in rate limiting compare to Rack::Attack?

The rate_limit macro uses Rails’ built-in cache store (usually Redis or Memcached in production) and limits by IP address. Rack::Attack offers more flexibility — throttling by user, custom discriminators, blocklists, and safelists. For login throttling, the built-in approach is sufficient. For comprehensive API rate limiting across multiple endpoints, Rack::Attack remains the better tool.

Is the generated authentication code secure enough for production?

Yes. It uses has_secure_password (bcrypt), signed cookies, constant-time token comparison, rate limiting, and prevents user enumeration on password reset. These are the same primitives Devise uses internally. The main security addition you should make is enforcing HTTPS (which Rails 8 does by default in production) and adding CSRF protection (also default). For zero-downtime deployments, the session table migration is straightforward.

Should I delete the generated code and write my own?

No. The generated code is meant to be modified, not replaced. Think of it as a well-tested starting point. Read through it, understand it, then adjust. The whole point is that you own the code — unlike Devise, where customization means monkey-patching gems or decorating controllers through a deeply nested inheritance chain.

T

About the Author

Roger Heykoop is a senior Ruby on Rails developer with 19+ years of Rails experience and 35+ years in software development. He specializes in Rails modernization, performance optimization, and AI-assisted development.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation