Rails 8 ships its own authentication, so you can understand and own every line instead of relying on a large gem.
Rails 8 includes an authentication generator: bin/rails generate authentication. It creates:
- A
Usermodel withhas_secure_passwordand anemail_addresscolumn, plus aSessionmodel that stores each login (with IP and user agent) in the database. - An
Authenticationconcern included inApplicationController, which requires login for every action by default. Opt out per controller withallow_unauthenticated_access only: %i[index show]. SessionsControllerfor sign-in and sign-out, rate-limited withrate_limit to: 10, within: 3.minutes, only: :create, andPasswordsControllerwith a mailer for password resets.Current.userandCurrent.session, available anywhere during a request throughCurrent(anActiveSupport::CurrentAttributesclass).
There is deliberately no sign-up flow; you add a registration controller that fits your app.
has_secure_password (needs the bcrypt gem) adds password and password_confirmation virtual attributes, stores a bcrypt password_digest, and provides authenticate. The generator uses User.authenticate_by(email_address:, password:), which takes constant time whether or not the user exists, so attackers can't detect valid emails by timing.
The session id lives in a signed, httponly permanent cookie. Signing out destroys the Session row, which makes revoking all of a user's sessions easy.
For OAuth, add OmniAuth; Devise remains an option for apps that need many features out of the box. Authorization (who may do what) is separate: check ownership in controllers, e.g. Current.user.articles.find(params[:id]), or use a gem like Pundit.
Example
# $ bin/rails generate authentication && bin/rails db:migrate
# app/models/user.rb (generated)
class User < ApplicationRecord
has_secure_password
has_many :sessions, dependent: :destroy
normalizes :email_address, with: ->(e) { e.strip.downcase }
end
# app/controllers/sessions_controller.rb (generated, abridged)
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
# Your controllers: scope records to the signed-in user (authorization)
class ArticlesController < ApplicationController
def edit = @article = Current.user.articles.find(params[:id]) # 404 for others' articles
endRemember
- bin/rails generate authentication creates User, Session, controllers and the Authentication concern
- Every action requires login unless allow_unauthenticated_access is used
- authenticate_by and rate_limit defend against timing and brute-force attacks
Watch out for
- Treating authentication as authorization: a logged-in user can still load other users' records unless you scope queries.