/ Tags: RAILS / Categories: RAILS

Rails 8 Authentication Generator — Built-in Auth Without Devise

Authentication is one of those problems that sounds simple and isn’t. Devise has solved it for Rails developers for over a decade — but it’s also a dependency that ships with opinions, a DSL, and abstractions that can make understanding your own auth code difficult. Rails 8 ships with a built-in authentication generator that produces plain, readable Ruby code you own from day one. No gem to learn, no DSL to fight, just your models and controllers.

What the Generator Produces


The Rails 8 auth generator doesn’t add a gem or install external code — it generates files directly into your app. What you get is yours: a User model, a Session model, controllers for registration and login, a Current concern, and a base controller with authentication helpers.

Setup:

rails generate authentication

This single command creates:

app/models/user.rb
app/models/session.rb
app/models/current.rb
app/controllers/sessions_controller.rb
app/controllers/passwords_controller.rb
app/controllers/concerns/authentication.rb
app/views/sessions/new.html.erb
app/views/passwords/new.html.erb
app/views/passwords/edit.html.erb
db/migrate/YYYYMMDDHHMMSS_create_users.rb
db/migrate/YYYYMMDDHHMMSS_create_sessions.rb
rails db:migrate

Done. Your app has working authentication.

The Generated Models


The generator produces clean, readable model code. No magic modules, no DSL — just Ruby.

Example:

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy

  normalizes :email_address, with: -> e { e.strip.downcase }
end

Example:

# app/models/session.rb
class Session < ApplicationRecord
  belongs_to :user
end

has_secure_password (built into Rails via ActiveModel) handles password hashing via bcrypt, adds password and password_confirmation virtual attributes, and provides authenticate for verification. The Session model stores login sessions separately from users — each login creates a new session row, making it straightforward to list active sessions or revoke access.

The sessions migration looks like:

Example:

class CreateSessions < ActiveRecord::Migration[8.0]
  def change
    create_table :sessions do |t|
      t.references :user, null: false, foreign_key: true
      t.string :ip_address
      t.string :user_agent
      t.timestamps
    end
  end
end

The Authentication Concern


The generator creates an Authentication concern that ApplicationController includes. This is where session management lives.

Example:

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

  included do
    before_action :require_authentication
    helper_method :authenticated?
  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])
  end

  def request_authentication
    session[:return_to_after_authenticating] = request.url
    redirect_to new_session_url
  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

The cookie is signed (tamper-resistant), HTTP-only (not accessible via JavaScript), and uses SameSite: Lax (CSRF protection). The session ID stored in the cookie maps to a database row — revocation is a row deletion.

Skipping Authentication for Public Actions


By default, all actions require authentication. To make specific actions public, use allow_unauthenticated_access.

Example:

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Authentication
end

Example:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  allow_unauthenticated_access only: [:index, :show]

  def index
    @posts = Post.all
  end

  def show
    @post = Post.find(params[:id])
  end

  def new
    # Requires authentication — redirects to login if not signed in
    @post = Post.new
  end
end

Accessing the Current User


The Current class (thread-safe via ActiveSupport::CurrentAttributes) provides the current user throughout the request.

Example:

# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :session
  delegate :user, to: :session, allow_nil: true
end

Example:

# In any controller
def create
  @post = Current.user.posts.build(post_params)
  # ...
end

# In any view
<%= Current.user.email_address %>

# In any model or service object
class PostPublisher
  def publish(post)
    post.update!(
      published: true,
      published_by: Current.user
    )
  end
end

Current.user is available anywhere during the request cycle — controllers, views, models, service objects — without needing to pass it explicitly.

Password Reset Flow


The generator includes a password reset flow via email token.

Example:

# app/controllers/passwords_controller.rb (generated)
class PasswordsController < ApplicationController
  allow_unauthenticated_access

  def new; end

  def create
    if (user = User.find_by(email_address: params[:email_address]))
      PasswordsMailer.reset(user).deliver_later
    end
    # Always redirect regardless — don't leak whether email exists
    redirect_to new_session_url, notice: "Check your email for reset instructions."
  end

  def edit
    @user = User.find_signed!(params[:token], purpose: :password_reset)
  rescue ActiveSupport::MessageVerifier::InvalidSignature
    redirect_to new_session_url, alert: "Password reset link is invalid or has expired."
  end

  def update
    @user = User.find_signed!(params[:token], purpose: :password_reset)
    @user.update!(params.permit(:password, :password_confirmation))
    redirect_to new_session_url, notice: "Password updated. Please sign in."
  end
end

Signed tokens are generated with User#signed_id(purpose:, expires_in:) — Rails’ built-in ActiveRecord::SignedId. No random token column needed in the database.

Pro-Tip: After generating auth, audit what you actually need and delete what you don’t. If your app uses OAuth only (GitHub login, Google login), you may not need the passwords controller at all. The generator’s value is not that it produces a perfect final solution — it’s that it produces readable, idiomatic Rails code that you can read, understand, and modify without fighting a third-party DSL. Read every generated file before shipping to production. Knowing exactly what your authentication code does is not optional.

Conclusion


Rails 8’s authentication generator is a meaningful shift in how Rails approaches auth. Rather than hiding implementation behind a dependency, it puts plain Ruby code in your app that you can read, understand, and own. It covers the common cases — login, logout, sessions, password reset — with secure defaults (signed cookies, bcrypt, signed tokens). For applications that need OAuth or more complex access control, it’s a foundation to extend, not a cage to work around.

FAQs


Q1: Should I use the Rails 8 auth generator or Devise?
It depends on your needs. Devise offers more out of the box — OAuth, confirmable, lockable, token auth. The generator is better if you want to understand and control your auth code, or if Devise’s feature set is overkill. For simple email/password auth on Rails 8, the generator is the cleaner starting point.

Q2: Is has_secure_password production-ready?
Yes. It uses bcrypt with a configurable cost factor (default: 12). BCrypt is battle-tested for password hashing. has_secure_password has been in Rails since version 3.1.

Q3: How do I add OAuth login alongside the generated auth?
Add OmniAuth to your Gemfile, configure the provider, and create a separate authentication flow that finds or creates a user by provider UID. The generated auth code doesn’t interfere — they can coexist.

Q4: How do I implement “remember me” functionality?
The generated cookie is already permanent (20-year expiry). For session-only cookies, change cookies.signed.permanent to cookies.signed in the start_new_session_for method.

Q5: Can I use this generator on Rails 7?
The generator was introduced with Rails 8. For Rails 7, you can manually replicate the pattern or use the authentication-zero gem, which generates similar code and supports Rails 7.

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More