Keycloak server v20.0.1 returns a not-before-policy flag with value 0 in the OAuth2 callback after successful sign in.

This flag is supposed to be persisted in the Account table managed by Prisma, but since the Prisma schema won't correctly map the not-before-policy field, thanks to the hyphens, your Next.js / Next-Auth server will fail to linkAccount (to create an account) but will successfully create a User table entry.

Worse, because a User was successfully created, your subsequent attempts to sign in will fail with a different cryptic error and no stacktrace even when debug is on – "To confirm your identity, sign in with the same account you used originally."

[next-auth][error][OAUTH_CALLBACK_HANDLER_ERROR] 
https://next-auth.js.org/errors#oauth_callback_handler_error 
Invalid `p.account.create()` invocation in
node_modules/@next-auth/prisma-adapter/dist/index.js:19:42

  16 },
  17 updateUser: ({ id, ...data }) => p.user.update({ where: { id }, data }),
  18 deleteUser: (id) => p.user.delete({ where: { id } }),
→ 19 linkAccount: (data) => p.account.create({
       data: {
         provider: 'keycloak',
         type: 'oauth',
...
       }
     })

Unknown arg `not-before-policy` in data.not-before-policy for type AccountUncheckedCreateInput. Available args:
type AccountUncheckedCreateInput {
...
}

Creating a new column not_before_policy and using @map("not-before-policy") won't help since that would create a database column named not-before-policy ...and we're looking to change the shape of the object passed to prisma.account.create({ data }).

Fix

If you need to figure out what additional fields are being supplied to the OAuth2 callback, you need to get the original persistence error in your stacktrace. For that, you need to delete the specific User and, if exists, Account for the user trying to sign in – turn on the debug flag in [...nextauth].js – then try signing in and check your logs.

Update your Account schema to handle additional fields received from Keycloak.

model Account {
  ...
  refresh_expires_in Int?
  not_before_policy  Int?
  ...
}
prisma.schema

For [...nextauth].js, this is a fairly generic snippet that will help you replace keys of objects received in callback – in this case, replacing "not-before-policy" with "not_before_policy". It uses a Proxy object to keep things separate.

import NextAuth from "next-auth"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import KeycloakProvider from "next-auth/providers/keycloak"

// replace this import with the path to your PrismaClient
import { prisma } from "../../../prisma/context"



/** @type {ProxyHandler<import("next-auth/adapters").Adapter<boolean>>} */
const adapterProxy = {
  replaceProps: {
    linkAccount: new Map([
      ['not-before-policy', 'not_before_policy'],
    ]),
  },

  renameProps(data, replaceMap) {
    for (const [oldKey, newKey] of replaceMap.entries()) {
      if (oldKey in data) {
        data[newKey] = data[oldKey];
        delete data[oldKey];
      }
    }
  },

  get(target, prop, receiver) {
    const value = target[prop];

    if (value instanceof Function) {
      return function (...args) {
        const replaceProps = adapterProxy.replaceProps;

        if (prop in replaceProps && args.length && typeof args[0] === 'object') {
          adapterProxy.renameProps(args[0], replaceProps[prop]);
        }

        return value.apply(this === receiver ? target : this, args);
      };
    }

    return value;
  },
}

/** @type {import("next-auth/adapters").Adapter} */
const adapter = new Proxy(PrismaAdapter(prisma), adapterProxy);

/** @type {import("next-auth").NextAuthOptions} */
export const authOptions = {
  debug: true,
  adapter: adapter,
  providers: [
    KeycloakProvider({
      clientId: process.env.KEYCLOAK_ID,
      clientSecret: process.env.KEYCLOAK_SECRET,
      issuer: process.env.KEYCLOAK_ISSUER,
    }),
  ],
}

export default NextAuth(authOptions)
[...nextauth].js