How to Test Auth0 Login in Rails with Minitest
Test Auth0 OAuth callbacks, sessions, and protected routes in Rails without hitting Auth0 in CI. OmniAuth test mode, integration tests, and Minitest helpers.
Auth0 login looks simple in the browser. You click Sign in, get redirected, and come back logged in. While testing, the same flow is a trap. You do not want every bin/rails test run to depend on:
- Auth0 being up
- Test users existing in a tenant, or
- Headless browser grinding through a multi-step redirect chain
But you still need confidence that your app does the right thing when Auth0 sends a user back:
- The session should exist
- Protected pages should work, and
- Sign-out should clear the session
This post walks through how to test that flow with Minitest and OmniAuth test mode, without relying on Auth0 in CI.
If you are using Rails 8’s built-in bin/rails generate authentication instead of Auth0, the testing authentication guide chapter is the better fit, it goes through the authentication integration in the app and teaches you a way to test all edge cases with Minitest. This post is for apps that delegate login to Auth0, or to any OmniAuth provider wired the same way.
Assumptions #
This walkthrough assumes the following:
- You are using Minitest for writing tests.
- You have already integrated auth0 in your app and it includes:
omniauth-auth0gem (or plainomniauth+ Auth0 strategy).- Auth0 callback route like
/auth/auth0/callbackand a failure route. - User identifier stored in the session after login (
session[:user_id],Current.session, etc.).
- You are using Fixtures to load test data.
Adjust names to match your app. The patterns stay the same even when paths or session keys differ.
Tested and working in #
The code in this walkthrough was tested and working in:
- Ruby 4.0.2
- Rails 8.1.3
- Minitest 6.0.6
- selenium-webdriver 4.44.0
- omniauth-auth0 3.2.0
Sample companion app #
Every code block in this post comes from a runnable Rails app: auth0-minitest-rails. Clone it, follow the README for local Auth0 setup, and diff your app when a test does not match.
The integration and system tests from this walkthrough are on the test-auth0 branch in PR #4. Open that pull request when you want the full file list in one place instead of copying snippets by hand.
What you are actually testing #
When I first wired Auth0 into a Rails app, I kept wanting the test suite to prove “login works end to end.” That usually means opening Auth0 in a browser, or stubbing ten redirects, or maintaining a test user in a real tenant. None of that belongs in every CI run.
It helps to split the problem in two:
-
Your Rails app
What happens after Auth0 sends the user back (callback route, session or user record, redirects, and who can reach protected pages).
-
Auth0
Their login screen, MFA, password reset, and issuing tokens.
Automated tests should cover the first bucket. Manual smoke tests, or Auth0’s own tooling, can cover the second. Trying to automate the full redirect flow on every push is slow, flaky, and usually unnecessary.
Here is how different types of Rails tests map to Auth0 login, once you accept that split:
| Test type | Good for Auth0 login |
|---|---|
| Model / unit | Auth0 user lookup, validations |
| Integration | Callback POST/GET, session cookie, protected routes, sign-out |
| System | Optional: one happy-path click if you stub OmniAuth in the browser session |
| Real Auth0 redirect in CI | Avoid |
Integration tests are where most of the value lives. They hit your callback and session logic the way the rest of your app will, without leaving your app host. I try to avoid system tests because they are slow and flaky in nature, I write them strictly for smoke tests.
Now let’s get on to testing.
Turn on OmniAuth test mode in test #
OmniAuth ships a test mode that lets you POST a fake auth hash to /auth/:provider/callback instead of visiting Auth0. That is the core trick for the rest of the post.
In config/environments/test.rb, turn it on for the test environment:
# config/environments/test.rb
Rails.application.configure do
# ...
OmniAuth.config.test_mode = true
end
Add Auth0 test helper #
Next, add a small helper file under test/support/ with two methods:
default_auth0_userholds the reusable Alice profile that matches yourtest/fixtures.user.ymlload_omniauth_mockwrites that user’s fake Auth0 response into OmniAuth’s test config andRails.application.env_config["omniauth.auth"]so callback tests behave like a real redirect landed.
Create test/support/auth0_test_helper.rb and add the following inside:
# test/support/auth0_test_helper.rb
module Auth0TestHelper
def default_auth0_user
OmniAuth::AuthHash.new(
provider: "auth0",
uid: "auth0|test-user-1",
info: {
email: "alice@example.com",
name: "Alice Example"
},
extra: {
raw_info: {
"sub" => "auth0|test-user-1",
"email" => "alice@example.com",
"name" => "Alice Example"
}
}
)
end
def load_omniauth_mock(auth0_uid:, email: nil, name: nil)
email ||= default_auth0_user.info["email"]
name ||= default_auth0_user.info["name"]
OmniAuth.config.mock_auth[:auth0] = OmniAuth::AuthHash.new(
"provider" => "auth0",
"uid" => auth0_uid,
"info" => {
"email" => email,
"name" => name
},
:extra => {
raw_info: {
"sub" => auth0_uid,
"email" => email,
"name" => name
}
}
)
Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[
:auth0
]
end
end
Open test/test_helper.rb. If it does not already load support files, add this near the top (after require "rails/test_help"). Skip this line if your project already has it:
# test/test_helper.rb
Dir[Rails.root.join("test/support/**/*.rb")].sort.each { |f| require f }
That line finds every .rb file under test/support/ and requires them in sorted order, so modules like Auth0TestHelper are defined before your tests run. This way you don’t have to add any other require for files inside the test support folder.
Just below class TestCase, include the helper:
# test/test_helper.rb
module ActiveSupport
class TestCase
include Auth0TestHelper
end
end
User fixtures for Auth0 #
Fixtures are sample records Rails loads into the test database before each run. Login tests (coming up in next section) look up an existing user by auth0_uid when the callback fires, so you need a fixture row that matches what default_auth0_user and load_omniauth_mock send back.
You might be using Factory Bot instead; if so, adjust the shape of your test data, but keep the same auth0_uid values.
Open test/fixtures/users.yml and make sure each column matches the following:
# test/fixtures/users.yml
alice:
email: alice@example.com
auth0_uid: auth0|test-user-1
name: Alice Example
We will use these columns and rely on their values in the next section where we add Integration Tests so these need to be correct.
Integration tests: login, signup, and failure #
Picture this flow in your app:
- User clicks Sign in and is redirected to Auth0.
- User enters credentials in Auth0 and, on success, returns to your app.
- Your app hits the callback URL.
- Your app finds or creates a local user, then sets the session so later requests stay signed in.
That would amount to three edge cases in the test file:
- an existing user logs in
- a new user is created on first login, and
- a failed Auth0 attempt still lands somewhere sensible
Create a new integration test file for the Auth0 login from your project root with:
$ nano test/integration/auth0_login_integration_test.rb
Add the following inside it:
# test/integration/auth0_login_integration_test.rb
require "test_helper"
class Auth0LoginIntegrationTest < ActionDispatch::IntegrationTest
test "logs in the user" do
user = users(:alice)
load_omniauth_mock(
auth0_uid: user.auth0_uid,
email: user.email,
name: user.name
)
assert_no_difference "User.count" do
get "/auth/auth0/callback"
end
follow_redirect!
assert_response :success
assert_equal user.id, session[:user_id]
assert_equal "auth0|test-user-1", session[:userinfo]["sub"]
end
test "creates a user and logs them in" do
load_omniauth_mock(
auth0_uid: "auth0|test-user-2",
email: "bob@example.com",
name: "Bob Example"
)
assert_difference "User.count", 1 do
get "/auth/auth0/callback"
end
user = User.find_by(email: "bob@example.com")
assert_equal "auth0|test-user-2", user.auth0_uid
follow_redirect!
assert_response :success
assert_equal user.id, session[:user_id]
end
test "failure redirects with a message" do
OmniAuth.config.mock_auth[:auth0] = :invalid_credentials
get "/auth/auth0"
follow_redirect! # OmniAuth failure redirect (redirected from Auth0)
follow_redirect! # /auth/failure
assert_response :success
assert_match(/authentication failed/i, response.body)
end
end
Run the test:
bin/rails test test/integration/auth0_login_integration_test.rb
You want 0 failures and 0 errors. If you see ActionController::RoutingError, your OmniAuth mount path differs from the example. Check config/routes.rb for how auth/:provider is wired in your app, this is what I have:
# config/routes.rb
Rails.application.routes.draw do
# ...
get "/auth/auth0/callback" => "auth0#callback"
get "/auth/failure" => "auth0#failure"
get "/auth/logout" => "auth0#logout"
# ...
end
What each test block is testing:
- The first test signs in
users(:alice)from fixtures. No new row should appear. - The second uses a mock for someone not in fixtures. Callback creates the user, then sets the session.
- The third sets an invalid mock and checks that failure still lands somewhere sensible in your UI.
Testing protected routes #
A protected route is a page your app only serves to signed-in users. The controller checks the session for signed-in user before it allows the access to these pages. If nobody is logged in, the app redirects the user to sign-in or sends the visitor back to the home page instead of showing the page.
A dashboard is a common example but it could also be any other pages like account settings, billing, or anything behind before_action :authenticate_user!. Guests should not see it. Someone who just got redirected back from Auth0 after successful login attempt should see the page.
The signed-in test repeats the same login steps from the callback test: load_omniauth_mock, get "/auth/auth0/callback", and follow_redirect!. Create a new integration file for the dashboard test:
nano test/integration/dashboard_integration_test.rb
Add the following inside it:
# test/integration/dashboard_integration_test.rb
require "test_helper"
class DashboardIntegrationTest < ActionDispatch::IntegrationTest
test "signed-in user can access the dashboard" do
user = users(:alice)
load_omniauth_mock(
auth0_uid: user.auth0_uid,
email: user.email,
name: user.name
)
get "/auth/auth0/callback"
follow_redirect!
get dashboard_path
assert_response :success
end
test "guest cannot access the dashboard" do
get dashboard_path
assert_redirected_to root_path # or your sign-in page
end
end
Helper: sign in without repeating the callback #
The login and dashboard tests both open with the same three lines: load_omniauth_mock(...), get "/auth/auth0/callback", and follow_redirect!. Every protected-route test you add will copy that block again.
Let’s extract it into a helper method sign_in_as on the same test/support/auth0_test_helper.rb file so we can reuse it anywhere in the app that requires login:
# test/support/auth0_test_helper.rb
module Auth0TestHelper
def default_auth0_user
# ... same as above
end
def load_omniauth_mock(auth0_uid:, email: nil, name: nil)
# ... same as above
end
def sign_in_as(user: nil, email: nil)
if user.nil? && email.nil?
raise ArgumentError, "Pass user: or email:"
end
if user
load_omniauth_mock(
auth0_uid: user.auth0_uid,
email: user.email,
name: user.name
)
else
load_omniauth_mock(
auth0_uid: "auth0|#{email.parameterize}",
email: email,
name: email.split("@").first.titleize
)
end
get "/auth/auth0/callback"
follow_redirect!
end
end
Because Auth0TestHelper is already included in ActiveSupport::TestCase, integration tests can call sign_in_as without extra setup. Update the dashboard_integration_test to use this new helper method, it should look like the following after the refactor:
# test/integration/dashboard_integration_test.rb
require "test_helper"
class DashboardIntegrationTest < ActionDispatch::IntegrationTest
test "signed-in user can access the dashboard" do
sign_in_as(user: users(:alice))
get dashboard_path
assert_response :success
end
test "guest cannot access the dashboard" do
# same as previous
end
end
We can’t use sign_in_as in Auth0LoginIntegrationTest because we are also testing User count difference after the login, but this method will be very handy for other protected routes and will increase reusability inside the app.
System test (optional, one happy path) #
System test is optional since we have already added thorough integration tests. But I still like to always make sure pages load correctly in the browser with Javascript running, so I write a small smoke test for each page.
System tests drive a real browser. They can answer “does the Sign in button exist and does the page update,” but you should still stub OmniAuth instead of opening Auth0.
# test/system/authentication_test.rb
require "application_system_test_case"
class AuthenticationTest < ApplicationSystemTestCase
setup do
load_omniauth_mock(auth0_uid: "auth0|test-user-1")
end
test "visitor signs in to the app" do
visit root_path
click_on "Sign in"
assert_text "Signed in as alice@example.com" # or match any other text in your UI
end
end
Run with:
bin/rails test test/system/authentication_test.rb
You should get 0 failures and 0 error.
One green system test is enough for a smoke check. The rest of your auth confidence should live in integration tests, which are faster and easier to debug.
What to commit #
After the tests we added above pass, commit a small slice:
bin/rails test test/integration/auth0_login_integration_test.rb
bin/rails test test/system/authentication_test.rb
git add test/ config/environments/test.rb
git commit -m "Add Auth0 login tests"
Small commits make it obvious which layer broke when someone changes the callback or session store later. You will thank yourself when a teammate refactors SessionsController and only one test file goes red.
CI gotcha: config/auth0.yml must exist #
Tests do not call Auth0, but Rails still boots your app and loads Auth0 config from a YAML file. If that file is gitignored (common for real credentials), CI fails before any test runs with the following error:
Could not load configuration. No such file - .../config/auth0.yml
Commit a placeholder file with fake values for the test environment. The sample app uses config/auth0.yml.example:
# config/auth0.yml.example
test:
auth0_domain: your-tenant.auth0.com
auth0_client_id: your_client_id
auth0_client_secret: your_client_secret
In GitHub Actions, copy it before bin/rails test and bin/rails test:system:
# .github/workflows/ci.yml
# ...
# .....
# ...
jobs:
test:
runs-on: ubuntu-latest
# ...
# .....
# ...
steps:
# ...
- name: Set up Auth0 config
run: cp config/auth0.yml.example config/auth0.yml
- name: Run tests
env:
RAILS_ENV: test
run: bin/rails db:test:prepare test
system-test:
runs-on: ubuntu-latest
steps:
# ...
- name: Set up Auth0 config
run: cp config/auth0.yml.example config/auth0.yml
- name: Run System Tests
env:
RAILS_ENV: test
run: bin/rails db:test:prepare test:system
# ...
# .....
# ...
Placeholder values are fine. OmniAuth test mode never opens a real tenant; the file just has to be there so Rails can boot.
Recap #
We made Auth0 login testable in CI without ever opening Auth0 in a browser.
- Use OmniAuth test mode so tests stay inside your app.
- Test the callback in an integration test:
- Existing fixture user logs in (no new row).
- First-time login creates a user and sets the session.
- Failed login redirects with a helpful message.
- Test one protected route (dashboard) for both signed-in and guest users.
- Test the login with smoke styled system test to ensure mock behaves correctly in real browser session as well.
- Keep live Auth0 for manual checks when you change tenant settings or the login UI.
Where to go next #
This post is one slice of a bigger topic: testing Rails apps with Minitest. The Minitest Rails guide walks through that from your first test through models, system tests, mailers, jobs, and CI. You build one small app as you read, so examples stay connected instead of jumping between toy snippets.
If your app uses Rails’ built-in authentication (email and password from bin/rails generate authentication) rather than Auth0, start with the testing authentication chapter when you get there in order. You do not need Auth0 or OmniAuth to follow the rest of the guide.
How was this blog?
Tell me what helped, what was unclear, or what to improve. I read every message.
Disclaimer
This blog is written in tandem with AI but reviewed and enhanced by me based on my experience with Rails and testing. I stand by the advice and patterns here.