Getting Started with Hosted Authentication
Learn how to integrate OAuth42's hosted authentication pages into your Next.js application in under 10 minutes.
What You'll Build
In this tutorial, you'll create a Next.js application that uses OAuth42's hosted authentication pages. Users will be redirected to OAuth42's secure login page, authenticate, and then return to your app with a session managed by NextAuth.
Benefits of Hosted Auth:
- No need to build login/signup UI
- Enterprise-grade security out of the box
- Customizable branding and features
- Automatic MFA support
- Faster time to market
Prerequisites
- Node.js 18+ installed
- An OAuth42 account (sign up at oauth42.com)
- Basic knowledge of Next.js and React
Step 1: Create an OAuth42 Application
- Log in to the OAuth42 Portal
- Click "Applications" in the sidebar
- Click "Create New Application"
- Fill in the application details:
- Name: "My Next.js App"
- Redirect URIs:
https://yourdomain.com/api/auth/callback/oauth42
- Copy your Client ID and Client Secret (you'll need these in Step 3)
⚠️ Important: Replace yourdomain.com with your actual domain, or use http://localhost:3000 for local testing. The callback path must be /api/auth/callback/oauth42.
💡 Testing with Your Account: You can test the login flow using the same email/password you used to sign up for OAuth42. Any user in your organization can authenticate with your applications.
Step 2: Create a Next.js Application
npx create-next-app@latest my-oauth42-app
cd my-oauth42-app
npm install @oauth42/nextStep 3: Configure Environment Variables
Create a .env.local file in your project root:
# OAuth42 Configuration (server-side)
OAUTH42_CLIENT_ID=your_client_id_here
OAUTH42_CLIENT_SECRET=your_client_secret_here
OAUTH42_ISSUER=https://api.oauth42.com
# OAuth42 Configuration (client-side - for logout)
NEXT_PUBLIC_OAUTH42_CLIENT_ID=your_client_id_here
NEXT_PUBLIC_OAUTH42_ISSUER=https://api.oauth42.com
# NextAuth Configuration
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=generate_a_random_secret_here⚠️ Important: The NEXT_PUBLIC_ prefixed variables are exposed to the browser. Never include the client secret in NEXT_PUBLIC_ variables.
💡 Generate a random secret:
openssl rand -base64 32Step 4: Configure Next.js for Local Development (Optional)
If you're using https://localhost:8443 for your OAUTH42_ISSUER, you need to configure Next.js to accept self-signed SSL certificates.
Update the dev script in your package.json:
{
"scripts": {
"dev": "NODE_TLS_REJECT_UNAUTHORIZED='0' next dev"
}
}💡 Production Note: This only affects the dev script. Production builds will use proper SSL validation. Skip this step if you're using the production API at https://api.oauth42.com.
Step 5: Create Authentication Configuration
First, create a lib directory and auth configuration file:
mkdir -p lib
touch lib/auth.tsAdd the following code to lib/auth.ts:
import { createAuth } from '@oauth42/next/server';
const { auth, handlers } = createAuth({
clientId: process.env.OAUTH42_CLIENT_ID,
clientSecret: process.env.OAUTH42_CLIENT_SECRET,
issuer: process.env.OAUTH42_ISSUER,
});
export const authOptions = auth;
export { handlers };💡 What's happening here:
- The
createAuthfunction sets up NextAuth with OAuth42 configuration - It automatically configures OIDC discovery, PKCE, token handling, and session callbacks
- Returns both the auth options (for use in pages) and handlers (for the API route)
- All environment variables are read automatically from your
.env.localfile
Now create the NextAuth API route directory and file:
mkdir -p "app/api/auth/[...nextauth]"
touch "app/api/auth/[...nextauth]/route.ts"📁 Note: The folder name is literally [...nextauth] (including brackets and dots). This is a Next.js catch-all route that handles all /api/auth/* paths.
Add the following code to app/api/auth/[...nextauth]/route.ts:
import { handlers } from '@/lib/auth';
export const { GET, POST } = handlers;🚨 Important: Next.js App Router Limitation
Route files in Next.js App Router can only export route handlers (GET, POST, etc.), not other values like authOptions. That's why we keep authOptions in lib/auth.ts and only export handlers from the route file.
Step 6: Protect Routes with Middleware
Create middleware.ts in your project root:
import { withOAuth42Auth } from '@oauth42/next/middleware';
export default withOAuth42Auth({
// Paths that don't require authentication
publicPaths: ['/api/auth', '/auth'],
});
export const config = {
matcher: [
// Match all paths except static files
'/((?!_next/static|_next/image|favicon.ico).*)',
]
}💡 What's happening here:
- The
withOAuth42Authmiddleware protects routes by checking for a valid session - Unauthenticated users are redirected to
/auth/signin - The
publicPathsoption excludes certain paths from authentication
✨ Automatic Token Refresh:
- The middleware automatically refreshes expired access tokens
- Uses secure refresh token rotation - each refresh issues a new refresh token and invalidates the old one
- Users stay logged in seamlessly without re-authenticating
- Works transparently on every navigation - no client-side polling needed
⚠️ Important: The middleware runs on the Edge runtime. Always import from @oauth42/next/middleware (not /server) to avoid Node.js module errors.
🚨 Critical: Never put '/api' in publicPaths!
If you add '/api' to publicPaths, token refresh will be skipped for ALL your API routes, causing 401 errors when tokens expire. Only '/api/auth' should be public.
Multi-App Cookie Isolation (Optional)
If you're running multiple Next.js apps on the same domain (e.g., multiple apps on localhost during development), you'll need to use unique cookie prefixes to prevent session conflicts.
Why this matters:
- By default, NextAuth uses the same cookie names for all apps
- When apps share a domain, logging into one app can log you out of another
- The
cookiePrefixoption gives each app unique cookie names
Step 1: Add cookiePrefix to your auth configuration in lib/auth.ts:
import { createAuth } from '@oauth42/next/server';
// Cookie prefix - must match middleware.ts
const COOKIE_PREFIX = 'myapp';
const { auth, handlers } = createAuth({
clientId: process.env.OAUTH42_CLIENT_ID,
clientSecret: process.env.OAUTH42_CLIENT_SECRET,
issuer: process.env.OAUTH42_ISSUER,
cookiePrefix: COOKIE_PREFIX, // Unique prefix for this app
});
export const authOptions = auth;
export { handlers };Step 2: Use the same prefix in your middleware:
import { withOAuth42Auth } from '@oauth42/next/middleware';
// Cookie prefix - must match lib/auth.ts
const COOKIE_PREFIX = 'myapp';
export default withOAuth42Auth({
cookiePrefix: COOKIE_PREFIX, // Must match the prefix in createAuth()
publicPaths: ['/api/auth', '/auth'],
});
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}Example: Running 3 apps on localhost
| App | Port | Cookie Prefix | Session Cookie |
|---|---|---|---|
| Portal | 3000 | portal | portal.session-token |
| Admin | 3001 | admin | admin.session-token |
| Customer App | 3002 | customer | customer.session-token |
With unique prefixes, you can be logged in as different users in each app simultaneously!
Client-Side Session Access (Optional)
If you need client-side session access (e.g., using the useSession hook), wrap your app with SessionProvider:
// app/providers.tsx
"use client"
import { SessionProvider } from "@oauth42/next/client"
export default function Providers({
children,
}: {
children: React.ReactNode
}) {
return (
<SessionProvider
refetchInterval={0}
refetchOnWindowFocus={false}
>
{children}
</SessionProvider>
)
}Then use it in your app/layout.tsx:
import Providers from "./providers"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}⚠️ Important: Disable background polling
- Set
refetchInterval={0}to disable background session polling - Token refresh is handled by middleware (on navigation), not client-side polling
- Background polling can cause issues with OAuth42's secure refresh token rotation
Step 7: Create a Protected Dashboard Page
Create app/dashboard/page.tsx:
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { redirect } from "next/navigation"
export default async function Dashboard() {
const session = await getServerSession(authOptions)
if (!session) {
redirect("/auth/signin")
}
return (
<div className="min-h-screen bg-gray-100 py-12 px-4">
<div className="max-w-4xl mx-auto">
<h1 className="text-3xl font-bold text-gray-900 mb-8">
Welcome, {session.user?.name || session.user?.email}!
</h1>
<div className="bg-white shadow rounded-lg p-6">
<h2 className="text-xl font-semibold mb-4">Profile Information</h2>
<dl className="space-y-2">
<div>
<dt className="text-sm font-medium text-gray-500">Email</dt>
<dd className="text-sm text-gray-900">{session.user?.email}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Name</dt>
<dd className="text-sm text-gray-900">{session.user?.name}</dd>
</div>
</dl>
</div>
</div>
</div>
)
}Step 8: Create the Sign In Page
Create app/auth/signin/page.tsx:
'use client';
import { signIn, useOAuth42Session } from '@oauth42/next/client';
import { useSearchParams, useRouter } from 'next/navigation';
import { Suspense, useEffect } from 'react';
function SignInContent() {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl') || '/dashboard';
const error = searchParams.get('error');
const { isAuthenticated, loading } = useOAuth42Session();
// Redirect if already authenticated
useEffect(() => {
if (!loading && isAuthenticated) {
router.push(callbackUrl);
}
}, [loading, isAuthenticated, callbackUrl, router]);
// Display error if present
if (error) {
let errorMessage = 'Authentication error';
if (error === 'RefreshAccessTokenError') {
errorMessage = 'Your session has expired. Please login again.';
}
return (
<div className="min-h-screen flex items-center justify-center px-4 bg-gray-100">
<div className="max-w-md w-full space-y-8 p-8 rounded-2xl border bg-white shadow">
<div className="text-center">
<h2 className="text-3xl font-bold text-gray-900">
Authentication Error
</h2>
<p className="mt-2 text-red-500">
{errorMessage}
</p>
</div>
<button
onClick={() => signIn('oauth42', { callbackUrl })}
className="w-full py-3 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors"
>
Try Again
</button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center px-4">
<div className="max-w-md w-full">
{/* Logo */}
<div className="text-center mb-8">
<div className="flex justify-center mb-4">
<div className="w-16 h-16 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center">
<svg className="w-10 h-10 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
</div>
<h1 className="text-3xl font-bold text-gray-900">Sign In</h1>
<p className="mt-2 text-gray-600">Authenticate with OAuth42</p>
</div>
{/* Sign in card */}
<div className="bg-white rounded-lg shadow-lg p-8">
<p className="text-center text-gray-600 text-sm mb-6">
Click below to sign in via OAuth42 hosted authentication.
You'll be able to use your email, Google, or other configured providers.
</p>
<button
onClick={() => signIn('oauth42', { callbackUrl })}
className="w-full py-4 px-6 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white font-semibold rounded-lg transition-all duration-300 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 flex items-center justify-center gap-3"
>
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
Sign in with OAuth42
</button>
</div>
</div>
</div>
);
}
export default function SignInPage() {
return (
<Suspense fallback={null}>
<SignInContent />
</Suspense>
);
}💡 What's happening here:
- This page is shown when unauthenticated users try to access protected routes
- The
signIn('oauth42', { callbackUrl })function triggers the standard NextAuth OAuth flow - Users are redirected to OAuth42's hosted login page
- After authentication, they return via
/api/auth/callback/oauth42 - The
useOAuth42Sessionhook checks if already logged in and redirects to dashboard
Step 9: Add Logout Button
Create a logout button component that properly clears the user's session. Create components/LogoutButton.tsx:
'use client';
import { logout } from '@oauth42/next/client';
export default function LogoutButton() {
const handleLogout = async () => {
await logout({
callbackUrl: '/',
clientId: process.env.NEXT_PUBLIC_OAUTH42_CLIENT_ID,
issuer: process.env.NEXT_PUBLIC_OAUTH42_ISSUER,
});
};
return (
<button
onClick={handleLogout}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors"
>
Sign Out
</button>
);
}💡 Note: This uses the NEXT_PUBLIC_ environment variables configured in Step 3.
💡 Why use the SDK's logout instead of NextAuth's signOut?
- The SDK's
logoutfunction clears the user's app selection from OAuth42's session registry - This enables the account picker to show on next login (Google-style multi-account support)
- Using NextAuth's
signOutdirectly only clears the local session, so the next login auto-selects the previous account - The SDK handles both OAuth42 logout and NextAuth session cleanup automatically
Now add the logout button to your dashboard page:
import { getServerSession } from "next-auth/next"
import { authOptions } from "@/lib/auth"
import { redirect } from "next/navigation"
import LogoutButton from "@/components/LogoutButton"
export default async function Dashboard() {
const session = await getServerSession(authOptions)
if (!session) {
redirect("/auth/signin")
}
return (
<div className="min-h-screen bg-gray-100 py-12 px-4">
<div className="max-w-4xl mx-auto">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold text-gray-900">
Welcome, {session.user?.name || session.user?.email}!
</h1>
<LogoutButton />
</div>
{/* ... rest of dashboard content */}
</div>
</div>
)
}Step 10: Test Your Application
- Start your development server:
npm run dev - Open http://localhost:3000 in your browser
- Navigate to a protected route (e.g.,
/dashboard) - You'll be redirected to the sign in page, then to OAuth42's hosted login
- Sign in using your OAuth42 account credentials
- After successful authentication, you'll be redirected back to your dashboard
✅ Success! If you see your dashboard with the user's email and name displayed, OAuth42 hosted authentication is working correctly!
Customizing Your Hosted Auth Pages
You can customize the look and feel of your hosted auth pages from the OAuth42 Portal:
- Go to Applications → Your App → Hosted Auth Settings
- Upload your logo
- Set your primary and accent colors
- Configure features (signup, password reset, MFA, etc.)
- Set password requirements
Troubleshooting
Here are solutions to common issues you might encounter:
Error: redirect_uri_mismatch
Cause: The callback URL in your OAuth42 application doesn't match the one NextAuth is using.
Solution: Update your OAuth42 application's redirect URI to:
http://localhost:3000/api/auth/callback/oauth42Error: self-signed certificate in certificate chain
Cause: You're using a local OAuth42 instance with a self-signed certificate.
Solution: Update your package.json dev script:
"dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 next dev"Note: This is only for development. Production should use valid SSL certificates.
Issue: Session not found after login
Cause: The cookiePrefix in middleware doesn't match the one in lib/auth.ts.
Solution: Ensure both files use the same cookiePrefix value.
Issue: Navbar or UI not showing user info after login
Cause: Components using useSession() aren't wrapped in SessionProvider.
Solution: Wrap your app with SessionProvider from @oauth42/next/client in your layout (see "Client-Side Session Access" section).
Issue: Environment variables undefined in browser
Cause: Using server-only environment variables in client components.
Solution: Use NEXT_PUBLIC_ prefix for client-side variables. Restart your dev server after adding new environment variables.
Next Steps
Need Help?
Join our Discord community or check out the documentation for more detailed guides.