
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:
| Module | What It Does | On by Default? |
|---|---|---|
database_authenticatable | Hashes a password with bcrypt, stores it, and verifies it on sign-in | Yes |
registerable | Lets users sign up, edit, and delete their accounts | Yes |
recoverable | Sends password reset emails and handles the reset flow | Yes |
rememberable | Remembers a signed-in user with a signed cookie | Yes |
validatable | Validates email format and password length | Yes |
confirmable | Emails a confirmation link on sign-up and blocks unconfirmed accounts | No |
lockable | Locks an account after too many failed logins; unlocks by email or after a timeout | No |
timeoutable | Expires sessions after a period of inactivity | No |
trackable | Records sign-in count, timestamps, and IP addresses | No |
omniauthable | Adds OmniAuth support for external providers like GitHub or Google | No |
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:
| Helper | What It Gives You |
|---|---|
current_user | The signed-in user, or nil |
user_signed_in? | true when a user is signed in |
user_session | The signed-in user’s session data |
authenticate_user! | A filter that redirects unauthenticated visitors to sign-in |
new_user_session_path | The sign-in page (/users/sign_in) |
destroy_user_session_path | Signs the user out (/users/sign_out) |
new_user_registration_path | The sign-up page (/users/sign_up) |
edit_user_registration_path | The 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:
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
before_action :authenticate_user!
endDevise 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:
| Capability | Devise | Rails 8 Generator |
|---|---|---|
| Modules | 10 composable modules, including confirmation, locking, and timeout | Fixed feature set: login and password reset |
| User registration | Full sign-up, edit, and delete flows via registerable | Not generated; you build sign-up yourself |
| Generated views | Sign-in, sign-up, password, confirmation, unlock, and mailer views | Sign-in and password reset views only |
| OmniAuth | Built in via omniauthable | Not supported |
| JWT / API auth | Via the devise-jwt extension | Not supported; sessions are cookie-based |
| Password reset | Yes, via recoverable | Yes, with a token-based mailer flow |
| Maintenance burden | A gem dependency you upgrade | A 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:
rails new tasks_app
cd tasks_appDevise 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:
bundle add devise
bin/rails g devise:installbundle 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:
# 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:
bin/rails g devise User
bin/rails db:migrateThe 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:
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
endThe 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): permitsemail,password, andpassword_confirmation.sign_in(Devise::SessionsController#create): permits the authentication keys,emailandpassword.account_update(Devise::RegistrationsController#update): permitsemail,password,password_confirmation, andcurrent_password.
To permit extra keys, add a filter to ApplicationController. Here we allow a username on sign-up:
# 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
endStrong 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:
bin/rails g devise:viewsThe views land in app/views/devise:

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:
<!-- 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:
bin/rails g devise:views -v sessions registrationsCustomizing 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):
bin/rails g devise:controllers users
Point Devise’s routes at the generated controllers you want to customize:
# config/routes.rb
Rails.application.routes.draw do
devise_for :users, controllers: {
sessions: "users/sessions",
registrations: "users/registrations"
}
endEach 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:
# 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
endsuper 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
bundle add omniauth-github omniauth-rails_csrf_protectionomniauth-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:3000in 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:
# 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:
bin/rails g migration AddOmniauthToUsers provider:string uid:string
bin/rails db:migrateThen enable the module and add a method that finds or creates a user from the OAuth payload:
# 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
endThe 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:
# 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
endAnd route Devise’s OmniAuth callbacks to it:
# config/routes.rb
devise_for :users, controllers: {
omniauth_callbacks: "users/omniauth_callbacks"
}Set Up the Login Links
OmniAuth 2.0+ requires the authorization request to be a POST, so use button_to rather than a plain link:
<!-- 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
Authorizationresponse header. - The client sends that token back in the
Authorizationheader 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):
# 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
endThe 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
bundle add devise devise-jwt
bin/rails g devise:install
bin/rails g devise User
bin/rails db:migrateSince 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:
# config/initializers/devise.rb
Devise.setup do |config|
# ...
config.navigational_formats = []
config.jwt do |jwt|
jwt.secret = ENV["DEVISE_JWT_SECRET"]
end
endGenerate 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:
bin/rails g model jwt_denylistReplace the generated migration’s contents so the table stores a token ID (jti) and expiry:
# 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
endThen point the model at the strategy and table, and make the user JWT-authenticatable:
# app/models/jwt_denylist.rb
class JwtDenylist < ApplicationRecord
include Devise::JWT::RevocationStrategies::Denylist
self.table_name = "jwt_denylist"
end# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:validatable, :jwt_authenticatable,
jwt_revocation_strategy: JwtDenylist
endRun 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:
# 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# 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# config/routes.rb
Rails.application.routes.draw do
devise_for :users, controllers: {
sessions: "users/sessions",
registrations: "users/registrations"
}
endThat’s the whole flow. A successful POST /users/sign_in now returns the token in the response headers:
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):
bundle add lockbox blind_index authtrail
bin/rails g authtrail:install --encryption=lockbox
bin/rails db:migrateThe 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
strategyand Devisescope. - 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?
- Subscribe to our Ruby Magic newsletter and never miss an article again.
- Start monitoring your Ruby app with AppSignal.
- Share this article on social media

Aestimo Kirina
Our guest author Aestimo is a full-stack developer, tech writer/author and SaaS entrepreneur.
All articles by Aestimo KirinaBecome our next author!
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!


