Testing Auth0 Integrations: Mock JWKS, Test Tenants, and cypress-auth0

Testing Auth0 Integrations: Mock JWKS, Test Tenants, and cypress-auth0

Auth0 testing requires a dedicated test tenant, programmatic token generation for API tests, mock JWKS servers for unit tests, and cypress-auth0 or Playwright helpers for E2E tests. Testing against your production Auth0 tenant pollutes logs and risks rate limits. This guide covers the right setup for each test layer.

Key Takeaways

Create a dedicated Auth0 test tenant. Don't run tests against your production or development Auth0 tenant. A test tenant gives you isolated users, clean logs, and no risk of polluting production data. Auth0 free tier includes multiple tenants.

Generate tokens programmatically for API tests. The Auth0 Management API and Resource Owner Password Grant let you create test tokens without browser interaction. This is the right approach for testing backend API authentication.

Use cypress-auth0 or auth0-spa-js mocking to skip login UI in E2E tests. Browser-based login flows are slow and flaky. cypress-auth0 provides session injection that bypasses the login UI entirely while still using real JWT tokens.

Mock the JWKS endpoint for pure unit tests. If you're testing a JWT validation middleware, you don't need a real Auth0 server — serve a mock JWKS with a test key pair and verify your middleware handles it correctly.

Test token expiry and refresh explicitly. Expired tokens and refresh flows are where authentication bugs hide. Set short expiry times in test tokens and verify your app handles 401 responses gracefully.

Why Auth0 Testing Requires Strategy

Auth0 is an OAuth2/OIDC identity provider. When you integrate Auth0, you're testing:

  1. Token validation — your API verifies JWTs against Auth0's JWKS endpoint
  2. Login flow — browser redirects to Auth0, user authenticates, returns with code
  3. Token exchange — authorization code exchanged for access/ID tokens
  4. Session management — storing tokens, refreshing them, handling expiry

Each layer requires a different testing approach. Testing all of them against a live Auth0 tenant in CI is slow, fragile, and pollutes your logs.

Setup: Dedicated Test Tenant

  1. Go to manage.auth0.com
  2. Click your tenant name (top left) → Create tenant
  3. Name it your-project-test or similar
  4. Create a test application and test API

Set environment variables for your test tenant:

AUTH0_DOMAIN=your-project-test.us.auth0.com
AUTH0_CLIENT_ID=test_client_id
AUTH0_CLIENT_SECRET=test_client_secret
AUTH0_AUDIENCE=https://api.your-project-test.com

API Tests: Programmatic Token Generation

For testing backend APIs that require Auth0 JWTs, generate tokens programmatically using the Resource Owner Password Grant (must be enabled in your test application settings):

# auth_helpers.py
import requests
import os


def get_test_token(email: str, password: str) -> str:
    """Get an Auth0 JWT for a test user."""
    response = requests.post(
        f"https://{os.environ['AUTH0_DOMAIN']}/oauth/token",
        json={
            "grant_type": "password",
            "username": email,
            "password": password,
            "audience": os.environ["AUTH0_AUDIENCE"],
            "scope": "openid profile email",
            "client_id": os.environ["AUTH0_CLIENT_ID"],
            "client_secret": os.environ["AUTH0_CLIENT_SECRET"],
        },
    )
    response.raise_for_status()
    return response.json()["access_token"]

Pytest fixtures for Auth0 tokens

# conftest.py
import pytest
from auth_helpers import get_test_token


@pytest.fixture(scope="session")
def auth_token():
    """Get a valid Auth0 token for the test session."""
    return get_test_token(
        email=os.environ["TEST_USER_EMAIL"],
        password=os.environ["TEST_USER_PASSWORD"],
    )


@pytest.fixture(scope="session")
def admin_token():
    return get_test_token(
        email=os.environ["TEST_ADMIN_EMAIL"],
        password=os.environ["TEST_ADMIN_PASSWORD"],
    )


# test_api.py
def test_protected_endpoint(client, auth_token):
    response = client.get(
        "/api/profile",
        headers={"Authorization": f"Bearer {auth_token}"},
    )
    assert response.status_code == 200
    assert response.json()["email"] == os.environ["TEST_USER_EMAIL"]


def test_admin_endpoint_rejected_for_regular_user(client, auth_token):
    response = client.get(
        "/api/admin/users",
        headers={"Authorization": f"Bearer {auth_token}"},
    )
    assert response.status_code == 403


def test_admin_endpoint_accessible_for_admin(client, admin_token):
    response = client.get(
        "/api/admin/users",
        headers={"Authorization": f"Bearer {admin_token}"},
    )
    assert response.status_code == 200

Unit Tests: Mocking the JWKS Endpoint

For unit tests of JWT validation middleware, you don't need a real Auth0 server. Generate a local RSA key pair and serve a mock JWKS:

# test_jwt_middleware.py
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
import jwt
import json
import base64
from unittest.mock import patch


def generate_rsa_key_pair():
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
        backend=default_backend(),
    )
    return private_key, private_key.public_key()


def make_jwks(public_key, kid="test-key-1"):
    """Convert an RSA public key to JWK format."""
    pub_numbers = public_key.public_key().public_numbers() if hasattr(public_key, 'private_numbers') else public_key.public_numbers()
    
    def to_base64url(n):
        # Convert integer to big-endian bytes, then base64url encode
        byte_length = (n.bit_length() + 7) // 8
        return base64.urlsafe_b64encode(n.to_bytes(byte_length, 'big')).rstrip(b'=').decode()
    
    return {
        "keys": [{
            "kty": "RSA",
            "use": "sig",
            "kid": kid,
            "n": to_base64url(pub_numbers.n),
            "e": to_base64url(pub_numbers.e),
            "alg": "RS256",
        }]
    }


def make_test_token(private_key, payload: dict, kid="test-key-1"):
    """Create a JWT signed with a test RSA private key."""
    return jwt.encode(
        payload,
        private_key,
        algorithm="RS256",
        headers={"kid": kid},
    )


@pytest.fixture
def rsa_key_pair():
    private_key, public_key = generate_rsa_key_pair()
    return private_key, public_key


def test_valid_jwt_accepted(client, rsa_key_pair):
    private_key, public_key = rsa_key_pair
    jwks = make_jwks(public_key)
    
    # Create a valid token
    import time
    token = make_test_token(private_key, {
        "sub": "test-user-123",
        "iss": "https://test-tenant.auth0.com/",
        "aud": "https://api.test.com",
        "iat": int(time.time()),
        "exp": int(time.time()) + 3600,
    })
    
    # Mock the JWKS endpoint
    with patch("your_app.auth.fetch_jwks", return_value=jwks):
        response = client.get(
            "/api/protected",
            headers={"Authorization": f"Bearer {token}"},
        )
    
    assert response.status_code == 200


def test_expired_jwt_rejected(client, rsa_key_pair):
    private_key, public_key = rsa_key_pair
    jwks = make_jwks(public_key)
    
    import time
    token = make_test_token(private_key, {
        "sub": "test-user-123",
        "iss": "https://test-tenant.auth0.com/",
        "aud": "https://api.test.com",
        "iat": int(time.time()) - 7200,
        "exp": int(time.time()) - 3600,  # Expired 1 hour ago
    })
    
    with patch("your_app.auth.fetch_jwks", return_value=jwks):
        response = client.get(
            "/api/protected",
            headers={"Authorization": f"Bearer {token}"},
        )
    
    assert response.status_code == 401


def test_wrong_audience_rejected(client, rsa_key_pair):
    private_key, public_key = rsa_key_pair
    jwks = make_jwks(public_key)
    
    import time
    token = make_test_token(private_key, {
        "sub": "test-user-123",
        "iss": "https://test-tenant.auth0.com/",
        "aud": "https://different-api.com",  # Wrong audience
        "iat": int(time.time()),
        "exp": int(time.time()) + 3600,
    })
    
    with patch("your_app.auth.fetch_jwks", return_value=jwks):
        response = client.get(
            "/api/protected",
            headers={"Authorization": f"Bearer {token}"},
        )
    
    assert response.status_code == 401

E2E Tests: cypress-auth0

cypress-auth0 is an official Auth0 plugin that injects authentication state into Cypress tests without going through the login UI.

Install

npm install --save-dev cypress @auth0/cypress-auth0

Configure

// cypress/support/commands.js
import { loginByHapi } from "@auth0/cypress-auth0";

Cypress.Commands.add("loginByAuth0Api", (username, password) => {
  return loginByHapi(username, password);
});

Test with session injection

// cypress/e2e/dashboard.cy.js
describe("Dashboard", () => {
  beforeEach(() => {
    cy.loginByAuth0Api(
      Cypress.env("TEST_USER_EMAIL"),
      Cypress.env("TEST_USER_PASSWORD")
    );
  });

  it("shows user profile", () => {
    cy.visit("/dashboard");
    cy.get("[data-testid='user-name']").should("be.visible");
    cy.get("[data-testid='user-email']")
      .should("contain", Cypress.env("TEST_USER_EMAIL"));
  });

  it("allows access to protected resources", () => {
    cy.visit("/dashboard/settings");
    cy.get("[data-testid='settings-form']").should("be.visible");
  });
});

Playwright alternative

For Playwright, implement a token injection helper:

# auth_fixtures.py
import pytest
from playwright.sync_api import Browser
import requests


@pytest.fixture
def authenticated_page(browser: Browser):
    """Create a browser page with Auth0 session pre-injected."""
    # Get token
    token_response = requests.post(
        f"https://{AUTH0_DOMAIN}/oauth/token",
        json={
            "grant_type": "password",
            "username": TEST_USER_EMAIL,
            "password": TEST_USER_PASSWORD,
            "audience": AUTH0_AUDIENCE,
            "client_id": AUTH0_CLIENT_ID,
            "client_secret": AUTH0_CLIENT_SECRET,
        }
    )
    tokens = token_response.json()
    
    context = browser.new_context()
    page = context.new_page()
    
    # Navigate to app first (needed to set cookies on the right domain)
    page.goto("https://your-app.com/")
    
    # Inject tokens into localStorage (or however your app stores them)
    page.evaluate(f"""() => {{
        localStorage.setItem('auth_access_token', '{tokens["access_token"]}');
        localStorage.setItem('auth_id_token', '{tokens["id_token"]}');
        localStorage.setItem('auth_expires_at', String(Date.now() + {tokens["expires_in"]} * 1000));
    }}""")
    
    page.reload()
    yield page
    context.close()

Testing Auth0 Actions and Rules

If you have Auth0 Actions or Rules that modify token claims, test them in the Auth0 dashboard's testing console before deploying. After deploying, verify the claims appear in tokens from your test tenant:

def test_custom_claims_in_token():
    token = get_test_token(TEST_USER_EMAIL, TEST_USER_PASSWORD)
    
    # Decode without verification to check claims
    payload = jwt.decode(token, options={"verify_signature": False})
    
    # Custom claims from your Auth0 Action
    assert "https://your-app.com/roles" in payload
    assert "user" in payload["https://your-app.com/roles"]
    assert payload["https://your-app.com/tenant_id"] is not None

CI Configuration

# .github/workflows/auth-tests.yml
env:
  AUTH0_DOMAIN: ${{ secrets.AUTH0_TEST_DOMAIN }}
  AUTH0_CLIENT_ID: ${{ secrets.AUTH0_TEST_CLIENT_ID }}
  AUTH0_CLIENT_SECRET: ${{ secrets.AUTH0_TEST_CLIENT_SECRET }}
  AUTH0_AUDIENCE: ${{ secrets.AUTH0_TEST_AUDIENCE }}
  TEST_USER_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
  TEST_USER_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}

Store all Auth0 test tenant credentials in CI secrets. Never commit them to the repo.

Summary

Auth0 testing has three layers: unit tests (mock JWKS for fast, isolated middleware testing), API tests (programmatic token generation with the password grant), and E2E tests (cypress-auth0 or Playwright session injection). A dedicated test tenant isolates your tests from production data. The password grant is the key to making API tests fast — it eliminates browser automation for scenarios where you only need a valid token.

Read more