Skip to main content

How to Implement OAuth 2.0 with Auth0 in 2026

·APIScout Team
Share:

Auth0 handles the hardest parts of authentication: OAuth flows, social login, MFA, token management, and security updates. This guide walks through a complete integration: login/signup, social providers, JWT verification, role-based access, and API protection.

What You'll Build

  • Login and signup with email/password
  • Social login (Google, GitHub)
  • Protected API routes with JWT verification
  • Role-based access control (admin, user)
  • User profile management

Prerequisites: Next.js 14+, Auth0 account (free: 25,000 MAU).

1. Setup

Create Auth0 Application

  1. Go to Auth0 Dashboard
  2. Create a new Application → "Regular Web Application"
  3. Note your: Domain, Client ID, Client Secret
  4. Set Allowed Callback URLs: http://localhost:3000/api/auth/callback
  5. Set Allowed Logout URLs: http://localhost:3000
  6. Set Allowed Web Origins: http://localhost:3000

Install the SDK

npm install @auth0/nextjs-auth0

Environment Variables

# .env.local
AUTH0_SECRET=<random-32-char-string>  # openssl rand -hex 32
AUTH0_BASE_URL=http://localhost:3000
AUTH0_ISSUER_BASE_URL=https://your-tenant.us.auth0.com
AUTH0_CLIENT_ID=your_client_id
AUTH0_CLIENT_SECRET=your_client_secret

2. Add Authentication

Create Auth Routes

// app/api/auth/[auth0]/route.ts
import { handleAuth } from '@auth0/nextjs-auth0';

export const GET = handleAuth();

This single route handler creates four endpoints:

  • /api/auth/login — redirect to Auth0 login
  • /api/auth/callback — handle OAuth callback
  • /api/auth/logout — clear session and log out
  • /api/auth/me — return current user profile

Add the Provider

// app/layout.tsx
import { UserProvider } from '@auth0/nextjs-auth0/client';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <UserProvider>{children}</UserProvider>
      </body>
    </html>
  );
}

Login / Logout Buttons

// components/AuthButtons.tsx
'use client';
import { useUser } from '@auth0/nextjs-auth0/client';

export function AuthButtons() {
  const { user, isLoading } = useUser();

  if (isLoading) return <div>Loading...</div>;

  if (user) {
    return (
      <div>
        <img src={user.picture!} alt={user.name!} width={32} height={32} />
        <span>{user.name}</span>
        <a href="/api/auth/logout">Log Out</a>
      </div>
    );
  }

  return (
    <div>
      <a href="/api/auth/login">Log In</a>
      <a href="/api/auth/login?screen_hint=signup">Sign Up</a>
    </div>
  );
}

3. Protect Pages

Client-Side Protection

// app/dashboard/page.tsx
'use client';
import { useUser } from '@auth0/nextjs-auth0/client';
import { redirect } from 'next/navigation';

export default function Dashboard() {
  const { user, isLoading } = useUser();

  if (isLoading) return <div>Loading...</div>;
  if (!user) redirect('/api/auth/login');

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome, {user.name}!</p>
      <p>Email: {user.email}</p>
    </div>
  );
}

Server-Side Protection

// app/dashboard/page.tsx
import { getSession } from '@auth0/nextjs-auth0';
import { redirect } from 'next/navigation';

export default async function Dashboard() {
  const session = await getSession();

  if (!session) {
    redirect('/api/auth/login');
  }

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome, {session.user.name}!</p>
    </div>
  );
}

Protect entire route groups with middleware:

// middleware.ts
import { withMiddlewareAuthRequired } from '@auth0/nextjs-auth0/edge';

export default withMiddlewareAuthRequired();

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*', '/api/protected/:path*'],
};

4. Social Login

Add Social Connections

In Auth0 Dashboard → Authentication → Social:

  1. Google: Create OAuth credentials in Google Cloud Console, paste Client ID + Secret
  2. GitHub: Create OAuth App in GitHub Developer Settings, paste credentials
  3. Apple: Requires Apple Developer Account, configure Sign In with Apple

Custom Login UI

To show specific social buttons:

<a href="/api/auth/login?connection=google-oauth2">
  Sign in with Google
</a>
<a href="/api/auth/login?connection=github">
  Sign in with GitHub
</a>

5. Protect API Routes

JWT Verification

// app/api/protected/route.ts
import { getSession } from '@auth0/nextjs-auth0';
import { NextResponse } from 'next/server';

export async function GET() {
  const session = await getSession();

  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  // session.user contains the user profile
  // session.accessToken contains the JWT

  return NextResponse.json({
    message: 'This is protected data',
    user: session.user.email,
  });
}

External API Protection

For protecting a separate API (not Next.js), verify JWTs directly:

// On your API server
import { auth } from 'express-oauth2-jwt-bearer';

const checkJwt = auth({
  audience: 'https://api.yourapp.com',
  issuerBaseURL: 'https://your-tenant.us.auth0.com/',
  tokenSigningAlg: 'RS256',
});

app.get('/api/data', checkJwt, (req, res) => {
  res.json({ data: 'protected' });
});

6. Role-Based Access Control

Set Up Roles

In Auth0 Dashboard → User Management → Roles:

  • Create "admin" role
  • Create "user" role
  • Assign roles to users

Add Roles to Token

Create an Auth0 Action (Actions → Flows → Login):

// Auth0 Action: Add Roles to Token
exports.onExecutePostLogin = async (event, api) => {
  const namespace = 'https://yourapp.com';
  const roles = event.authorization?.roles || [];

  api.idToken.setCustomClaim(`${namespace}/roles`, roles);
  api.accessToken.setCustomClaim(`${namespace}/roles`, roles);
};

Check Roles in Your App

// lib/auth.ts
import { getSession } from '@auth0/nextjs-auth0';

export async function requireRole(role: string) {
  const session = await getSession();

  if (!session) {
    throw new Error('Not authenticated');
  }

  const roles = session.user['https://yourapp.com/roles'] || [];

  if (!roles.includes(role)) {
    throw new Error('Insufficient permissions');
  }

  return session;
}

// Usage in API route
export async function DELETE(req: Request) {
  const session = await requireRole('admin');
  // Only admins reach this point
}

Admin-Only Page

// app/admin/page.tsx
import { getSession } from '@auth0/nextjs-auth0';
import { redirect } from 'next/navigation';

export default async function AdminPage() {
  const session = await getSession();
  const roles = session?.user['https://yourapp.com/roles'] || [];

  if (!roles.includes('admin')) {
    redirect('/dashboard');
  }

  return <h1>Admin Panel</h1>;
}

7. User Profile Management

Update Profile

// app/api/user/profile/route.ts
import { getSession } from '@auth0/nextjs-auth0';
import { ManagementClient } from 'auth0';

const management = new ManagementClient({
  domain: process.env.AUTH0_ISSUER_BASE_URL!.replace('https://', ''),
  clientId: process.env.AUTH0_M2M_CLIENT_ID!,
  clientSecret: process.env.AUTH0_M2M_CLIENT_SECRET!,
});

export async function PATCH(req: Request) {
  const session = await getSession();
  if (!session) return new Response('Unauthorized', { status: 401 });

  const { name, nickname } = await req.json();

  await management.users.update(
    { id: session.user.sub },
    { name, nickname }
  );

  return Response.json({ success: true });
}

Production Checklist

ItemNotes
Update callback/logout URLs for production domainRequired
Enable MFA (multi-factor authentication)Recommended
Configure brute-force protectionEnabled by default
Set up custom domain (auth.yourapp.com)Professional look
Enable anomaly detectionDetects suspicious logins
Configure password policyMinimum complexity requirements
Set token expiration appropriatelyAccess: 1 hour, Refresh: 7-30 days
Add rate limiting to auth endpointsPrevent abuse

Pricing

PlanMAU (Monthly Active Users)Price
Free25,000$0
EssentialUnlimitedFrom $35/month
ProfessionalUnlimitedFrom $240/month
EnterpriseUnlimitedCustom

Free tier includes: social login, MFA, 2 social connections, custom domain.

Common Mistakes

MistakeImpactFix
Not setting AUTH0_SECRETSessions are insecureGenerate random 32-char secret
Forgetting callback URLs for productionLogin fails in prodAdd production URLs in Auth0 dashboard
Storing roles only in Auth0 metadataSlow role checks (API call per request)Add roles to JWT via Actions
Not rotating secretsSecurity riskRotate secrets quarterly
Client-side only auth checksSecurity bypassAlways verify server-side

Testing Your Auth Integration

Auth0 provides first-class testing support that most teams underuse. Every application in the Auth0 dashboard has a "Test" tab that lets you simulate the full login flow — walking through the authorization URL, consent screen, and token exchange — without touching your application code. This is invaluable for isolating whether a problem lives in your Auth0 configuration or your Next.js integration.

For automated testing, the Auth0 Management API lets you create test users programmatically, run your test suite, and clean up afterward. This keeps your test users out of production data and lets CI pipelines run end-to-end auth flows reliably:

// lib/auth-test-utils.ts
export function mockSession(user: Partial<Auth0User>) {
  // Override getSession() in tests
  jest.mock('@auth0/nextjs-auth0', () => ({
    getSession: () => Promise.resolve({ user: { sub: 'test|123', ...user } }),
  }));
}

For E2E tests with Playwright or Cypress, Auth0 supports a testingToken option that bypasses the interactive login flow — your test can programmatically obtain a token and inject it into the browser session rather than automating the login UI on every test run. This is significantly faster and more reliable than scraping the login form.

One important operational note: Auth0's test environments share rate limits with your tenant's production quotas on the free and Essential plans. If your CI pipeline runs hundreds of auth tests per day, you can hit rate limits that also affect real users. The practical fix is either to mock getSession() at the unit test level (as shown above) and reserve real Auth0 calls for a smaller set of integration tests, or to use a separate Auth0 tenant for testing entirely.

Auth0 vs Building Your Own Authentication

The real question when evaluating Auth0 is what you're paying for — and the honest answer is that you're paying to not maintain a security-sensitive system. The list of things Auth0 handles that you would otherwise own: brute-force protection (built-in, automatic lockouts after failed attempts), anomaly detection (flags logins from new devices or geographies), MFA enrollment UI (flows for TOTP, SMS, and push notifications), and security patches when authentication vulnerabilities are discovered. When a new OAuth exploit surfaces, Auth0 patches their infrastructure. If you've rolled your own auth, that's your emergency.

At 25,000 MAU, Auth0's free tier makes this a straightforward decision — you get enterprise-grade security infrastructure at zero cost. The calculus shifts above 25,000 MAU, where Auth0 starts at $35/month on the Essential plan. At that point, the question becomes whether $35-240/month is worth the engineering hours you'd spend maintaining auth. For most teams, it is — auth maintenance is a persistent tax on your engineering capacity, not a one-time cost.

The B2B SaaS case is where Auth0's value is clearest and hardest to replicate. Enterprise buyers expect SSO — specifically SAML-based SSO so they can provision and deprovision users through their identity provider. Building SAML yourself is genuinely difficult: the spec is sprawling, IdP-specific behavior varies widely (Azure AD, Okta, and Google Workspace each have quirks), and a SAML implementation bug can be a hard blocker for a customer's IT department. Auth0's SAML support is turnkey. If you're selling to enterprises and SSO comes up in deals — and it will — Auth0 pays for itself in the first closed deal.

The cases where Auth0 is not the right call: when you have strict data ownership requirements and can't route authentication through a third-party service, when your MAU count is large enough that Auth0's per-MAU pricing becomes material, or when you're building a consumer app with basic email/password requirements and no enterprise aspirations. For those cases, NextAuth.js (now Auth.js) or Lucia provide solid self-hosted alternatives — you own the data, there's no per-seat cost at scale, and the auth requirements are simple enough that the maintenance burden is manageable.

Debugging Auth Issues

Auth0's dashboard gives you three powerful debugging tools that should be your first stop when something breaks. The Logs section provides real-time login events with structured error codes and plain-English descriptions — you can see exactly why a login attempt failed, which user attempted it, and from what IP. The Token Inspector lets you paste any JWT and decode and validate it against your tenant's JWKS, which is useful for verifying that claims (like roles) are actually being included in tokens. The Dashboard "Try" flow (Applications → your app → Advanced Settings → Test) lets you manually walk the entire OAuth flow to isolate whether a problem is in Auth0 configuration or your application.

The most common error codes you'll encounter: access_denied means the user denied consent or a rule/action blocked the login; invalid_client means the Client ID doesn't match what Auth0 expects (usually an environment variable issue); redirect_uri_mismatch is the single most frequent production misconfiguration — the callback URL in your request isn't in the Allowed Callback URLs list in your Auth0 application settings; and too_many_attempts means brute-force protection triggered, which is usually what you want to see, but can surface unexpectedly if a legitimate user has many failed attempts.

For programmatic log access, the Management API exposes the same log data:

const logs = await management.logs.getAll({ q: 'type:f', per_page: 10 });
// type:f = failed logins; type:s = successful logins

This is useful for alerting — if you want to get paged when failed login rates spike, you can poll this endpoint and route events to your monitoring system. Auth0 also supports log streaming to Datadog, Splunk, and similar platforms via the Log Streams feature in the dashboard, which is the recommended approach for production monitoring rather than polling, and the only viable approach at high MAU where log volume makes periodic API polling impractical.


Choosing an auth provider? Compare Auth0 vs Clerk vs Firebase Auth on APIScout — pricing, features, and developer experience.

Related: How to Implement Passwordless Auth with Magic Links, How to Add Firebase Auth to a React App, Set Up Clerk Authentication in Next.js 2026

The API Integration Checklist (Free PDF)

Step-by-step checklist: auth setup, rate limit handling, error codes, SDK evaluation, and pricing comparison for 50+ APIs. Used by 200+ developers.

Join 200+ developers. Unsubscribe in one click.