Okta Authentication
License Server Detail uses NextAuth v5 with Okta as the OAuth provider for secure authentication. This guide covers the complete setup process.
Overview
Section titled “Overview”The authentication system provides:
- OAuth 2.0 with PKCE - Secure authorization flow
- JWT Strategy - Stateless session management
- Automatic Token Refresh - Seamless access token renewal
- Encrypted Token Cache - Client-side token storage with AES-GCM
Prerequisites
Section titled “Prerequisites”Before configuring authentication, you need:
- An Okta developer or enterprise account
- Admin access to create OAuth applications
- The License Server Detail application deployed or running locally
Okta Application Setup
Section titled “Okta Application Setup”-
Log in to Okta Admin Console
Navigate to your Okta admin dashboard at
https://your-org-admin.okta.com. -
Create a new application
Go to Applications > Applications > Create App Integration.
Select:
- Sign-in method: OIDC - OpenID Connect
- Application type: Web Application
-
Configure the application
Set the following values:
Field Value App integration name License Server Detail Grant type Authorization Code, Refresh Token Sign-in redirect URIs http://localhost:3000/api/auth/callback/oktaSign-out redirect URIs http://localhost:3000 -
Configure API scopes
Under Okta API Scopes, enable:
openidprofileemailoffline_access(for refresh tokens)
-
Note your credentials
After saving, note the following values:
- Client ID - Used as
AUTH_OKTA_ID - Client Secret - Used as
AUTH_OKTA_SECRET - Okta Domain - Used to construct
AUTH_OKTA_ISSUER
- Client ID - Used as
Environment Configuration
Section titled “Environment Configuration”Create or update your .env file with the Okta credentials:
# Okta OAuth ConfigurationAUTH_OKTA_ID=0oa1234567890abcdefAUTH_OKTA_SECRET=your-client-secret-hereAUTH_OKTA_ISSUER=https://your-org.okta.com/oauth2/default
# NextAuth ConfigurationAUTH_SECRET=generate-a-strong-random-secret-at-least-32-charsAUTH_TRUST_HOST=falseNEXTAUTH_URL=http://localhost:3000Generating AUTH_SECRET
Section titled “Generating AUTH_SECRET”Generate a secure secret using one of these methods:
openssl rand -base64 32node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Use the NextAuth secret generator at https://generate-secret.vercel.app/32
Authentication Flow
Section titled “Authentication Flow”Sign-In Process
Section titled “Sign-In Process”┌──────────────┐│ User ││ Browser │└──────┬───────┘ │ │ 1. Click "Sign In" ▼┌──────────────┐ 2. Redirect to Okta ┌──────────────┐│ Next.js │ ──────────────────────────> │ Okta ││ Server │ │ │└──────────────┘ └──────┬───────┘ ▲ │ │ 3. User authenticates │ │ 4. Redirect with auth code │ │ <─────────────────────────────────────────┘ │ │ 5. Exchange code for tokens │ 6. Create JWT session ▼┌──────────────┐│ User ││ Dashboard │└──────────────┘Token Refresh Flow
Section titled “Token Refresh Flow”// JWT callback in auth.config.tsasync jwt({ token, account, user }) { // Initial sign in if (account && user) { return { ...token, accessToken: account.access_token, refreshToken: account.refresh_token, accessTokenExpires: account.expires_at * 1000, user, }; }
// Return token if not expired if (Date.now() < token.accessTokenExpires) { return token; }
// Refresh expired token return refreshAccessToken(token);}Refresh Token Implementation
Section titled “Refresh Token Implementation”async function refreshAccessToken(token: JWT) { try { const response = await fetch( `${process.env.AUTH_OKTA_ISSUER}/v1/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: token.refreshToken, client_id: process.env.AUTH_OKTA_ID!, client_secret: process.env.AUTH_OKTA_SECRET!, }), } );
const tokens = await response.json();
if (!response.ok) { throw tokens; }
return { ...token, accessToken: tokens.access_token, accessTokenExpires: Date.now() + tokens.expires_in * 1000, refreshToken: tokens.refresh_token ?? token.refreshToken, }; } catch (error) { logger.error('[auth][token:refresh] Error refreshing token', { error }); return { ...token, error: 'RefreshAccessTokenError', }; }}Client-Side Token Store
Section titled “Client-Side Token Store”The application includes an encrypted client-side token cache for improved performance:
Token Store Architecture
Section titled “Token Store Architecture”┌─────────────────────────────────────────────────────────────┐│ Secure Token Store │├─────────────────────────────────────────────────────────────┤│ Storage: sessionStorage (cleared on tab close) ││ Encryption: AES-GCM via Web Crypto API ││ TTL: Configurable (default: session duration) ││ Fallback: In-memory cache if sessionStorage unavailable │├─────────────────────────────────────────────────────────────┤│ Source of Truth: Server-side JWT (NextAuth) ││ Cache Purpose: Reduce API calls for token retrieval │└─────────────────────────────────────────────────────────────┘Token Store Usage
Section titled “Token Store Usage”// Token store is managed by AuthProviderimport { useSession } from 'next-auth/react';import { useTokenStore } from '@/lib/security/token-store';
function MyComponent() { const { data: session } = useSession(); const tokenStore = useTokenStore();
// Token is automatically available in session const accessToken = session?.accessToken;
// The HTTP client automatically injects the token // No manual token handling needed in most cases}Protected Routes
Section titled “Protected Routes”Middleware Configuration
Section titled “Middleware Configuration”import { auth } from '@/lib/auth';
export default auth((req) => { const isAuthenticated = !!req.auth; const isAuthPage = req.nextUrl.pathname.startsWith('/auth'); const isApiRoute = req.nextUrl.pathname.startsWith('/api'); const isPublicApi = req.nextUrl.pathname.startsWith('/api/health');
// Allow public API routes if (isPublicApi) { return; }
// Redirect authenticated users away from auth pages if (isAuthPage && isAuthenticated) { return Response.redirect(new URL('/dashboard', req.url)); }
// Protect dashboard routes if (req.nextUrl.pathname.startsWith('/dashboard') && !isAuthenticated) { return Response.redirect(new URL('/auth/signin', req.url)); }});
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],};Page-Level Protection
Section titled “Page-Level Protection”import { auth } from '@/lib/auth';import { redirect } from 'next/navigation';
export default async function DashboardPage() { const session = await auth();
if (!session) { redirect('/auth/signin'); }
return ( <div> <h1>Welcome, {session.user?.name}</h1> {/* Dashboard content */} </div> );}Session Management
Section titled “Session Management”Accessing Session Data
Section titled “Accessing Session Data”// Server Componentimport { auth } from '@/lib/auth';
export default async function ServerComponent() { const session = await auth(); return <div>User: {session?.user?.email}</div>;}
// Client Component'use client';import { useSession } from 'next-auth/react';
export function ClientComponent() { const { data: session, status } = useSession();
if (status === 'loading') { return <div>Loading...</div>; }
if (!session) { return <div>Not authenticated</div>; }
return <div>User: {session.user?.email}</div>;}Session Type Definitions
Section titled “Session Type Definitions”import 'next-auth';
declare module 'next-auth' { interface Session { user: { id: string; name: string; email: string; image?: string; }; accessToken: string; error?: string; }}
declare module 'next-auth/jwt' { interface JWT { accessToken: string; refreshToken: string; accessTokenExpires: number; error?: string; }}Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”| Issue | Cause | Solution |
|---|---|---|
| Redirect loop on sign in | Missing or incorrect redirect URI | Verify URIs in Okta match exactly |
| Token refresh fails | Expired refresh token | User must re-authenticate |
| 401 errors after sign in | Clock skew between servers | Sync server time with NTP |
| Session not persisting | AUTH_SECRET mismatch | Use same secret across instances |
Debug Logging
Section titled “Debug Logging”Enable debug logging for authentication issues:
# DevelopmentNEXT_PUBLIC_LOG_LEVEL=debug
# Check server logs for:# [auth][token:refresh] - Token refresh attempts# [auth][callback] - OAuth callback handling# [auth][session] - Session creation/updatesNext Steps
Section titled “Next Steps”- Real-Time Streaming - WebSocket/SSE with authentication
- API Authentication - License Monitor API keys
- Production Setup - Production configuration