Skip to content

Okta Authentication

License Server Detail uses NextAuth v5 with Okta as the OAuth provider for secure authentication. This guide covers the complete setup process.

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

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
  1. Log in to Okta Admin Console

    Navigate to your Okta admin dashboard at https://your-org-admin.okta.com.

  2. Create a new application

    Go to Applications > Applications > Create App Integration.

    Select:

    • Sign-in method: OIDC - OpenID Connect
    • Application type: Web Application
  3. Configure the application

    Set the following values:

    FieldValue
    App integration nameLicense Server Detail
    Grant typeAuthorization Code, Refresh Token
    Sign-in redirect URIshttp://localhost:3000/api/auth/callback/okta
    Sign-out redirect URIshttp://localhost:3000
  4. Configure API scopes

    Under Okta API Scopes, enable:

    • openid
    • profile
    • email
    • offline_access (for refresh tokens)
  5. 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

Create or update your .env file with the Okta credentials:

Terminal window
# Okta OAuth Configuration
AUTH_OKTA_ID=0oa1234567890abcdef
AUTH_OKTA_SECRET=your-client-secret-here
AUTH_OKTA_ISSUER=https://your-org.okta.com/oauth2/default
# NextAuth Configuration
AUTH_SECRET=generate-a-strong-random-secret-at-least-32-chars
AUTH_TRUST_HOST=false
NEXTAUTH_URL=http://localhost:3000

Generate a secure secret using one of these methods:

Terminal window
openssl rand -base64 32
┌──────────────┐
│ 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 │
└──────────────┘
// JWT callback in auth.config.ts
async 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);
}
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',
};
}
}

The application includes an encrypted client-side token cache for improved performance:

┌─────────────────────────────────────────────────────────────┐
│ 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 is managed by AuthProvider
import { 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
}
middleware.ts
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).*)'],
};
app/dashboard/page.tsx
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>
);
}
// Server Component
import { 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>;
}
types/next-auth.d.ts
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;
}
}
IssueCauseSolution
Redirect loop on sign inMissing or incorrect redirect URIVerify URIs in Okta match exactly
Token refresh failsExpired refresh tokenUser must re-authenticate
401 errors after sign inClock skew between serversSync server time with NTP
Session not persistingAUTH_SECRET mismatchUse same secret across instances

Enable debug logging for authentication issues:

Terminal window
# Development
NEXT_PUBLIC_LOG_LEVEL=debug
# Check server logs for:
# [auth][token:refresh] - Token refresh attempts
# [auth][callback] - OAuth callback handling
# [auth][session] - Session creation/updates