Guides
NextAuth.js Integration

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-here

Troubleshooting 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:

  1. The JWKS endpoint is returning empty keys
  2. Key ID mismatch between the ID token and JWKS
  3. 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:

  1. Cross-site cookie restrictions
  2. Previous authentication errors causing stale cookies

Solution:

  1. Clear browser cookies and try again
  2. Ensure your NEXTAUTH_URL matches your actual URL

Custom Claims Not Appearing

If organizationId or licenses are missing:

  1. Check the profile callback maps organization_id (snake_case from ID token) to organizationId
  2. Check your jwt and session callbacks pass the data through
  3. 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[];
  }
}