How to Add Auth to a Rails App (Rails 8 Generator vs Devise)
A practical guide to adding authentication to a Rails app: the Rails 8 built-in generator, has_secure_password, sessions, and Devise — with code.
For years, adding auth to a Rails app meant one of two things: install Devise, or write your own with has_secure_password and hope you didn’t miss anything. Rails 8 changed that. There’s now an official authentication generator that scaffolds a secure, convention-based setup straight into your app — no gem required.
So the question in 2026 isn’t “Devise or roll my own.” It’s “the built-in generator, or Devise, or something else?” This guide walks through adding auth to a Rails app with each option, the real code, and where each one fits.
The Rails 8 Built-in Generator
Rails 8 ships an authentication generator that produces a sane email/password system out of the box:
bin/rails generate authentication
bin/rails db:migrate
That gives you a real, secure foundation:
- A
Usermodel usinghas_secure_password(bcrypt under the hood, so passwords are hashed, never stored plaintext). - A
Sessionmodel that persists sessions in the database with a unique token, plusip_addressanduser_agentcolumns. - A
Currentmodel built onActiveSupport::CurrentAttributesfor accessing the logged-in user anywhere in a request. - A
SessionsController(login/logout) andPasswordsController(password reset), with views. - Migrations: a
userstable with a uniquely indexedemail_addressandpassword_digest, and asessionstable.
The generated User model is roughly:
class User < ApplicationRecord
has_secure_password
has_many :sessions, dependent: :destroy
normalizes :email_address, with: ->(e) { e.strip.downcase }
end
And the concern it wires into ApplicationController gives you an authenticate helper plus Current.user:
class DashboardController < ApplicationController
def show
@user = Current.user # the authenticated user
end
end
# Allow a public page through the before_action:
class HomeController < ApplicationController
allow_unauthenticated_access only: :index
end
Two things to know. First, sessions live in the database, so revoking one is a single destroy — genuinely better than a stateless cookie for kicking someone out. Second, the generator deliberately omits a registration flow. It’s focused on secure authentication, not onboarding, so you add the signup action yourself:
class RegistrationsController < ApplicationController
allow_unauthenticated_access only: [:new, :create]
def create
user = User.new(user_params)
if user.save
start_new_session_for(user) # helper from the generator
redirect_to dashboard_path
else
render :new, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:email_address, :password, :password_confirmation)
end
end
Best for: Most new Rails apps. You own readable code, sessions are revocable, and there’s no gem to track.
Option 2: Devise
Devise is still the most-installed auth gem in Rails, and for good reason — it bundles a lot: registration, password reset, email confirmation, account locking, “remember me,” and an ecosystem of add-ons.
# Gemfile
gem "devise"
bundle install
bin/rails generate devise:install
bin/rails generate devise User
bin/rails db:migrate
Then protecting controllers is one line:
class DashboardController < ApplicationController
before_action :authenticate_user!
def show
@user = current_user
end
end
The trade-off: Devise is convention-heavy and somewhat magic. Customizing flows means overriding controllers and learning Devise’s structure, and you’re depending on the gem keeping pace with Rails releases. For complex needs (confirmation emails, lockable accounts, omniauth providers) it saves real time. For a simple app, it can be more machinery than you need now that the built-in generator exists.
Best for: Apps that want confirmation/lockable/recoverable features without building them, or teams already fluent in Devise.
Option 3: OAuth / “Sign in with Google”
For social login, omniauth plus a provider strategy (e.g. omniauth-google-oauth2) is the standard. The flow: redirect to the provider, handle the callback, find-or-create the user, then start a session — either with the Rails 8 generator’s start_new_session_for or Devise’s omniauth integration.
class SessionsController < ApplicationController
allow_unauthenticated_access only: :omniauth
def omniauth
auth = request.env["omniauth.auth"]
user = User.find_or_create_by(email_address: auth.info.email) do |u|
u.password = SecureRandom.hex(32) # random; they log in via OAuth
end
start_new_session_for(user)
redirect_to dashboard_path
end
end
Always verify the OmniAuth CSRF state (the omniauth-rails_csrf_protection gem handles this). If you’re weighing OAuth against passwordless email, the magic link authentication guide lays out the trade-offs.
Which to Pick
| Option | Code you own | Built-in extras | Best for |
|---|---|---|---|
| Rails 8 generator | High (it’s yours) | Auth + reset, DB sessions | Most new apps |
| Devise | Lower (gem magic) | Confirmation, lockable, etc. | Feature-rich auth |
| OmniAuth | Medium | Social providers | ”Sign in with Google” |
For a fresh Rails 8 SaaS, start with the built-in generator. Add OmniAuth when users expect social login. Reach for Devise only if you specifically want its extra modules.
The Part None of These Cover: Payments
Every option above gets you a logged-in user. None of them charge that user money. For a SaaS you still build the billing layer yourself: the stripe gem or pay, a Stripe customer tied to each User, webhook handling for subscription events, plan gating in your controllers, and a customer portal. Then you keep session state and subscription state in sync for the life of the app.
That’s the work Beag removes. Instead of bolting a Stripe integration onto your Rails auth, Beag bundles server-validated authentication and Stripe subscriptions together. After login, the user’s plan and status are available to your frontend, so gating a premium feature is a plan check rather than a billing system:
if %w[pro enterprise].include?(current_user.plan)
# grant access
end
No webhook controllers, no customer sync, no second system to reconcile with your users table. You keep Rails for your app and skip building payments from scratch.
A Practical Plan
- Run the Rails 8 generator and add your own registration action.
- Lock down session cookies and put password reset on a real mailer.
- Add OmniAuth if users expect “Sign in with Google.”
- When it’s time to charge, don’t hand-roll auth-plus-Stripe — use an all-in-one so identity and billing stay aligned.
Wire auth up before your features. Every model gets an owner and every controller gets a guard — do it first and everything after assumes a known user.
Add auth and Stripe payments to your Rails app fast. Get started with Beag — no webhook controllers, no billing system to build.
For more guides, see how to add auth to a Next.js app or browse the Beag blog.
Ready to Make Money From Your SaaS?
Turn your SaaS into cash with Beag.io. Get started now!
Start 7-day free trial →