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:
- Token validation — your API verifies JWTs against Auth0's JWKS endpoint
- Login flow — browser redirects to Auth0, user authenticates, returns with code
- Token exchange — authorization code exchanged for access/ID tokens
- 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
- Go to manage.auth0.com
- Click your tenant name (top left) → Create tenant
- Name it
your-project-testor similar - 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.comAPI 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 == 200Unit 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 == 401E2E 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-auth0Configure
// 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 NoneCI 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.