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?
...
}
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)