Ruby

Devise for Ruby on Rails: Setup, Modules & Rails 8 Auth

Devise for Ruby on Rails: Setup, Modules & Rails 8 Auth

Devise is a full-featured authentication gem for Ruby on Rails, built on Warden. It provides registration, login, password recovery, session timeout, and account locking via 10 composable modules. Install it with bundle add devise, then run rails g devise:install and rails g devise User. It works with Rails 7 and 8; Rails 8's built-in generator covers only simpler needs.

This guide takes you from installation to production concerns: the module system, helpers, strong parameters, custom views and controllers, social login with OmniAuth, JWT authentication for APIs, and login tracking with Authtrail. Every command and code sample was tested on Rails 8.1 with Devise 5.0.

Introducing Devise for Ruby

Devise is an authentication library built on top of Warden, a Rack-based authentication framework. Warden verifies user identities with secure session strings and keeps unauthenticated requests away from restricted resources. But because Warden works purely at the Rack level, it ships no controller actions, views, or helpers. Devise adds that whole Rails layer for you: routes, controllers, views, mailers, and helper methods.

Devise’s other defining feature is modularity. Authentication is split into 10 modules, and you activate only the ones your app needs. A basic app might use five; an app with email confirmation, account locking, and social login turns on more.

If your needs are small, you don’t have to use Devise at all: Devise is compared against Rails 8’s built-in generator later in this article.

Modules in Devise for Ruby

Here are all 10 modules, straight from Devise’s source. The first five come enabled in a freshly generated model:

ModuleWhat It DoesOn by Default?
database_authenticatableHashes a password with bcrypt, stores it, and verifies it on sign-inYes
registerableLets users sign up, edit, and delete their accountsYes
recoverableSends password reset emails and handles the reset flowYes
rememberableRemembers a signed-in user with a signed cookieYes
validatableValidates email format and password lengthYes
confirmableEmails a confirmation link on sign-up and blocks unconfirmed accountsNo
lockableLocks an account after too many failed logins; unlocks by email or after a timeoutNo
timeoutableExpires sessions after a period of inactivityNo
trackableRecords sign-in count, timestamps, and IP addressesNo
omniauthableAdds OmniAuth support for external providers like GitHub or GoogleNo

To activate an extra module, add its symbol to the devise call in your model and uncomment its columns in the generated migration (more on that in the generated model notes). The Devise module documentation describes each one’s configuration options.

Devise Helpers and Filters

Once a User model is in place, Devise mixes these helpers into your controllers and views:

HelperWhat It Gives You
current_userThe signed-in user, or nil
user_signed_in?true when a user is signed in
user_sessionThe signed-in user’s session data
authenticate_user!A filter that redirects unauthenticated visitors to sign-in
new_user_session_pathThe sign-in page (/users/sign_in)
destroy_user_session_pathSigns the user out (/users/sign_out)
new_user_registration_pathThe sign-up page (/users/sign_up)
edit_user_registration_pathThe account edit page (/users/edit)

The names derive from your model. A Member model gives you current_member and authenticate_member! instead.

To require login for a controller, use the filter:

Ruby
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  before_action :authenticate_user!
end

Devise vs. Rails 8’s Built-In Authentication Generator

Rails 8 added rails generate authentication, a built-in generator that writes a session-based login system directly into your app. It creates a User model with has_secure_password, a database-backed Session model, sessions and passwords controllers, a password reset mailer, and rate limiting on the login endpoint. What it doesn’t create is everything else:

CapabilityDeviseRails 8 Generator
Modules10 composable modules, including confirmation, locking, and timeoutFixed feature set: login and password reset
User registrationFull sign-up, edit, and delete flows via registerableNot generated; you build sign-up yourself
Generated viewsSign-in, sign-up, password, confirmation, unlock, and mailer viewsSign-in and password reset views only
OmniAuthBuilt in via omniauthableNot supported
JWT / API authVia the devise-jwt extensionNot supported; sessions are cookie-based
Password resetYes, via recoverableYes, with a token-based mailer flow
Maintenance burdenA gem dependency you upgradeA few hundred lines of app code you own and maintain

A one-line verdict per use case:

  • Internal tool or MVP with email/password login: the built-in generator is enough.
  • Product with self-serve sign-up, confirmation emails, or account locking: Devise.
  • API backend for a mobile or JavaScript client: Devise with devise-jwt.
  • “Log in with GitHub/Google” requirements: Devise with omniauthable.
  • You want to own and audit every line of auth code: the generator, extended by hand.

Getting Started: Installing Devise

Creating a Rails 8 App

Devise 5.0 requires Rails 7.0 or newer, so a current Rails 8 app works out of the box. Create one:

Shell
rails new tasks_app
cd tasks_app

Devise redirects to your root route after sign-in, so define one before you begin (for example, root "home#index" pointing at a controller you’ve created).

Add Devise and run its installer:

Shell
bundle add devise
bin/rails g devise:install

bundle add pins gem "devise", "~> 5.0" in your Gemfile. The installer creates config/initializers/devise.rb (where every module option lives) and a locale file, then prints a short setup checklist. The one item required for all apps is the mailer host, which password reset emails need:

Ruby
# config/environments/development.rb
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }

Generating a Devise User Model

Devise needs a model to authenticate. It’s usually called User or Admin:

Shell
bin/rails g devise User
bin/rails db:migrate

The generator creates app/models/user.rb and a migration for the users table, and adds devise_for :users to your routes. That one routing line mounts everything: sign-in, sign-up, password reset, and account editing.

The Generated User Model

The generated model enables the five default modules:

Ruby
# app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end

The migration mirrors this: columns for trackable, confirmable, and lockable are there, commented out. To turn a module on, add its symbol to the devise call and uncomment its columns before you migrate. Enabling a module later means adding the same columns in a new migration.

Devise and Ruby on Rails’ Strong Parameters

Rails’ strong parameters require request parameters to be explicitly permitted before mass assignment. Devise applies the same rule to its own controllers, with three sanitizer actions:

  • sign_up (Devise::RegistrationsController#create): permits email, password, and password_confirmation.
  • sign_in (Devise::SessionsController#create): permits the authentication keys, email and password.
  • account_update (Devise::RegistrationsController#update): permits email, password, password_confirmation, and current_password.

To permit extra keys, add a filter to ApplicationController. Here we allow a username on sign-up:

Ruby
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?
 
  protected
 
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
  end
end

Strong parameters only permit scalar values by default. To permit an array or hash (say, a user picking multiple roles from checkboxes), pass a block to devise_parameter_sanitizer.permit and call permit yourself with { roles: [] } among the keys. The Devise documentation on strong parameters shows the pattern.

Customizing Devise Views

Devise is a Rails engine, so its views for signing in, signing up, and resetting passwords live inside the gem. To change them, copy them into your app:

Shell
bin/rails g devise:views

The views land in app/views/devise:

Devise folder structure

Let’s extend the earlier strong parameters example and add a username field to sign-up. Give the users table the column with bin/rails g migration AddUsernameToUsers username:string, migrate, then edit the registration view:

erb
<!-- app/views/devise/registrations/new.html.erb -->
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <div class="field">
    <%= f.label :username %><br />
    <%= f.text_field :username, autofocus: true %>
  </div>
  <!-- ... -->
<% end %>

If you only want to customize a couple of views, scope the generator with the -v flag. This copies only the login and sign-up views:

Shell
bin/rails g devise:views -v sessions registrations

Customizing Devise Controllers and Routes

Views can only get you so far. To change authentication behavior, generate Devise’s controllers into your app under a scope (here, users):

Shell
bin/rails g devise:controllers users
Devise controllers folder structure

Point Devise’s routes at the generated controllers you want to customize:

Ruby
# config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    sessions: "users/sessions",
    registrations: "users/registrations"
  }
end

Each generated controller subclasses its Devise counterpart, so you override only what you need and call super for the rest. Here we notify an admin on every new registration:

Ruby
# app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  def create
    super do |user|
      AdminMailer.signup_notification(user).deliver_later if user.persisted?
    end
  end
end

super runs Devise’s own create action and yields the new user to your block, so the standard flow (validation, sign-in, redirects) stays intact.

Authentication with OmniAuth for Ruby

Most apps now offer “Log in with GitHub” or “Log in with Google” alongside a password form. In the Ruby world, OmniAuth, a library that gives every OAuth provider one unified API, handles that multi-provider flow. Each provider ships as its own strategy gem, like omniauth-github or omniauth-google-oauth2.

Devise’s omniauthable module wires OmniAuth into the authentication flow you already have: Devise keeps handling passwords, sessions, and helpers, while OmniAuth handles the external provider handshake.

Getting Started with OmniAuth and Devise

Let’s add GitHub login to the app from the first half of this guide.

Install OmniAuth Gems

Shell
bundle add omniauth-github omniauth-rails_csrf_protection

omniauth-github pulls in OmniAuth itself. OmniAuth 2.0+ only accepts POST requests to the OAuth flow, and the omniauth-rails_csrf_protection gem wires Rails’ CSRF token verification into that request phase. Together they close off the request forgery attack described in CVE-2015-9284.

Create a New GitHub OAuth App

On GitHub, go to Settings → Developer settings → OAuth Apps → New OAuth App. Fill in:

  • Application name: anything that identifies your app.
  • Homepage URL: http://localhost:3000 in development; your real domain in production.
  • Authorization callback URL: http://localhost:3000/users/auth/github/callback. Devise’s OmniAuth callbacks follow the pattern /users/auth/<provider>/callback.

Register the app, generate a client secret, and store both the client ID and secret in environment variables. The secret is shown only once.

Configure the Devise Initializer

Uncomment and edit the GitHub line in the Devise initializer:

Ruby
# config/initializers/devise.rb
config.omniauth :github, ENV["GITHUB_CLIENT_ID"], ENV["GITHUB_CLIENT_SECRET"], scope: "user:email"

The user:email scope is enough for authentication; only request broader scopes if your app calls the GitHub API on the user’s behalf.

Make the Devise Model Omniauthable

GitHub identifies users by provider name and UID, so add both columns:

Shell
bin/rails g migration AddOmniauthToUsers provider:string uid:string
bin/rails db:migrate

Then enable the module and add a method that finds or creates a user from the OAuth payload:

Ruby
# app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable,
         :omniauthable, omniauth_providers: [:github]
 
  def self.from_omniauth(auth)
    where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
      user.email = auth.info.email
      user.password = Devise.friendly_token[0, 20]
    end
  end
end

The random Devise.friendly_token password satisfies validatable for accounts that only ever sign in through GitHub.

Create the OmniAuth Callbacks Controller

If you generated Devise’s controllers earlier, users/omniauth_callbacks_controller.rb already exists. Give it a github action:

Ruby
# app/controllers/users/omniauth_callbacks_controller.rb
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def github
    @user = User.from_omniauth(request.env["omniauth.auth"])
    sign_in_and_redirect @user, event: :authentication
  end
 
  def failure
    redirect_to root_path
  end
end

And route Devise’s OmniAuth callbacks to it:

Ruby
# config/routes.rb
devise_for :users, controllers: {
  omniauth_callbacks: "users/omniauth_callbacks"
}

OmniAuth 2.0+ requires the authorization request to be a POST, so use button_to rather than a plain link:

erb
<!-- app/views/devise/sessions/new.html.erb -->
<%= button_to "Sign in with GitHub", user_github_omniauth_authorize_path, data: { turbo: false } %>

The user_github_omniauth_authorize_path helper (which resolves to /users/auth/github) is defined automatically once the model is omniauthable with the :github provider.

API Authentication with Devise for Ruby

Browser authentication rides on cookies, but API clients (mobile apps, SPAs, other services) authenticate with tokens instead — usually JSON Web Tokens (JWTs) passed in the Authorization header. The devise-jwt gem extends Devise to issue and revoke JWTs.

This section assumes an API-only app, created with rails new api_app --api.

The JWT-based Authentication Flow

  • A client signs in by POSTing credentials to the API.
  • The API responds with a signed JWT in the Authorization response header.
  • The client sends that token back in the Authorization header on every request.
  • Signing out hits Devise’s session destroy action, which revokes the token.

Setting Up CORS

If browser-based clients on other origins will call your API, configure cross-origin resource sharing with the rack-cors gem (bundle add rack-cors):

Ruby
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "app.example.com"
 
    resource "*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      expose: ["Authorization"]
  end
end

The expose: ["Authorization"] line matters: without it, browsers hide the response header that carries your token. Lock origins down to the domains that need access rather than "*" wherever you can.

Add Devise and Devise-JWT Gems to Your Rails App

Shell
bundle add devise devise-jwt
bin/rails g devise:install
bin/rails g devise User
bin/rails db:migrate

Since an API app has no HTML pages to navigate, tell Devise so, and give devise-jwt a signing key. Generate a dedicated key with bin/rails secret (don’t reuse secret_key_base), store it in an environment variable, and add both settings to the initializer:

Ruby
# config/initializers/devise.rb
Devise.setup do |config|
  # ...
  config.navigational_formats = []
 
  config.jwt do |jwt|
    jwt.secret = ENV["DEVISE_JWT_SECRET"]
  end
end

Generate and Configure the Models

JWTs are stateless: the server signs them but stores nothing, so by default there’s no way to log a user out. devise-jwt solves this with revocation strategies. It ships three: JTIMatcher (each user row stores the ID of their one valid token), Denylist (revoked token IDs go into a table that every request is checked against), and Allowlist (valid tokens are stored per user). We’ll use the denylist:

Shell
bin/rails g model jwt_denylist

Replace the generated migration’s contents so the table stores a token ID (jti) and expiry:

Ruby
# db/migrate/xxxx_create_jwt_denylists.rb
class CreateJwtDenylists < ActiveRecord::Migration[8.1]
  def change
    create_table :jwt_denylist do |t|
      t.string :jti, null: false
      t.datetime :exp, null: false
    end
    add_index :jwt_denylist, :jti
  end
end

Then point the model at the strategy and table, and make the user JWT-authenticatable:

Ruby
# app/models/jwt_denylist.rb
class JwtDenylist < ApplicationRecord
  include Devise::JWT::RevocationStrategies::Denylist
 
  self.table_name = "jwt_denylist"
end
Ruby
# app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :validatable, :jwt_authenticatable,
         jwt_revocation_strategy: JwtDenylist
end

Run bin/rails db:migrate to create the table.

Controller Setup

Devise’s controllers speak HTML by default, so subclass them to respond with JSON, and route to the subclasses:

Ruby
# app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  respond_to :json
 
  private
 
  # API clients hold no session, so sign the new user in without storing one.
  # Without this, sign-up raises DisabledSessionError in an API-only app.
  def sign_up(resource_name, resource)
    sign_in(resource_name, resource, store: false)
  end
 
  def respond_with(user, _opts = {})
    if user.persisted?
      render json: { message: "Signed up", user: user }, status: :ok
    else
      render json: { errors: user.errors.full_messages },
             status: :unprocessable_entity
    end
  end
end
Ruby
# app/controllers/users/sessions_controller.rb
class Users::SessionsController < Devise::SessionsController
  respond_to :json
 
  private
 
  def respond_with(user, _opts = {})
    render json: { message: "Signed in", user: user }, status: :ok
  end
 
  def respond_to_on_destroy(_signed_out = true)
    head :no_content
  end
end
Ruby
# config/routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: {
    sessions: "users/sessions",
    registrations: "users/registrations"
  }
end

That’s the whole flow. A successful POST /users/sign_in now returns the token in the response headers:

Shell
curl -i -X POST http://localhost:3000/users/sign_in \
  -H "Content-Type: application/json" \
  -d '{"user":{"email":"api@example.com","password":"password123"}}'
 
# HTTP/1.1 200 OK
# Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...

Sign-up (POST /users) dispatches a token too, so new users are authenticated immediately. And a DELETE /users/sign_out with the token in its Authorization header revokes it by writing its jti into jwt_denylist. Once this is serving real clients, instrument your auth endpoints so failed token exchanges and 401 spikes show up in your monitoring instead of your support inbox.

Tracking Devise Logins with Authtrail

Suppose you want to email users when someone logs into their account, with the IP address and timestamp of the attempt. That means recording login activity, and the Authtrail gem does exactly that for any Warden-based setup, Devise included.

Login records contain emails and IP addresses, so encrypt them. With Lockbox and Blind Index in your bundle, install Authtrail like this (or pass --encryption=none to skip encryption):

Shell
bundle add lockbox blind_index authtrail
bin/rails g authtrail:install --encryption=lockbox
bin/rails db:migrate

The generator creates a LoginActivity model, and from then on every login attempt is recorded automatically with:

  • The email address used (identity) and the matched user, if any.
  • Whether the attempt succeeded, and the failure reason when it didn’t.
  • The authentication strategy and Devise scope.
  • The IP address, user agent, and referrer.

Query LoginActivity to power notification emails, staff audit trails, or a “recent devices” page. Authtrail’s documentation covers exclusions, geocoding, and custom storage.

Broken sign-in flows usually surface as quiet 401s and callback exceptions rather than loud crashes. AppSignal’s Ruby error tracking groups Devise controller errors automatically, so you spot a failing login flow before your users do.

Wrapping Up

Devise packages the hard parts of authentication (password storage, recovery, confirmation, locking, sessions) into 10 modules you compose per app. We also drew the line between Devise and Rails 8’s built-in generator: reach for the generator when login is all you need, and for Devise when authentication is a feature set rather than a form.

Happy coding!

P.S. If you’d like to read Ruby Magic posts as soon as they get off the press, subscribe to our Ruby Magic newsletter and never miss a single post!

Frequently asked questions

What is the Devise gem in Ruby on Rails?
Devise is a full-featured authentication library for Rails built on Warden. It handles registration, login, password recovery, remember-me, account locking, and more through composable modules, and ships the routes, controllers, views, and helpers (like current_user and authenticate_user!) that a sign-in system needs.
How do I install Devise in Rails 8?
Run bundle add devise, then rails g devise:install to create the initializer and locale files. Generate your model with rails g devise User and run rails db:migrate. Devise 5.0 supports Rails 7.0 and up, including Rails 8, with no extra configuration.
What modules does the Devise gem include?
Devise ships 10 modules: database_authenticatable, registerable, recoverable, rememberable, validatable, confirmable, lockable, timeoutable, trackable, and omniauthable. A generated model enables the first five by default. You activate the rest by adding them to the devise call and uncommenting their columns in the migration.
Should I use Devise or Rails 8’s built-in authentication generator?
Use the built-in generator when you only need email and password login with password resets, and want to own the code. Choose Devise when you want registration, email confirmation, account locking, session timeout, OmniAuth social login, or JWT API authentication without building those flows yourself.

Published , Updated

Wondering what you can do next?

  • Share this article on social media
Aestimo Kirina

Aestimo Kirina

Our guest author Aestimo is a full-stack developer, tech writer/author and SaaS entrepreneur.

All articles by Aestimo Kirina

Become our next author!

Find out more
$appsignal install

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal