Documentation Index
Fetch the complete documentation index at: https://mintlify.com/zitadel/zitadel/llms.txt
Use this file to discover all available pages before exploring further.
Next.js Example
Learn how to integrate ZITADEL authentication into a Next.js application using the secure OAuth 2.0 PKCE flow with NextAuth.js.
Overview
Next.js is a React framework for building full-stack web applications. This example demonstrates user authentication, session management, protected routes, and federated logout.
Auth Library
This example uses next-auth (Auth.js) with openid-client to implement OpenID Connect authentication with PKCE.
What You’ll Build
- Public landing page with sign-in button
- PKCE authentication flow with ZITADEL
- Protected profile page displaying user information and claims
- JWT-based session management with automatic token refresh
- Federated logout that terminates both local and ZITADEL sessions
Prerequisites
Before starting, create a PKCE application in the ZITADEL Console:
- Create a new Web application in your ZITADEL project
- Select PKCE as the authentication method
- Configure redirect URIs:
- Development:
http://localhost:3000/api/auth/callback/zitadel
- Configure post-logout redirect URIs:
- Development:
http://localhost:3000/api/auth/logout/callback
- Copy your Client ID
- Enable refresh tokens in Token Settings (optional, for long-lived sessions)
Enable Dev Mode in ZITADEL Console when using HTTP URLs during local development. Always use HTTPS in production.
Installation
Clone the example repository:
git clone https://github.com/zitadel/example-auth-nextjs.git
cd example-auth-nextjs
Install dependencies:
Configuration
Create a .env.local file:
cp .env.example .env.local
Configure the environment variables:
NODE_ENV=development
PORT=3000
# Session configuration
SESSION_SECRET=your-very-secret-and-strong-session-key
SESSION_DURATION=3600
# ZITADEL configuration
ZITADEL_DOMAIN=https://your-instance.zitadel.cloud
ZITADEL_CLIENT_ID=your-client-id
ZITADEL_CLIENT_SECRET=random-secret-for-nextauth
ZITADEL_CALLBACK_URL=http://localhost:3000/api/auth/callback/zitadel
ZITADEL_POST_LOGIN_URL=/profile
ZITADEL_POST_LOGOUT_URL=http://localhost:3000/api/auth/logout/callback
# NextAuth configuration
NEXTAUTH_URL=http://localhost:3000
Generate secrets:
# Generate SESSION_SECRET
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Generate ZITADEL_CLIENT_SECRET (required by NextAuth even for PKCE)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Implementation
Create app/api/auth/[...nextauth]/route.ts:
import NextAuth from "next-auth";
import type { NextAuthOptions } from "next-auth";
const authOptions: NextAuthOptions = {
providers: [
{
id: "zitadel",
name: "ZITADEL",
type: "oauth",
issuer: process.env.ZITADEL_DOMAIN,
clientId: process.env.ZITADEL_CLIENT_ID!,
clientSecret: process.env.ZITADEL_CLIENT_SECRET!,
wellKnown: `${process.env.ZITADEL_DOMAIN}/.well-known/openid-configuration`,
authorization: {
params: {
scope: "openid profile email offline_access",
},
},
checks: ["pkce"],
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
};
},
},
],
session: {
strategy: "jwt",
maxAge: parseInt(process.env.SESSION_DURATION || "3600"),
},
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
token.idToken = account.id_token;
}
return token;
},
async session({ session, token }) {
session.accessToken = token.accessToken as string;
return session;
},
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
Protected Page
Create app/profile/page.tsx:
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
export default async function ProfilePage() {
const session = await getServerSession();
if (!session) {
redirect("/api/auth/signin");
}
return (
<div className="container">
<h1>Profile</h1>
<div className="profile-info">
<p><strong>Name:</strong> {session.user?.name}</p>
<p><strong>Email:</strong> {session.user?.email}</p>
<p><strong>ID:</strong> {session.user?.id}</p>
</div>
<a href="/api/auth/signout">Sign Out</a>
</div>
);
}
Landing Page
Create app/page.tsx:
import { getServerSession } from "next-auth";
import Link from "next/link";
export default async function HomePage() {
const session = await getServerSession();
return (
<div className="container">
<h1>Welcome to ZITADEL + Next.js</h1>
{session ? (
<div>
<p>Signed in as {session.user?.email}</p>
<Link href="/profile">View Profile</Link>
</div>
) : (
<div>
<p>You are not signed in</p>
<a href="/api/auth/signin">Sign In</a>
</div>
)}
</div>
);
}
Run the Application
Start the development server:
Open http://localhost:3000 in your browser.
Testing the Flow
- Click Sign In on the home page
- You’ll be redirected to ZITADEL for authentication
- After successful login, you’ll return to the profile page
- The profile page displays your user information
- Click Sign Out to test federated logout
Advanced Features
Access Tokens
Access the token in server components:
import { getServerSession } from "next-auth";
const session = await getServerSession();
const accessToken = session?.accessToken;
// Use token to call ZITADEL API
const response = await fetch(
`${process.env.ZITADEL_DOMAIN}/v2/users/me`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
Role-Based Access
Extend the JWT callback to include roles:
callbacks: {
async jwt({ token, account, profile }) {
if (profile) {
token.roles = profile["urn:zitadel:iam:org:project:roles"];
}
return token;
},
}
Resources
Next Steps