Rails 8

The Rails 8 auth generator vs Devise

What bin/rails generate authentication gives you, what it leaves out, when Devise still wins, and how to move an existing app.

ANAbhay Nikam, Founder, Navio Labs

For fifteen years, the first line of most Rails apps’ Gemfiles after rails was devise. Rails 8 changed the default answer by shipping its own authentication generator. Now every new project starts with the same question: do we still need Devise?

Short answer: often not. Here’s how we decide.

What the generator actually gives you

bin/rails generate authentication
bin/rails db:migrate

You get real, readable code in your app — not a gem you configure:

  • Models: User (with has_secure_password and has_many :sessions), Session (stores ip_address and user_agent), and Current for the signed-in user per request.
  • Controllers: SessionsController for sign-in and sign-out, and PasswordsController for password resets.
  • A concern: Authentication, included in ApplicationController. Every controller requires sign-in by default; you opt out with allow_unauthenticated_access.
  • A mailer: PasswordsMailer, with HTML and text templates for the reset email.
  • Views: sign-in, forgot-password and reset-password forms.
  • Routes: resource :session and resources :passwords, param: :token.
  • Migrations for users (email_address, password_digest) and sessions.
  • bcrypt added to your Gemfile.

Sessions are database-backed. Each sign-in creates a Session row and stores its id in a signed, permanent cookie. Signing out deletes the row. That gives you something Devise doesn’t do out of the box: a list of active sessions you can show users and revoke.

The generated SessionsController also rate-limits sign-in attempts:

class SessionsController < ApplicationController
  allow_unauthenticated_access only: %i[ new create ]
  rate_limit to: 10, within: 3.minutes, only: :create,
    with: -> { redirect_to new_session_url, alert: "Try again later." }

  def create
    if user = User.authenticate_by(params.permit(:email_address, :password))
      start_new_session_for user
      redirect_to after_authentication_url
    else
      redirect_to new_session_path, alert: "Try another email address or password."
    end
  end

  # ...
end

authenticate_by is worth noticing: it takes the same time whether or not the email exists, which protects against timing attacks that reveal who has an account.

What it deliberately leaves out

  • Sign-up. There’s no registration flow. You write the controller, form and route yourself. It’s about 30 lines.
  • Password rules. has_secure_password checks presence, confirmation and a 72-byte maximum. It does not enforce a minimum length or complexity — add a validation.
  • Email confirmation, account locking, “remember me” options, OAuth, magic links and two-factor authentication.

That isn’t a gap so much as a philosophy: you get a small, correct core and you own the rest.

Adding sign-up

Here’s the minimal version we start with:

# config/routes.rb
resource :registration, only: %i[ new create ]
# app/controllers/registrations_controller.rb
class RegistrationsController < ApplicationController
  allow_unauthenticated_access
  rate_limit to: 10, within: 3.minutes, only: :create

  def new
    @user = User.new
  end

  def create
    @user = User.new(params.expect(user: [ :email_address, :password, :password_confirmation ]))

    if @user.save
      start_new_session_for @user
      redirect_to root_path
    else
      render :new, status: :unprocessable_entity
    end
  end
end
# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
  has_many :sessions, dependent: :destroy

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

  validates :email_address, presence: true, uniqueness: true
  validates :password, length: { minimum: 12 }, allow_nil: true
end

Add a form, and you have registration. Add email confirmation the same way: a confirmed_at column, a signed token via generates_token_for, and a mailer.

When we still reach for Devise

Devise remains a good choice when you need several of these at once:

  • OAuth / social sign-in through OmniAuth, already well integrated with Devise.
  • Confirmable, lockable, timeoutable and trackable behavior on day one.
  • Several user types (admins, customers, partners) with separate sign-in flows.
  • An ecosystem gem that expects Devise, such as some admin panels or multi-tenancy libraries.
  • A team that already knows Devise well and an app where auth isn’t interesting.

If your auth requirements are a checklist of Devise modules, writing all of that by hand is not a good use of your time.

When the generator wins

  • New apps with email-and-password sign-in. That’s most B2B tools and internal apps.
  • Teams that want to understand their auth code. Everything is in app/; nothing hides in a gem.
  • Apps that want session management — “sign out other devices” is a few lines because sessions are rows.
  • Apps that would otherwise fight Devise with custom controllers, overridden views and monkey-patches.

Moving an existing app from Devise

We don’t do this unless there’s a reason — fighting Devise customizations, or a planned rewrite of auth anyway. When there is, the key fact is that Devise and has_secure_password both use bcrypt, so you can keep users’ passwords.

  1. Generate, don’t migrate yet. Run the generator and keep its controllers, concern and views. Delete its create_users migration if you already have a users table.
  2. Rename the columns. Devise uses email and encrypted_password. Either rename them to email_address and password_digest in a migration, or adjust the generated code to use your column names. Renaming is cleaner; existing bcrypt hashes keep working.
  3. Create the sessions table from the generated migration.
  4. Swap the helpers. Replace authenticate_user! with the concern’s default behavior (and allow_unauthenticated_access where needed), current_user with Current.user, and Devise route helpers with the new ones.
  5. Rebuild what you used. Confirmable, lockable or OAuth won’t come for free. List the Devise modules you actually enabled before you start.
  6. Ship behind a flag. Run both sign-in paths for a short period if you can, and keep Devise in the Gemfile until nobody hits its routes.
  7. Expect everyone to sign in again. Devise’s session cookie won’t be recognized by the new system. Tell users before you deploy.

Our rule of thumb

If the requirements fit in one sentence — “email and password, with password reset” — use the generator. If they take a paragraph, start with Devise. Either way, add rate limiting to every form that creates sessions or accounts, and enforce a sensible minimum password length.

New to the rest of Rails 8? Start with what’s new in Rails 8. If you’d rather have someone review or rebuild your auth with you, that’s what we do.

Sources

More blogs

Rather have someone do this with you?

That’s what we do. Tell us where you’re stuck and you’ll hear back from the founder within one business day.

Start a project with Navio Labs
Get help with this