How to Set Up Auth0 Securely

How-To Guide

How to Set Up Auth0 Securely

Enterprise-grade authentication for your application

TL;DR

TL;DR (25 minutes): Use the correct application type (Regular Web App for server-side, not SPA). Callback URLs must be exact. No wildcards. Validate tokens server-side using Auth0's JWKS endpoint. Use Authorization Code flow with PKCE. Store tokens in httpOnly cookies, not localStorage. And actually implement logout: it needs to clear both your local session and Auth0's.

Prerequisites:

  • Auth0 account (free tier works)
  • Node.js backend or Next.js application
  • HTTPS for production (required for secure cookies)
  • Basic understanding of OAuth 2.0

Why This Matters

Auth0 handles authentication well, but only if you configure it right. Wildcard callback URLs are the most common mistake: they let attackers redirect tokens to domains they control. Missing token validation and incomplete logout handling round out the top three. Each one can lead to account takeover, and each is easy to miss when you're moving fast.

Step-by-Step Guide

1

Create and configure your Auth0 application

In the Auth0 Dashboard:

# Auth0 Dashboard Configuration

1. Create Application
   - Go to Applications > Create Application
   - Choose "Regular Web Applications" for server-side apps
   - Choose "Single Page Application" only for pure SPAs

2. Configure Application URIs (Settings tab)
   Allowed Callback URLs:
   - http://localhost:3000/api/auth/callback (development)
   - https://yourapp.com/api/auth/callback (production)

   Allowed Logout URLs:
   - http://localhost:3000 (development)
   - https://yourapp.com (production)

   Allowed Web Origins (for SPAs):
   - http://localhost:3000
   - https://yourapp.com

3. Security Settings
   - Token Endpoint Authentication Method: Post
   - Refresh Token Rotation: Enabled
   - Refresh Token Expiration: Absolute (set to 30 days or less)
   - ID Token Expiration: 36000 (10 hours)

4. Advanced Settings > Grant Types
   - Authorization Code: Enabled
   - Refresh Token: Enabled
   - Implicit: DISABLED (not secure for most use cases)

Critical: Never use wildcard (*) in callback URLs. Attackers can redirect tokens to their own domains. Use exact URLs only.

2

Configure environment variables

Add to your .env.local:

# Auth0 Configuration
AUTH0_SECRET='use-openssl-rand-base64-32-to-generate'
AUTH0_BASE_URL='http://localhost:3000'
AUTH0_ISSUER_BASE_URL='https://YOUR_DOMAIN.auth0.com'
AUTH0_CLIENT_ID='your-client-id'
AUTH0_CLIENT_SECRET='your-client-secret'

# For API authorization
AUTH0_AUDIENCE='https://api.yourapp.com'

# Production overrides
# AUTH0_BASE_URL='https://yourapp.com'

Generate a secure secret:

# Generate AUTH0_SECRET
openssl rand -base64 32
3

Install and configure Auth0 SDK for Next.js

npm install @auth0/nextjs-auth0

Create app/api/auth/[auth0]/route.ts:

import { handleAuth, handleLogin, handleLogout, handleCallback } from '@auth0/nextjs-auth0';

export const GET = handleAuth({
  login: handleLogin({
    authorizationParams: {
      audience: process.env.AUTH0_AUDIENCE,
      scope: 'openid profile email offline_access',
    },
    returnTo: '/dashboard',
  }),

  callback: handleCallback({
    afterCallback: async (req, session) => {
      // Optional: Sync user to your database
      // const user = session.user;
      // await syncUserToDatabase(user);
      return session;
    },
  }),

  logout: handleLogout({
    returnTo: '/',
  }),
});

export const POST = GET;
4

Set up UserProvider

Wrap your app in app/layout.tsx:

import { UserProvider } from '@auth0/nextjs-auth0/client';

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

Protect pages with middleware

Create middleware.ts:

import { withMiddlewareAuthRequired, getSession } from '@auth0/nextjs-auth0/edge';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export default withMiddlewareAuthRequired(async function middleware(req: NextRequest) {
  const res = NextResponse.next();
  const session = await getSession(req, res);

  // Check for admin routes
  if (req.nextUrl.pathname.startsWith('/admin')) {
    const roles = session?.user?.['https://yourapp.com/roles'] || [];

    if (!roles.includes('admin')) {
      return NextResponse.redirect(new URL('/unauthorized', req.url));
    }
  }

  return res;
});

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

Protect Server Components and API routes

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

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

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

  return (
    <div>
      <h1>Welcome, {session.user.name}</h1>
      <p>Email: {session.user.email}</p>
      <img src={session.user.picture} alt="Profile" />
    </div>
  );
}

Protect API routes:

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

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

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

  const userId = session.user.sub;

  // Fetch data scoped to user
  const data = await db.records.findMany({
    where: { ownerId: userId },
  });

  return NextResponse.json(data);
});
7

Set up a protected API with token validation

For a separate API backend, validate access tokens:

npm install express-oauth2-jwt-bearer
// Express API server
import express from 'express';
import { auth, requiredScopes } from 'express-oauth2-jwt-bearer';

const app = express();

// Token validation middleware
const checkJwt = auth({
  audience: process.env.AUTH0_AUDIENCE,
  issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL,
  tokenSigningAlg: 'RS256',
});

// Public endpoint
app.get('/api/public', (req, res) => {
  res.json({ message: 'Public endpoint - no auth required' });
});

// Protected endpoint - requires valid token
app.get('/api/private', checkJwt, (req, res) => {
  const userId = req.auth?.payload.sub;
  res.json({
    message: 'Protected endpoint',
    userId,
  });
});

// Scoped endpoint - requires specific permission
app.get('/api/admin',
  checkJwt,
  requiredScopes('admin:read'),
  (req, res) => {
    res.json({ message: 'Admin endpoint' });
  }
);

// Error handling
app.use((err, req, res, next) => {
  if (err.name === 'UnauthorizedError') {
    return res.status(401).json({
      error: 'Invalid or missing token',
    });
  }
  next(err);
});

app.listen(3001);
8

Configure roles and permissions in Auth0

# Auth0 Dashboard - Authorization Setup

1. Create API (APIs > Create API)
   - Name: Your App API
   - Identifier: https://api.yourapp.com
   - Signing Algorithm: RS256

2. Define Permissions (API > Permissions tab)
   - read:own_data - Read own data
   - write:own_data - Write own data
   - admin:read - Read all data
   - admin:write - Modify all data

3. Create Roles (User Management > Roles)
   - user: read:own_data, write:own_data
   - admin: all permissions

4. Add Action to include roles in token
   Actions > Flows > Login > Add Action > Custom

// Action code
exports.onExecutePostLogin = async (event, api) => {
  const namespace = 'https://yourapp.com';

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

Implement proper logout

// Client component
'use client';

import { useUser } from '@auth0/nextjs-auth0/client';

export function LogoutButton() {
  const { user } = useUser();

  if (!user) return null;

  return (
    <a href="/api/auth/logout">
      Log out
    </a>
  );
}

// For federated logout (clears Auth0 session too)
// This is configured in handleLogout:
handleLogout({
  returnTo: '/',  // Where to redirect after logout
})

// The SDK handles:
// 1. Clearing local session cookie
// 2. Redirecting to Auth0 /v2/logout
// 3. Redirecting back to your app
10

Handle refresh tokens securely

// The Auth0 Next.js SDK handles refresh automatically
// But here's how to manually refresh if needed:

import { getAccessToken } from '@auth0/nextjs-auth0';

export async function GET(req: Request) {
  try {
    // This automatically refreshes if expired
    const { accessToken } = await getAccessToken({
      refresh: true,
    });

    // Use accessToken to call your API
    const response = await fetch('https://api.yourapp.com/data', {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    return Response.json(await response.json());
  } catch (error) {
    // Handle refresh failure - user needs to re-authenticate
    if (error.code === 'ERR_EXPIRED_ACCESS_TOKEN') {
      return Response.redirect('/api/auth/login');
    }
    throw error;
  }
}

Auth0 Security Checklist:

  • Exact callback URLs - No wildcards. Exact match only, trailing slashes and all.
  • Implicit grant disabled - Use Authorization Code with PKCE instead
  • Tokens validated server-side - Check the JWKS endpoint, don't trust unverified tokens
  • AUTH0_SECRET generated securely - Run openssl rand -base64 32, never commit it
  • Refresh token rotation on - Prevents token reuse after rotation
  • API audience set - Scopes your tokens to your API, not anyone else's
  • Roles added via Action - Custom claims don't get included automatically
  • Logout clears both sessions - Local cookie and Auth0's session. One without the other doesn't work.
  • HTTPS in production - httpOnly cookies require it

How to Verify It Worked

  1. Callback URL mismatch: Try a modified callback URL in the browser; it should be rejected outright
  2. Token validation: Send a fake or expired token to your API; you want a 401, not leaked data
  3. Token refresh: Let an access token expire and confirm the SDK silently refreshes it without a re-login prompt
  4. Role-based access: Hit an admin route as a non-admin; you should get redirected to /unauthorized, not a blank 403
  5. Logout: Sign out, then try navigating to a protected page; you should be redirected to login
  6. Cookie flags: Open DevTools > Application > Cookies and confirm httpOnly and Secure are both set on the session cookie

Common Errors & Troubleshooting

Error: "Callback URL mismatch"

The URL you're redirecting to doesn't exactly match what's listed in Auth0 Dashboard. Check for trailing slashes, http vs https, and the exact path. Even a single character off will fail.

Error: "Invalid token" or "jwt malformed"

Token validation failed. Make sure AUTH0_AUDIENCE and AUTH0_ISSUER_BASE_URL match your Auth0 config, and that you're sending the access token to your API, not the ID token.

Error: "Missing required parameter: audience"

AUTH0_AUDIENCE isn't set. This is your API identifier from Auth0 Dashboard (usually something like https://api.yourapp.com).

Roles not appearing in token

Auth0 doesn't include roles automatically. You need a Post-Login Action that explicitly adds them as custom claims.

Session not persisting

AUTH0_SECRET is either unset or different between deployments. It must be consistent; rotating it invalidates all existing sessions.

Logout not working completely

The returnTo URL must be in "Allowed Logout URLs" in Auth0 Dashboard. If it's missing, the logout redirect fails and Auth0's session stays active.

When should I use ID tokens vs access tokens?

ID tokens are for your frontend: they carry user info like name and email. Access tokens are for your API. Don't send ID tokens to your API; they're not meant for that and won't validate the same way.

How do I add custom data to tokens?

Use a Post-Login Auth0 Action and add custom claims with a namespaced key (like https://yourapp.com/roles). The namespace prevents collisions with standard JWT claims.

Should I store users in my own database?

Usually yes. Auth0 handles authentication, but your app almost always needs its own user records for relationships and app-specific data. A post-login Action or webhook can sync users on first login.

How do I handle multi-tenancy?

Auth0 Organizations is the right tool for B2B multi-tenancy. Each org can have its own connection, branding, and member list. Include org_id in your access tokens to scope API requests.

Related guides:OAuth Setup · JWT Security · Session Management

How-To Guides

How to Set Up Auth0 Securely