Blog

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 authentication minitest rails omniauth

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-auth0 gem (or plain omniauth + Auth0 strategy).
    • Auth0 callback route like /auth/auth0/callback and 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:

  1. Your Rails app

    What happens after Auth0 sends the user back (callback route, session or user record, redirects, and who can reach protected pages).

  2. 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_user holds the reusable Alice profile that matches your test/fixtures.user.yml
  • load_omniauth_mock writes that user’s fake Auth0 response into OmniAuth’s test config and Rails.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.

Send feedback