These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
What Is Auth0?
Auth0 is a managed identity access management (IAM) platform built on OAuth 2.0 and OpenID Connect. It acts as an authorization server: your application delegates authentication to Auth0 rather than handling credentials, sessions, and security policies directly.
Auth0 provides Universal Login (a hosted login UI), social identity provider connectors, multi-factor authentication (MFA), and role-based access control. It issues JWT access tokens and ID tokens that your backend validates to authorize API requests.
For full-stack developers, the practical value is straightforward: adding social login, MFA, or passwordless authentication requires configuration changes in Auth0's dashboard, not application code changes.
Sign up for the Logbook, Strapi's Monthly newsletter
Why Integrate Auth0 with Strapi
Strapi v5's Users & Permissions plugin supports Auth0 out of the box, which means you can wire up a production-grade auth flow without building custom OAuth middleware. Here's what that combination gives you:
- Standards-based security without custom implementation. Auth0 handles OWASP-recommended practices including TLS enforcement, unpredictable session tokens, and brute-force protection through configuration rather than code. MFA and attack protection (credential stuffing detection, anomalous login patterns) come included per Auth0's security features.
- Decoupled auth infrastructure. Authentication traffic scales independently from your Strapi content delivery. Auth0 absorbs login spikes and manages credential storage, while your Strapi instance handles only content operations.
- Enterprise SSO with pre-built connectors. Auth0's SSO integrations connect to Active Directory, Azure AD, Google Workspace, and other SAML/OIDC providers. This pairs with Strapi's admin SSO to let your content team log in with corporate credentials.
- Two-tier RBAC. Auth0's role system manages who the user is (identity roles). Strapi's RBAC system manages what content they can access. Auth0 answers "is this user an editor?" and Strapi answers "can editors publish articles?"
- Reduced integration time. Auth0 is a first-party provider in Strapi v5. You need only a Client ID, Client Secret, and subdomain configured in the Admin Panel. Auth0's SDK libraries provide framework-specific quickstarts for your frontend.
- Compliance-ready token management. Auth0's architecture aligns with NIST IR 8587 guidance on token security. Configurable token expiration, audit logging, and MFA enforcement policies help satisfy security audits in regulated industries.
How to Integrate Auth0 with Strapi
This section walks through the complete setup: from creating your Auth0 application to making authenticated Strapi API calls. The integration uses Strapi v5's Users & Permissions plugin and its built-in OAuth provider flow.
Prerequisites
Before starting, confirm you have the following:
- Node.js 18+ (LTS recommended)
- Strapi v5 project initialized. Run
npx create-strapi@latest my-project --quickstartif you need a fresh one - Auth0 account — sign up for free if you don't have one
- A frontend application (React, Next.js, or any SPA) running on
http://localhost:3000 - Basic familiarity with OAuth 2.0 flows and JWT tokens
Verify your Strapi version is v5 by checking package.json:
{
"dependencies": {
"@strapi/strapi": "^5.37.1"
}
}Step 1: Create an Auth0 Application
Log into the Auth0 Dashboard and navigate to Applications → Create Application. Select Regular Web Application. This is required for server-side OAuth flows where Strapi handles the callback.
Record your Domain, Client ID, and Client Secret from the application settings page.
Configure these URLs in your Auth0 application settings:
| Setting | Value |
|---|---|
| Allowed Callback URLs | http://localhost:1337/api/connect/auth0/callback |
| Allowed Logout URLs | http://localhost:3000 |
| Allowed Web Origins | http://localhost:3000 |
The callback URL follows the pattern <strapi-backend-url>/api/connect/<provider>/callback. This is where Auth0 sends the authorization code after the user authenticates.
Next, open Advanced Settings → Grant Types and enable all four:
- ✅ Implicit
- ✅ Authorization Code
- ✅ Refresh Token
- ✅ Client Credentials
Per the Strapi Auth0 documentation, all four grant types must be enabled for the provider to function correctly.
Step 2: Configure Strapi Environment Variables
Add your Auth0 credentials to the .env file in your Strapi project root:
AUTH0_DOMAIN=your-auth0-tenant.auth0.com
AUTH0_CLIENT_ID=your-auth0-client-id
AUTH0_CLIENT_SECRET=your-auth0-client-secretStrapi also needs to know its own public URL to construct correct callback URLs. Update /config/server.js:
module.exports = ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
url: env('PUBLIC_URL', 'http://localhost:1337'),
});The url property must align with the domain and port used for your OAuth callback endpoint, and Auth0 will return a visible 'Callback URL mismatch' error if the callback URL does not exactly match one of the Allowed Callback URLs.
Step 3: Enable the Auth0 Provider in Strapi Admin Panel
Start your Strapi server and navigate to http://localhost:1337/admin/settings/users-permissions/providers. Find Auth0 in the provider list and configure it:
| Field | Value | Notes |
|---|---|---|
| Enable | ON | Activates the provider |
| Client ID | Your Auth0 Client ID | From Auth0 application settings |
| Client Secret | Your Auth0 Client Secret | From Auth0 application settings |
| Subdomain | e.g., my-tenant.eu | For https://my-tenant.eu.auth0.com/, enter my-tenant.eu |
| Redirect URL | http://localhost:3000/connect/auth0 | Your frontend callback route |
The Subdomain field needs only the tenant portion. If your Auth0 domain is https://my-tenant.eu.auth0.com/, enter my-tenant.eu, not the full URL.
The Redirect URL is where Strapi sends the user after processing the OAuth callback. This points to your frontend application, which will extract the Strapi JWT from the query parameters.
In Strapi v5, some provider configurations are managed through the admin UI (such as Users & Permissions OAuth providers), while others still rely on environment variables in configuration files (such as email and media providers). For automated or Infrastructure-as-Code deployments, plan for an admin UI setup step or implement a bootstrap script using Strapi's internal API.
Step 4: Configure JWT Settings
Strapi v5 introduces a refresh-token mode for JWT management. Set this in /config/plugins.js:
module.exports = {
'users-permissions': {
config: {
jwt: {
expiresIn: '7d',
},
jwtManagement: 'refresh-token',
},
},
};The refresh-token mode is recommended for new projects. A legacy-support option exists for teams migrating from v4, but you should avoid it for fresh builds.
You can also configure rate limiting on auth endpoints:
module.exports = {
'users-permissions': {
config: {
jwt: {
expiresIn: '7d',
},
jwtManagement: 'refresh-token',
rateLimit: {
enabled: true,
max: 100,
windowMs: 60000,
},
},
},
};Step 5: Initiate the Auth0 Login from Your Frontend
The OAuth flow starts when your frontend redirects the user to Strapi's connect endpoint. Strapi then handles the redirect to Auth0's Universal Login page.
// Login button handler in your frontend application
function handleLogin() {
window.location.href = 'http://localhost:1337/api/connect/auth0';
}Here's what happens behind the scenes:
- Your frontend redirects to
http://localhost:1337/api/connect/auth0 - Strapi redirects the browser to Auth0's authorization endpoint
- The user authenticates on Auth0's hosted login page
- Auth0 redirects back to
http://localhost:1337/api/connect/auth0/callbackwith an authorization code - Strapi exchanges the code for tokens, creates or finds the user, then redirects to your frontend's Redirect URL with a Strapi JWT as a query parameter
Step 6: Handle the Callback and Extract the Strapi JWT
After the OAuth flow completes, Strapi redirects to the URL you configured in Step 3 (e.g., http://localhost:3000/connect/auth0) with the Strapi JWT appended as an access_token query parameter.
Create a callback handler in your frontend:
// /connect/auth0 page or route handler
async function handleAuth0Callback() {
const params = new URLSearchParams(window.location.search);
const strapiToken = params.get('access_token');
if (!strapiToken) {
console.error('No access_token found in callback URL');
return;
}
// Store the Strapi JWT for subsequent API calls
localStorage.setItem('strapiToken', strapiToken);
// Fetch the authenticated user's profile from Strapi
const userRes = await fetch('http://localhost:1337/api/users/me', {
headers: {
Authorization: `Bearer ${strapiToken}`,
},
});
const user = await userRes.json();
console.log('Authenticated user:', user);
// Redirect to your app's main page
window.location.href = '/';
}
handleAuth0Callback();The access_token in the query parameter is a Strapi JWT, not an Auth0 token. Strapi issues its own JWT after verifying the OAuth callback, and this is what you use for all subsequent Strapi API calls.
Step 7: Make Authenticated API Calls
With the Strapi JWT stored, you can now access protected content:
async function fetchProtectedContent() {
const strapiToken = localStorage.getItem('strapiToken');
const response = await fetch('http://localhost:1337/api/articles', {
headers: {
Authorization: `Bearer ${strapiToken}`,
'Content-Type': 'application/json',
},
});
const { data } = await response.json();
return data;
}Strapi's Users & Permissions plugin automatically validates the JWT and assigns the user's role. Content access is determined by the permissions you've configured for the Authenticated role in the Admin Panel under Settings → Users & Permissions → Roles.
Step 8: Add Custom User Sync Logic (Optional)
Strapi automatically creates a user record on first OAuth login. If you need to store additional Auth0 metadata, like the Auth0 user ID (sub claim), extend the user model and add sync logic.
First, add an auth0Id field to the User content type via the Admin Panel or by editing the schema directly.
Then create a sync controller using the Document Service API:
// src/api/user-sync/controllers/user-sync.js
module.exports = {
async syncUser(ctx) {
const { auth0Id, email, name } = ctx.request.body;
// Look up existing user by Auth0 ID using Document Service API
const existingUsers = await strapi
.documents('plugin::users-permissions.user')
.findMany({
filters: { auth0Id: auth0Id },
});
if (existingUsers.length > 0) {
// Update existing user
const user = existingUsers[0];
const updated = await strapi
.documents('plugin::users-permissions.user')
.update({
documentId: user.documentId,
data: {
username: name || email,
email,
},
});
return ctx.send(updated);
}
// Create new user
const defaultRole = await strapi
.documents('plugin::users-permissions.role')
.findMany({
filters: { type: 'authenticated' },
});
const newUser = await strapi
.documents('plugin::users-permissions.user')
.create({
data: {
auth0Id,
email,
username: name || email,
provider: 'auth0',
confirmed: true,
blocked: false,
},
});
return ctx.send(newUser);
},
};Register the route in src/api/user-sync/routes/user-sync.js:
module.exports = {
routes: [
{
method: 'POST',
path: '/users/sync',
handler: 'user-sync.syncUser',
config: {
policies: [],
middlewares: [],
},
},
],
};The Document Service API replaces the deprecated Entity Service from Strapi v4 when migrating to Strapi v5. Documents are identified by documentId instead of id, and relations, media fields, and components are not auto-populated. You must specify them explicitly with the populate parameter.
Project Example: Protected Content Platform with Next.js
This project combines Auth0, Strapi v5, and Next.js to build a content platform where articles are only accessible to authenticated users. Unauthenticated visitors see a login prompt, while logged-in users access premium content based on their role.
Architecture Overview
┌─────────────────┐ OAuth Flow ┌─────────────────┐
│ │ ──────────────────► │ │
│ Next.js App │ │ Auth0 │
│ (localhost:3000)│ ◄────────────────── │ ([Authorization ](https://auth0.com/docs/get-started/identity-fundamentals/introduction-to-auth0)│
│ │ ID + Access Token │ Server) │
└────────┬────────┘ └──────────────────┘
│
│ Strapi JWT (via /api/connect/auth0)
▼
┌─────────────────┐
│ Strapi v5 │
│ (localhost:1337) │
│ Content + RBAC │
└─────────────────┘Set Up the Strapi Content Types
In the Strapi Admin Panel, create an Article Collection Type with these fields:
title(Text, required)content(Rich Text)tier(Enumeration:free,premium)author(Relation: belongs to User)
Then configure permissions under Settings → Users & Permissions → Roles:
- Public role:
findandfindOneon Articles (filtered bytier = free) - Authenticated role:
find,findOne,create, andupdateon Articles
Create the Next.js Frontend
Install the Auth0 Next.js SDK:
npm install @auth0/nextjs-auth0Add environment variables to .env.local:
AUTH0_SECRET=a-long-random-value-at-least-32-characters
AUTH0_BASE_URL=http://localhost:3000
AUTH0_ISSUER_BASE_URL=https://your-tenant.auth0.com
AUTH0_CLIENT_ID=your-client-id
AUTH0_CLIENT_SECRET=your-client-secret
NEXT_PUBLIC_STRAPI_URL=http://localhost:1337Initialize Auth0 in lib/auth0.ts using the Auth0 Next.js SDK quickstart configuration:
import { Auth0Client } from '@auth0/nextjs-auth0/server';
export const auth0 = new Auth0Client();Build the Login Flow
Create a profile component that conditionally renders based on authentication state:
// components/AuthNav.js
'use client';
import { useUser } from '@auth0/nextjs-auth0/client';
export default function AuthNav() {
const { user, isLoading } = useUser();
if (isLoading) return <div>Loading...</div>;
if (!user) {
return (
<nav>
<a href="http://localhost:1337/api/connect/auth0">
Log in with Auth0
</a>
</nav>
);
}
return (
<nav>
<span>Welcome, {user.name}</span>
<a href="/api/auth/logout">Log out</a>
</nav>
);
}Handle the OAuth Callback
Create the callback page that extracts the Strapi JWT:
// app/connect/auth0/page.js
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
export default function Auth0Callback() {
const router = useRouter();
const [status, setStatus] = useState('Authenticating...');
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const accessToken = params.get('access_token');
if (!accessToken) {
setStatus('Authentication failed: no token received.');
return;
}
localStorage.setItem('strapiToken', accessToken);
// Fetch user profile from Strapi
fetch(`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/users/me`, {
headers: { Authorization: `Bearer ${accessToken}` },
})
.then((res) => res.json())
.then((user) => {
localStorage.setItem('strapiUser', JSON.stringify(user));
router.push('/dashboard');
})
.catch(() => {
setStatus('Failed to fetch user profile.');
});
}, [router]);
return <div>{status}</div>;
}Fetch Protected Articles
Build an articles page that uses the Strapi JWT to access role-restricted content:
// app/dashboard/articles/page.js
'use client';
import { useEffect, useState } from 'react';
export default function Articles() {
const [articles, setArticles] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const token = localStorage.getItem('strapiToken');
if (!token) {
window.location.href = '/';
return;
}
fetch(
`${process.env.NEXT_PUBLIC_STRAPI_URL}/api/articles?populate=author`,
{
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
}
)
.then((res) => res.json())
.then(({ data }) => {
setArticles(data);
setLoading(false);
})
.catch((err) => {
console.error('Failed to fetch articles:', err);
setLoading(false);
});
}, []);
if (loading) return <div>Loading articles...</div>;
return (
<div>
<h1>Your Articles</h1>
{articles.map((article) => (
<article key={article.documentId}>
<h2>{article.title}</h2>
<span>Tier: {article.tier}</span>
Add Role-Based Content Filtering in Strapi
Create a custom policy that filters content based on the authenticated user's role. In current Strapi versions (v4/v5), a policy should use the (policyContext, config, { strapi }) signature:
// src/policies/is-authenticated.js
module.exports = async (policyContext, config, { strapi }) => {
if (policyContext.state.user) {
return true;
}
return false;
};Apply the policy and add tier-based filtering in a custom controller:
// src/api/article/controllers/article.js
const { createCoreController } = require('@strapi/strapi').factories;
module.exports = createCoreController('api::article.article', ({ strapi }) => ({
async find(ctx) {
const { user } = ctx.state;
if (!user) {
// Public users only see free articles
ctx.query.filters = { ...ctx.query.filters, tier: 'free' };
}
if (user?.role?.name === 'Author') {
// Authors see only their own articles
ctx.query.filters = { ...ctx.query.filters, author: user.id };
}
const { data, meta } = await super.find(ctx);
return { data, meta };
},
}));Protect Dashboard Routes with Next.js Middleware
Add route-level protection so unauthenticated users can't access /dashboard paths:
// middleware.js
import { NextResponse } from 'next/server';
export function middleware(req) {
// Check for Strapi token in cookies or redirect to login
const token = req.cookies.get('strapiToken');
if (!token) {
return NextResponse.redirect(new URL('/', req.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*'],
};This gives you a working three-layer architecture: Auth0 handles identity, Next.js manages the session bridge, and Strapi enforces content access through its RBAC permissions. You can extend this pattern with Strapi webhooks to trigger notifications on content changes, or add custom middleware for request logging and analytics.
Strapi Open Office Hours
If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours, Monday through Friday, at 6:00 am and 12:30 pm CST on the Strapi Community Discord: Strapi Discord Open Office Hours.
For more details, visit the Strapi documentation and Auth0 documentation.
Get Started in Minutes
npx create-strapi-app@latest in your terminal and follow our Quick Start Guide to build your first Strapi project.