Init.

Install Auth.js

After installing auth.js you will need to add a secret and Keycloak variables in your .env file.

>_ Terminal

yarn add next-auth@beta

.env

AUTH_SECRET={secret}
AUTH_KEYCLOAK_ID={CLIENT_ID}
AUTH_KEYCLOAK_SECRET={CLIENT_SECRET}
AUTH_KEYCLOAK_ISSUER={ISSUER}

Setup Auth.js

>_ Terminal

echo "" > .\src\lib\auth.ts

auth.js

import NextAuth, { Session } from "next-auth";
import { JWT } from "next-auth/jwt";
import Keycloak from "next-auth/providers/keycloak";

declare module "next-auth" {
  interface User {
    // Add your additional properties here:
  }

  interface Session {
    idToken: string;
    accessToken: string;
    expiresAt: number;
    user: User;
  }
}

function refreshAccessToken(token: JWT) {
  return fetch(
    `${process.env.AUTH_KEYCLOAK_ISSUER}/protocol/openid-connect/token`,
    {
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        client_id: process.env.AUTH_KEYCLOAK_ID ?? "",
        client_secret: process.env.AUTH_KEYCLOAK_SECRET ?? "",
        grant_type: "refresh_token",
        refresh_token: token?.refreshToken as string,
      }),
      method: "POST",
      cache: "no-store",
    }
  );
}

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [Keycloak],
  callbacks: {
    async jwt({ token, account }) {
      if (account) {
        token.idToken = account.id_token;
        token.accessToken = account.access_token;
        token.refreshToken = account.refresh_token;
        token.expiresAt = account.expires_at;
        return token;
      }
      if (Date.now() < (token?.expiresAt as number) * 1000 - 60 * 1000) {
        return token;
      } else {
        try {
          const response = await refreshAccessToken(token);
          const tokens = await response.json();
          if (!response.ok) throw tokens;

          const updatedToken: JWT = {
            ...token,
            idToken: tokens.id_token,
            accessToken: tokens.access_token,
            expiresAt: Math.floor(
              Date.now() / 1000 + (tokens.expires_in as number)
            ),
            refreshToken: tokens.refresh_token ?? token.refreshToken,
          };
          return updatedToken;
        } catch (error) {
          console.error("Error refreshing access token", error);

          // By returning null it looks like user is logged off
          // Maybe push this way and logout keycloak session & redirect

          return null;

          // Or else you just return the session with the error to the frontend
          // return { ...token, error: "RefreshAccessTokenError" }
        }
      }
    },
    async session({ session, token }: any) {
      session.accessToken = token.accessToken;
      session.idToken = token.idToken;
      session.error = token.error;

      return session;
    },
  },
});

export async function logout(session: Session | null) {
  if (!session) {
    return;
  }

  await fetch(
    `${process.env.AUTH_KEYCLOAK_ISSUER}/protocol/openid-connect/logout?id_token_hint=${session.idToken}`,
    { method: "GET" }
  );

  await signOut();
}

Setup API Route

Add a Route Handler under /app/api/auth/[...nextauth]/route.ts.

route.ts

import { handlers } from "@lib/auth" // Referring to the auth.ts we just created
export const { GET, POST } = handlers

Setup Middleware

Add a middleware to keep the session alive.

>_ Terminal

echo "export { auth as middleware } from '@lib/auth'" > .\src\app\middleware.ts