How to Set Up Auth0 Securely

Share
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). Configure exact callback URLs - no wildcards. Validate tokens server-side using Auth0's JWKS. Use Authorization Code flow with PKCE. Store tokens in httpOnly cookies, not localStorage. Implement proper logout with Auth0 session clearing.

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 is a powerful identity platform, but misconfiguration leads to serious vulnerabilities. Wildcard callback URLs, missing token validation, and improper logout handling are common mistakes that can result in account takeover. This guide covers secure Auth0 implementation.

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
  • Implicit grant disabled - Use Authorization Code with PKCE
  • Token validation enabled - Verify tokens server-side with JWKS
  • AUTH0_SECRET is secure - Generated with openssl, never committed
  • Refresh token rotation enabled - Prevents token reuse
  • API audience configured - Tokens are scoped to your API
  • Roles in tokens - Custom action adds roles to ID/access tokens
  • Proper logout implemented - Clears both local and Auth0 sessions
  • HTTPS in production - Required for secure cookies
  • Session cookie httpOnly - SDK handles this automatically

How to Verify It Worked

  1. Test callback URL validation: Try modifying callback URL in browser - should fail
  2. Test token validation: Send invalid token to API - should return 401
  3. Test expired tokens: Wait for token expiry, verify refresh works
  4. Test role-based access: Access admin route without admin role - should deny
  5. Test logout: Sign out, try to access protected page - should redirect to login
  6. Verify cookies: Check that session cookie has httpOnly and secure flags

Common Errors & Troubleshooting

Error: "Callback URL mismatch"

The callback URL doesn't exactly match what's configured in Auth0. Check for trailing slashes, http vs https, and exact path matching.

Error: "Invalid token" or "jwt malformed"

Token validation failed. Verify the audience and issuer match your Auth0 configuration. Check that you're using the access token, not the ID token.

Error: "Missing required parameter: audience"

Set AUTH0_AUDIENCE in your environment. This is your API identifier from Auth0 Dashboard.

Roles not appearing in token

Create an Action in Auth0 to add roles as custom claims. Roles must be explicitly added to tokens.

Session not persisting

Check AUTH0_SECRET is set and consistent across deployments. Verify cookie settings allow persistence.

Logout not working completely

Ensure returnTo URL is in "Allowed Logout URLs" in Auth0 Dashboard. Use federated logout to clear Auth0 session.

When should I use ID tokens vs access tokens?

ID tokens are for your frontend to get user info (name, email). Access tokens are for authorizing API requests. Never send ID tokens to your API - use access tokens with proper audience.

How do I add custom data to tokens?

Use Auth0 Actions to add custom claims. Use a namespaced key (like https://yourapp.com/roles) to avoid conflicts with standard claims.

Should I store users in my own database?

Usually yes. Auth0 handles authentication, but you'll need user records for relationships and app-specific data. Use the post-login Action or a webhook to sync users.

How do I handle multi-tenancy?

Use Auth0 Organizations for B2B multi-tenancy. Each organization can have its own connection, branding, and member management. Add org_id to your access tokens.

Related guides:OAuth Setup · JWT Security · Session Management

How-To Guides

How to Set Up Auth0 Securely