NextAuth.js Integration Guide
This guide provides instructions on how to integrate Optare SSO with a Next.js application using NextAuth.js (v4 or v5).
Configuration
Optare SSO is strictly OpenID Connect (OIDC) compliant and requires specific checks for security.
Prerequisites
- Client ID and Client Secret from the Optare Developer Console.
- Issuer URL:
https://id.optare.one(or your specific instance URL). - NextAuth.js installed in your project.
Complete Configuration Example
IMPORTANT The
client.token_endpoint_auth_method: 'client_secret_post'setting is required. Optare's token endpoint expects credentials in the POST body, not in the Authorization header.
import NextAuth from 'next-auth'
import type { NextAuthOptions } from 'next-auth'
export const authOptions: NextAuthOptions = {
providers: [
{
id: 'optare',
name: 'Optare ID',
type: 'oauth',
wellKnown: `${process.env.OPTARE_ISSUER || 'https://id.optare.one'}/.well-known/openid-configuration`,
clientId: process.env.OPTARE_CLIENT_ID,
clientSecret: process.env.OPTARE_CLIENT_SECRET,
authorization: {
params: {
scope: 'openid email profile'
}
},
idToken: true,
checks: ['pkce', 'state', 'nonce'],
// CRITICAL: Force client credentials in POST body instead of Basic Auth header
client: {
token_endpoint_auth_method: 'client_secret_post',
},
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
// Custom claims from Optare ID token
organizationId: profile.organization_id,
licenses: profile.licenses,
entitlements: profile.entitlements,
}
}
}
],
callbacks: {
async jwt({ token, profile }) {
// Persist custom claims to the JWT token
if (profile) {
token.organizationId = profile.organization_id;
token.licenses = profile.licenses;
token.entitlements = profile.entitlements;
}
return token;
},
async session({ session, token }) {
// Make custom claims available in the session
if (session.user) {
session.user.organizationId = token.organizationId as string;
session.user.licenses = token.licenses as string[];
session.user.entitlements = token.entitlements as string[];
}
return session;
},
},
secret: process.env.NEXTAUTH_SECRET,
}
export default NextAuth(authOptions)Environment Variables
Create a .env.local file with:
# Optare ID OAuth Configuration
OPTARE_ISSUER=https://id.optare.one
OPTARE_CLIENT_ID=your-client-id
OPTARE_CLIENT_SECRET=your-client-secret
# NextAuth Configuration
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-random-secret-hereTroubleshooting Common Issues
"invalid_request: client_id is required"
This error occurs when NextAuth sends the client_id and client_secret in the HTTP Authorization header instead of the POST body.
Solution:
Add the client configuration block to force POST body authentication:
{
// ... other provider config
client: {
token_endpoint_auth_method: 'client_secret_post',
},
}"no valid key found in issuer's jwks_uri"
This error means NextAuth cannot verify the ID token signature.
Possible causes:
- The JWKS endpoint is returning empty keys
- Key ID mismatch between the ID token and JWKS
- Environment variable issues on the server
Solution: Verify the JWKS endpoint returns keys:
curl https://id.optare.one/.well-known/jwks.json"nonce is required for openid scope"
This error occurs if the nonce parameter is missing from the authorization request.
Solution:
Ensure you have checks: ['pkce', 'state', 'nonce'] in your provider configuration.
"State cookie was missing"
This typically indicates cookie issues or multiple failed authentication attempts.
Possible causes:
- Cross-site cookie restrictions
- Previous authentication errors causing stale cookies
Solution:
- Clear browser cookies and try again
- Ensure your
NEXTAUTH_URLmatches your actual URL
Custom Claims Not Appearing
If organizationId or licenses are missing:
- Check the
profilecallback mapsorganization_id(snake_case from ID token) toorganizationId - Check your
jwtandsessioncallbacks pass the data through - Verify the user has product licenses assigned in Optare
Type Definitions (TypeScript)
To properly type your session, add this to your types/next-auth.d.ts:
import NextAuth, { DefaultSession } from "next-auth"
declare module "next-auth" {
interface Session {
user: {
organizationId?: string;
licenses?: string[];
entitlements?: string[];
} & DefaultSession["user"]
}
interface Profile {
organizationId?: string;
licenses?: string[];
entitlements?: string[];
}
}