Integrating Custom OIDC Provider with Next.js and Next Auth v5: A Step-by-Step Guide

Table of contents

📖 Source Code: https://github.com/ednoah/projects/tree/main/nextjs-authjs-oidc
Introduction
Let’s be real – integrating Ping Identity’s OAuth provider into Next.js wasn’t a walk in the park. The NextAuth v5 (now calling Auth.js) docs left me scratching my head, so I thought I’d share my own solution to save you from the same pain!
In modern web applications, secure authentication is essential for protecting user data and ensuring privacy. One of the most popular ways to handle authentication in a Next.js application is by using NextAuth.js, a flexible and easy-to-use authentication library for React. In this post, we'll dive deep into integrating NextAuth v5 with Next.js and OpenID Connect (OIDC), a secure authentication protocol that allows applications to authenticate users through identity providers (such as Google, GitHub, or custom OIDC providers).
By the end of this guide, you'll have a fully functioning authentication system with Next.js and clean UI with shadcn that works seamlessly with OIDC providers.
What is Auth.js?
Auth.js (formerly known as NextAuth.js) is an open-source authentication library designed for Next.js applications. It offers an easy-to-integrate authentication system with built-in support for a wide range of authentication providers (Google, Facebook, GitHub, and more), as well as databases and custom OAuth implementations. Auth.js is known for its simplicity, flexibility, and extensibility, making it a popular choice for handling authentication in Next.js applications.
You can find the latest documentation for Auth.js here: Auth.js Documentation
What is OpenID Connect (OIDC)?
OpenID Connect (OIDC) is an identity layer built on top of the OAuth 2.0 protocol. It allows applications to authenticate users by delegating the authentication process to an identity provider (such as Google, Microsoft, or your custom provider). OIDC provides a standardized way to securely authenticate users without handling sensitive user credentials directly.
OIDC is widely used because it offers secure authentication and ensures that sensitive data (like passwords) is never exposed to your application.
Prerequisites
Before you start, you’ll need the following:
Node.js pnpm: Make sure you have these installed on your machine.
Download Node.js: https://nodejs.org/en/download/package-manager
pnpm is a fast, disk space-efficient package manager for JavaScript that uses a unique approach to store dependencies in a global content-addressable storage, improving performance and reducing duplication across projects.
npm install -g pnpm
An OIDC Provider: You have access to a OIDC provider
Everything setup, then let’s start 🚀
Setup
Step 1: Setting Up a Next.js Project
First, create a Next.js project by running the following command in your terminal:
npx create-next-app@latest nextjs-authjs-oidc
√ Would you like to use TypeScript? ... Yes
√ Would you like to use ESLint? ... Yes
√ Would you like to use Tailwind CSS? ... Yes
√ Would you like your code inside a `src/` directory? ... Yes
√ Would you like to use App Router? (recommended) ... Yes
√ Would you like to use Turbopack for `next dev`? ... Yes
√ Would you like to customize the import alias (`@/*` by default)? ... No
Navigate into your project folder:
cd .\nextjs-authjs-oidc\
Step 2: Install Dependencies
We will install the required dependencies for our project.
Install shadcn ui library:
pnpm dlx shadcn@latest init
Which style would you like to use? » New York
Which color would you like to use as the base color? Neutral
Would you like to use CSS variables for theming? yes
# It looks like you are using React 19.
# Some packages may fail to install due to peer dependency issues in npm (see https://ui.shadcn.com/react-19).
How would you like to proceed? Use --force
Installing button component from shadcn.
pnpm dlx shadcn@latest add button
Install other dependencies:
pnpm add next-auth@beta # Auth.js package
Step 3: Setup Basics
Environment variables
Auth.js requires a random value to encrypt token and email verification hashes. More infos
The following command will create an environment variable named AUTH_SECRET and store it in .env.local file.
npx auth secret
We need to define other environment variables for the OIDC provider. Add the following variables to the .env.local file.
Example using GitHub as OAuth Provider
AUTH_GITHUB_ID= # The client ID from GitHub.
AUTH_GITHUB_SECRET= # The client secret from GitHub
Example when using custom OIDC Provider (We’ll use that one)
Return URL: http://localhost:3000/api/auth/callback/<provider_id>
AUTH_OIDC_CLIENT_ID= # The client ID for your OIDC provider.
AUTH_OIDC_CLIENT_SECRET= # The client secret for your OIDC provider.
AUTH_OIDC_ISSUER= # The OIDC provider's issuer URL.
Define routes
I use a file to specify which routes are publicly accessible, where to redirect when you sign in, and what the default entry point is. To set this up, create a file named routes.ts
in the /src/lib/
directory.
touch src/lib/routes.ts
Export now three variables
export const ROOT = '/sign-in';
export const PUBLIC_ROUTES = ['/sign-in'];
export const DEFAULT_REDIRECT = '/dashboard';
Step 4: Configure Auth.js
auth.ts
Next, create the Auth.js configuration file and object. This is where you can control the behavior of the library and define custom authentication logic, adapters, etc. It is recommended to create an auth.ts
file in the project for all frameworks. In this file, all options should be passed to the framework-specific initialization function, and then the route handler(s), sign-in, and sign-out methods should be exported.
When you want to add custom parameters, such as the username, to the user session, you can include it using the profile parameter. In TypeScript, you then need to update the user type to include "username" as a parameter. The console log in the profile is also useful to collect when a user signs in to your application.
You need then to add the username also in the jwt token and the user session.
Create file auth.ts under src folder:
touch src/auth.ts
Paste following content
import NextAuth from "next-auth"
//import GitHub from "next-auth/providers/github";
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
//GitHub, // Example when using existing provider from next-auth
{
id: 'custom_oidc_provider', // Unique identifier for the provider
name: 'Custom OIDC Provider', // Name of the provider
type: 'oidc', // Provider type
issuer: process.env.AUTH_OIDC_ISSUER, // Issuer URL from environment variable
clientId: process.env.AUTH_OIDC_CLIENT_ID, // Client ID for authentication from environment variable
clientSecret: process.env.AUTH_OIDC_CLIENT_SECRET, // Client Secret for authentication from environment variable
profile(profile) {
// Log essential information when a user logs in
console.log('User logged in', { userId: profile.sub });
return {
id: profile.sub, // User ID from the profile
username: profile.sub?.toLowerCase(), // Username (converted to lowercase)
name: `${profile.given_name} ${profile.family_name}`, // Full name from given and family names
email: profile.email, // User email
};
}
}
],
pages: {
error: '/sign-in', // Redirect to the home page (or other page) on error
signIn: '/sign-in', // Redirect to the home page (or other page) for sign-in
signOut: '/sign-in', // Redirect to the home page (or other page) after sign-out
},
callbacks: {
jwt({ token, user }) {
if(user) token.username = user.username
return token
},
session({ session, token }) {
session.user.username = token.username
return session
}
}
})
If typescript marks “username” red, don’t worry, we will fix that now.
- Add file next-auth.d.ts file under types: Create folder and file:
mkdir -p src/types/ && touch src/types/next-auth.d.ts
- Add the username parameter as string to the session, user and the JWT Token
Ref: https://next-auth.js.org/getting-started/typescript#module-augmentation
import { DefaultSession, DefaultUser } from "next-auth"
import { JWT, DefaultJWT } from "next-auth/jwt"
declare module "next-auth" {
interface Session {
user: {
id: string,
username: string,
name: string,
email: string
} & DefaultSession
}
interface User extends DefaultUser {
username: string
}
}
declare module "next-auth/jwt" {
interface JWT extends DefaultJWT {
username: string,
}
}
Route handler
Add a Route Handler. Create following folders and file:
mkdir -p src/app/api/auth/[...nextauth] && touch src/app/api/auth/[...nextauth]/route.ts
Add the handlers from the auth.ts file
import { handlers } from "@/auth"
export const { GET, POST } = handlers
Middleware
In the middleware, we define authentication logic that checks if a user is authenticated and whether the requested route is public. If the route is public and the user is authenticated, they are redirected to a default page. If the user is not authenticated and tries to access a non-public route, they are redirected to the login page. The matcher
configuration ensures this middleware is applied to all routes except for static and API routes.
- Create Middleware File:
touch src/middleware.ts
import { auth } from "@/auth"
import { DEFAULT_REDIRECT, PUBLIC_ROUTES, ROOT } from "@/lib/routes";
// Or like this if you need to do something here.
export default auth((req) => {
const { nextUrl } = req; // Extract the nextUrl (requested URL) from the request object
// Check if the user is authenticated (i.e., if the 'auth' property exists in the request)
const isAuthenticated = !!req.auth;
// Check if the current route is a public route (i.e., if it's listed in the PUBLIC_ROUTES array)
const isPublicRoute = PUBLIC_ROUTES.includes(nextUrl.pathname);
// If the route is public and the user is authenticated, redirect them to the default redirect URL
if (isPublicRoute && isAuthenticated) {
return Response.redirect(new URL(DEFAULT_REDIRECT, nextUrl)); // Redirect to the default page if logged in
}
// If the route is not public and the user is not authenticated, redirect them to the root (login) page
if (!isAuthenticated && !isPublicRoute) {
return Response.redirect(new URL(ROOT, nextUrl)); // Redirect to the root URL (e.g., login) if not authenticated
}
// Optionally log in the middleware the user session. Remove in production mode
console.log(req.auth) // { session: { user: { ... } } }
})
// Read more: https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}
SessionProvider
Finally, we need to wrap our App in the SessionProvider from Next.js
Wrap the children in the SessionProvider
File: src/app/layout.tsx
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} font-[family-name:var(--font-geist-sans)] antialiased`}
>
<SessionProvider>
{children}
</SessionProvider>
</body>
</html>
);
}
Step 5: Build Sign-In Page
Let’s build now our frontend. Starting with the Sign-In page.
Create a new page sign-in:
mkdir -p src/app/sign-in && touch src/app/sign-in/page.tsx
Make that a client component and add and add a sign in button. On click it calles the signin function from next-auth.
'use client';
import { Button } from "@/components/ui/button";
import { signIn } from "next-auth/react";
export default function SignInPage() {
const handleSignIn = () => signIn('custom_oidc_provider');
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-6">
<div className="w-full max-w-sm rounded-xl bg-white border p-6 space-y-6">
<h1 className="text-xl font-semibold text-gray-800">
Welcome
</h1>
<p className="text-sm text-gray-600">
This is a sample web application built with Next.js and Auth.js to demonstrate the implementation of authentication using a custom OpenID Connect (OIDC) provider. It serves as a practical example for integrating custom OIDC authentication flows in modern web applications, showcasing the flexibility and simplicity of Auth.js for managing secure sign-in processes.
<br />
<br />
The app is lightweight and modular, making it an excellent reference for developers seeking to implement similar authentication solutions in their projects.
</p>
<div className="space-y-4">
<Button
className="w-full flex items-center justify-center"
onClick={handleSignIn}
variant="outline"
>
Sign in
</Button>
</div>
</div>
</div>
);
}
Step 6: Build Dashboard
The dashboard will contain a Header navigation with user info and a sign-out button. On the page, the session will be displayed.
Create dashboard page:
mkdir -p src/app/dashboard && touch src/app/dashboard/page.tsx
The user session can be received over the auth function.
import { auth } from '@/auth';
import { Header } from '@/components/header';
const DashboardPage = async () => {
const session = await auth();
return (
<div>
<Header />
<div className='px-4 py-10 space-y-4'>
<h1 className='text-2xl font-semibold'>Dashboard</h1>
<p>User session:</p>
<p className='text-sm'>{JSON.stringify(session?.user)}</p>
</div>
</div>
);
};
export default DashboardPage;
Create Header component.
touch src/components/header.tsx
File content:
import { auth, signOut } from '@/auth'
import { Button } from "@/components/ui/button"
function SignOutButton() {
return (
<form
action={async () => {
'use server'
await signOut()
}}
>
<Button type="submit" variant="outline">Sign out</Button>
</form>
)
}
export async function Header() {
const session = await auth()
return (
<header className='p-4 bg-gray-100'>
<nav className='flex justify-between items-center'>
<p className='font-bold'>Next Auth v5 + Next.js + OIDC</p>
<div className="flex items-center space-x-4">
<span>{session?.user?.name}</span>
<SignOutButton />
</div>
</nav>
</header>
)
}
Start application
Now we’re finished. Let’s start the application and see how if it works.
pnpm run dev
Lets open URL: http://localhost:3000/
When starting the application, we are redirected to the login page.
Let’s sign in. After sign in, you are redirected to dashboard page and see the user session. When nothing happens when you click sign-in, check the ID in the handleSignIn function. That has to match a provider in auth.js.
Final words
Integrating NextAuth.js v5 with Next.js and OIDC allows you to easily implement secure and flexible authentication in your application. By using OIDC providers like Google or custom provider, you can leverage powerful authentication mechanisms without handling sensitive data directly.
Source Code: GitHub
You find the entire template in my GitHub. Link: https://github.com/ednoah/projects/tree/main/nextjs-authjs-oidc
Recommendation
Utilize a state management solution to store user information after signing in. Leverage the callback function to retrieve user details post-authentication, such as fetching data from a user API or other sources. This logic doesn't need to reside within the provider's profile configuration.
Enhance your application by assigning roles to users, such as admin roles. Update the
routes.ts
file to include additional routes, like admin-specific paths. Then, manage access control for these routes directly in the middleware to ensure proper role-based access.
Hopefully I could help you with this article and if you have any feedback let me know.
Happy coding 🔥
Subscribe to my newsletter
Read articles from Noah Ediz directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Noah Ediz
Noah Ediz
Hi, I'm Noah Ediz, a passionate developer based in Switzerland. I currently work in the DevOps team at a large company, where I focus on building robust systems and infrastructure. I’ve previously co-founded a startup and am now working on a new venture. When I’m not at the office, I love coding with Node.js, constantly exploring new ideas and technologies. Follow my journey as I share insights on DevOps, startups, and coding!