PKCE code_verifier cookie was missing.. on keycloak OIDC login
Environment
System:
OS: macOS 14.5
CPU: (10) arm64 Apple M1 Max
Memory: 857.22 MB / 32.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 20.12.0 - /usr/local/bin/node
Yarn: 1.22.17 - /usr/local/bin/yarn
npm: 10.5.0 - /usr/local/bin/npm
pnpm: 9.5.0 - ~/Library/pnpm/pnpm
bun: 1.1.25 - ~/.bun/bin/bun
Browsers:
Brave Browser: 119.1.60.118
Chrome: 126.0.6478.185
Safari: 17.5
npmPackages:
next: 15.0.0-canary.103 => 15.0.0-canary.103
next-auth: 5.0.0-beta.18 => 5.0.0-beta.18
react: 19.0.0-rc-187dd6a7-20240806 => 19.0.0-rc-187dd6a7-20240806
Reproduction URL
https://github.com/MarkLyck/keycloak-pkce-error-reproduction
Describe the issue
I have been stuck on this error for 2 weeks, and could really use some help.
I'm using [email protected], with multiple Keycloak providers.
The "main" keycloak provider with direct login works fine both on localhost, and when the app is deployed on Vercel. However we also have two keycloak providers with OIDC logins, which only exists in production. When I deploy my app to production, and attempt to login from these two different sites through Keycloak I get the error:
[31m[auth][error][0m InvalidCheck: PKCE code_verifier cookie was missing.. Read more at https://errors.authjs.dev#invalidcheck
Before this error happens, it looks like the PKCECODEVERIFIER is created succesfully:
[90m[auth][debug]:[0m CREATE_PKCECODEVERIFIER {
"value": "ymKd5vWZdJYvahpKJYSVnCNLvhInp1V7KDQ0oKKVBvg",
"maxAge": 900
}
After the users attempts to login the above error happens and they are redirected to /api/auth/error?error=Configuration With a message that says: "There is a problem with the server configuration. Check the server logs for more information".
auth config;
const KEYCLOAK_CLIENT_ID = 'frontend-standard-flow-app'
const providers = flattenedEnvironments.map((environment) => {
return Keycloak({
id: environment.provider,
clientId: KEYCLOAK_CLIENT_ID,
clientSecret: 'REQUIRED_BY_NEXT_AUTH_BUT_UNUSED',
issuer: `${environment.keycloakURL}/realms/${environment.keycloakRealm}`,
})
})
export const { handlers, auth, signIn, signOut } = NextAuth(() => {
return {
basePath: '/api/auth',
trustHost: true,
secret: process.env.AUTH_SECRET,
providers: providers,
debug: true, // process.env.NEXT_AUTH_DEBUG === 'true',
cookies: {
pkceCodeVerifier: {
name: 'next-auth.pkce.code_verifier',
options: {
httpOnly: true,
sameSite: 'none',
path: '/',
secure: true,
},
},
},
callbacks: {
async session({ session, token }) {
try {
if (token) {
const decodedJWT = parseJwt(token.access_token as string)
// @ts-expect-error - token.error is added in the jwt callback
session.error = token.error
session.user = {
// @ts-expect-error - token.user is added in the jwt callback
...token.user,
roles: decodedJWT.realm_access?.roles,
}
session.token = {
access_token: token.access_token as string,
refresh_token: token.refresh_token as string,
expires_at: token.expires_at as number,
}
}
return session
} catch (error) {
console.error('🛑 session callback ERROR:', error)
return session
}
},
async jwt({ token, account, user, profile, trigger }) {
try {
if (trigger === 'update') {
return await refreshAccessToken(token)
}
if (account) {
const userProfile: User = {
...user,
user_name: profile?.preferred_username as string,
allowed_company_uids: profile?.allowed_company_uids as string,
id: token.sub,
}
// First login, save the `access_token`, `refresh_token`, and other
// details into the JWT token.
// This is returning the enhanced "token"
return {
provider: account.provider,
id_token: account.id_token,
access_token: account.access_token,
expires_at: Math.floor(
Date.now() / 1000 + (account.expires_in as number),
),
refresh_token: account.refresh_token,
user: userProfile,
}
}
// @ts-expect-error - token.expires_at exists
if (Date.now() < token.expires_at * 1000) {
// Subsequent logins, if the `access_token` is still valid, return the JWT
return token
}
// Subsequent logins, if the `access_token` has expired, try to refresh it
if (typeof token.refresh_token !== 'string') {
throw new Error('Missing refresh token')
}
const decodedRefreshToken = parseJwt(token.refresh_token as string)
if (decodedRefreshToken.exp < Date.now() / 1000) {
return { ...token, error: 'REFRESH_TOKEN_EXPIRED' as const }
}
try {
return await refreshAccessToken(token)
} catch (_err) {
// The error property is used to force sign-out if the refresh token is invalid
return { ...token, error: 'REFRESH_ACCESS_TOKEN_ERROR' as const }
}
} catch (error) {
console.error('🛑 jwt callback ERROR:', error)
// @ts-expect-error - can be any error
return { ...token, error: error?.message }
}
},
},
events: {
async signOut(data: { token: JWT } | Record<string, unknown>) {
try {
const APP_CONFIG = await getServerAppConfig()
const logOutUrl = new URL(
`${APP_CONFIG.KEYCLOAK_URL}/realms/${APP_CONFIG.KEYCLOAK_REALM}/protocol/openid-connect/logout`,
)
// @ts-expect-error - token.id_token is added in the jwt callback
logOutUrl.searchParams.set('id_token_hint', data.token.id_token)
await fetch(logOutUrl)
} catch (error) {
console.error('🛑 signOut event ERROR:', error)
}
},
},
}
})
signIn function with idp_hint:
import { cookies } from 'next/headers'
import { signIn } from '@/auth'
import { getServerAppConfig } from '@/common/config/serverConfig'
export async function GET() {
const APP_CONFIG = await getServerAppConfig()
const providerOverride = cookies().get('provider_override')?.value
const { PROVIDER, IDP_HINT } = APP_CONFIG
await signIn(
providerOverride ?? PROVIDER,
undefined,
{ identity_provider: IDP_HINT }
)
return <div />
}
How to reproduce
I created a minimal reproduction, but it requires setting up a 3rd party OIDC system to reproduce the issue, so it's no small task. 😞
- Set up a 3rd party OIDC login through Keycloak, hosted at a 3rd party URL, which takes
easas the idp_hint - Set the correct issuer url for the keycloak instance.
- Deploy the reproduction to production on Vercel (haven't tested other providers).
- Attempt to login from the 3rd party site through keycloak OIDC login.
- The user will seemingly? be successfully logged in from the keycloak side, but the app will redirect them to an error page and say the pkce code_verifier cookie was missing.
I'm replacing a client-side keycloak system with next-auth. Everything besides adding next-auth is the exact same, and my 3rd party OIDC login through keycloak works fine in current production, but broken when I deploy next-auth
Expected behavior
User should be logged in and redirect to /
I patched the next-auth and @auth/core libraries with some more logs. This time i also deleted all of my browser history and cookies before the test.
Here's the full sequence of logs I gathered leading up to the error:
correct idp hint
🔈 ~ next-auth / signIn / authorizationParams: { kc_idp_hint: 'eas', identity_provider: 'eas', idp_hint: 'eas' }
🔈 ~ @auth / Auth / internalRequest.url: URL {
href: 'https://sso.yyy.xxx.com/api/auth/signin/yyy-sso-xxx?kc_idp_hint=eas&identity_provider=eas&idp_hint=eas',
origin: 'https://sso.rogers.colonynetworks.com/',
protocol: 'https:',
username: '',
password: '',
host: 'sso.yyy.xxx.com',
hostname: 'sso.yyy.xxx.com',
port: '',
pathname: '/api/auth/signin/yyy-sso-xxx',
search: '?kc_idp_hint=eas&identity_provider=eas&idp_hint=eas',
searchParams: URLSearchParams { 'kc_idp_hint' => 'eas', 'identity_provider' => 'eas', 'idp_hint' => 'eas' },
hash: ''
}
🔈 ~ @auth / Auth / internalRequest.url.searchParams: URLSearchParams { 'kc_idp_hint' => 'eas', 'identity_provider' => 'eas', 'idp_hint' => 'eas' }
correct keycloak URL
🔈 ~ @auth / actions / signIn / signInUrl: https://sso.yyy.xxx.com/api/auth/signin
correct provider
🔈 ~ @auth / actions / getAuthorizationUrl / provider: {
id: 'yyy-sso-xxx',
name: 'Keycloak',
type: 'oidc',
style: { brandColor: '#428bca' },
clientId: 'frontend-standard-flow-app',
clientSecret: 'REQUIRED_BY_NEXT_AUTH_BUT_UNUSED',
issuer: 'https://sso.xxx.com/auth/realms/yyy',
signinUrl: 'https://sso.yyy.xxx.com/api/auth/signin/yyy-sso-xxx',
callbackUrl: 'https://sso.yyy.xxx.com/api/auth/callback/yyy-sso-xxx',
redirectProxyUrl: undefined,
wellKnown: 'https://sso.xxx.com/auth/realms/yyy/.well-known/openid-configuration',
authorization: undefined,
token: undefined,
checks: [ 'pkce' ],
userinfo: undefined,
profile: [Function: re],
account: [Function: rt]
}
🔈 ~ @auth / actions / getAuthorizationUrl / url: undefined
🔈 ~ @auth / actions / getAuthorizationUrl / url.searchParams: URLSearchParams {}
🔈 ~ @auth / actions / getAuthorizationUrl / redirect_uri: https://sso.yyy.xxx.com/api/auth/callback/yyy-sso-xxx
✅ ~ @auth / actions / getAuthorizationUrl / pkce check is happening: true
🔐 ~ @auth / actions / getAuthorizationUrl / checks.pkce.create {
debug: true,
pages: {},
theme: { colorScheme: 'auto', logo: '', brandColor: '', buttonText: '' },
basePath: '/api/auth',
trustHost: true,
secret: 'brA/qMwPxzJiy13rkpSWtJQ3HIb+bh+yCCl3FH5C8hU=',
providers: [ all of my providers were listed here ]
seems to create the PKCE code verifier correctly
[90m[auth][debug]:[0m CREATE_PKCECODEVERIFIER {
"value": "0xVG98aVaKu-F-46k0oqvDBuYlSZIhMSwb3WRZXkVzA",
"maxAge": 900
}
pkce has a vale in getAuthorizationUrl, but it doesn't match the one generated above?
🔐 ~ @auth / actions / getAuthorizationUrl / pkce value MCROqQtkYxoQoxKNmm61tce9CdyqhDjybhaocyXrwgI
pkce cookie exists in getAuthorizationUrl, but the value is different yet again?
🔐 ~ @auth / actions / getAuthorizationUrl / pkce cookie {
name: 'next-auth.pkce.code_verifier',
value: 'eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoienJlMGlVeGZZcnpVSGI1Z3dzUlRONDM3MHR6bk9jVkFQZWt4a3JQMmZWa1ZsVGtZUENWMG4ydE1hVmozcXpRWHhVbl80TERDdjZSeXlVWmV3NEZYRncifQ..ZhAOB0nor1lRcMFbEuTYVQ.seIqoaqyIOk1svcTyvvgVxJvv9gUCT8txRPcDv14Ea7x99hEpBSPmD0seui1m_zl4w52Tr5WhItYq-lvFF62A9dukHLZfyq78BbK9QMWBTaL48mcm_f4feDFPS-oXLRTRam7KnyIeBfhJhFjMubP5h9YtMuGmYvGK1jR8irhf76dxrkWp9aN4nSlwQ1_lDsX.r5IzWQsnDn66qsKlwJx6kdkvQjdunjrn6EsOnZTBXvI',
options: {
httpOnly: true,
sameSite: 'none',
path: '/',
secure: true,
maxAge: 900,
expires: 2024-08-20T21:23:28.056Z
}
}
in the code challenge, the value matches one of the previous pkce values
🔐 ~ @auth / actions / getAuthorizationUrl / code_challenge MCROqQtkYxoQoxKNmm61tce9CdyqhDjybhaocyXrwgI
🔈 ~ @auth / actions / getAuthorizationUrl / authParams: URLSearchParams {
'response_type' => 'code',
'client_id' => 'frontend-standard-flow-app',
'redirect_uri' => 'https://sso.yyy.xxx.com/api/auth/callback/yyy-sso-xxx',
'kc_idp_hint' => 'eas',
'identity_provider' => 'eas',
'idp_hint' => 'eas',
'code_challenge' => 'MCROqQtkYxoQoxKNmm61tce9CdyqhDjybhaocyXrwgI',
'code_challenge_method' => 'S256' }
authorization url is ready with pkce cookie
[90m[auth][debug]:[0m authorization url is ready {
"url": "https://sso.xxx.com/auth/realms/yyy/protocol/openid-connect/auth?response_type=code&client_id=frontend-standard-flow-app&redirect_uri=https%3A%2F%2Fsso.yyy.xxx.com%2Fapi%2Fauth%2Fcallback%2Frogers-sso-colonynetworks&kc_idp_hint=eas&identity_provider=eas&idp_hint=eas&code_challenge=MCROqQtkYxoQoxKNmm61tce9CdyqhDjybhaocyXrwgI&code_challenge_method=S256&scope=openid+profile+email",
"cookies": [
{
"name": "next-auth.pkce.code_verifier",
"value": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoienJlMGlVeGZZcnpVSGI1Z3dzUlRONDM3MHR6bk9jVkFQZWt4a3JQMmZWa1ZsVGtZUENWMG4ydE1hVmozcXpRWHhVbl80TERDdjZSeXlVWmV3NEZYRncifQ..ZhAOB0nor1lRcMFbEuTYVQ.seIqoaqyIOk1svcTyvvgVxJvv9gUCT8txRPcDv14Ea7x99hEpBSPmD0seui1m_zl4w52Tr5WhItYq-lvFF62A9dukHLZfyq78BbK9QMWBTaL48mcm_f4feDFPS-oXLRTRam7KnyIeBfhJhFjMubP5h9YtMuGmYvGK1jR8irhf76dxrkWp9aN4nSlwQ1_lDsX.r5IzWQsnDn66qsKlwJx6kdkvQjdunjrn6EsOnZTBXvI",
"options": {
"httpOnly": true,
"sameSite": "none",
"path": "/",
"secure": true,
"maxAge": 900,
"expires": "2024-08-20T21:23:28.056Z"
}
}
],
"provider": {
"id": "yyy-sso-xxx",
"name": "Keycloak",
"type": "oidc",
"style": {
"brandColor": "#428bca"
},
"clientId": "frontend-standard-flow-app",
"clientSecret": "REQUIRED_BY_NEXT_AUTH_BUT_UNUSED",
"issuer": "https://sso.xxx.com/auth/realms/yyy",
"signinUrl": "https://sso.yyy.xxx.com/api/auth/signin/yyy-sso-xxx",
"callbackUrl": "https://sso.yyy.xxx.com/api/auth/callback/yyy-sso-xxx",
"wellKnown": "https://sso.xxx.com/auth/realms/rogers/.well-known/openid-configuration",
"checks": [
"pkce"
]
}
}
signIn url seems correct
🔈 ~ next-auth / signIn / url: https://sso.yyy.xxx.com/api/auth/signin/yyy-sso-xxx?kc_idp_hint=eas&identity_provider=eas&idp_hint=eas
callbackUrl is the 3rd party oidc login website, which seems right.
🔈 ~ next-auth / signIn / body: URLSearchParams { 'callbackUrl' => 'https://zzz.yyy.com/' }
@auth/core internalRequest.url
🔈 ~ @auth / Auth / internalRequest.url: URL {
href: 'https://sso.yyy.xxx.com/api/auth/callback/yyy-sso-xxx?session_state=1bb44980-30db-4859-8ea1-e1b0d8b097fb&iss=https%3A%2F%2Fsso.xxx.com%2Fauth%2Frealms%2Fyyy&code=5ef5cc45-d8bc-49d5-899f-5d07dcd3fd07.1bb44980-30db-4859-8ea1-e1b0d8b097fb.445c02ab-27e4-4dde-93ca-31c6b8f3422f',
origin: 'https://sso.yyy.xxx.com/',
protocol: 'https:',
username: '',
password: '',
host: 'sso.yyy.xxx.com',
hostname: 'sso.yyy.xxx.com',
port: '',
pathname: '/api/auth/callback/yyy-sso-colonynetworks',
search: '?session_state=1bb44980-30db-4859-8ea1-e1b0d8b097fb&iss=https%3A%2F%2Fsso.xxx.com%2Fauth%2Frealms%2Fyyy&code=5ef5cc45-d8bc-49d5-899f-5d07dcd3fd07.1bb44980-30db-4859-8ea1-e1b0d8b097fb.445c02ab-27e4-4dde-93ca-31c6b8f3422f',
searchParams: URLSearchParams {
'session_state' => '1bb44980-30db-4859-8ea1-e1b0d8b097fb',
'iss' => 'https://sso.xxx.com/auth/realms/yyy',
'code' => '5ef5cc45-d8bc-49d5-899f-5d07dcd3fd07.1bb44980-30db-4859-8ea1-e1b0d8b097fb.445c02ab-27e4-4dde-93ca-31c6b8f3422f' },
hash: ''
}
in @auth/core oauth checks file, the pkce cookies is empty 😞, this is the cause of the error, but the problem is that it shouldn't be empty.
🛡️ ~ @auth / oauth / checks / pkce / cookies: {}
resCookies are also empty
🛡️ ~ @auth / oauth / checks / pkce / resCookies: []
cookie options
🛡️ ~ @auth / oauth / checks / pkce / options.cookies: {
sessionToken: {
name: '__Secure-authjs.session-token',
options: { httpOnly: true, sameSite: 'lax', path: '/', secure: true }
},
callbackUrl: {
name: '__Secure-authjs.callback-url',
options: { httpOnly: true, sameSite: 'lax', path: '/', secure: true }
},
csrfToken: {
name: '__Host-authjs.csrf-token',
options: { httpOnly: true, sameSite: 'lax', path: '/', secure: true }
},
pkceCodeVerifier: {
name: 'next-auth.pkce.code_verifier',
options: {
httpOnly: true,
sameSite: 'none',
path: '/',
secure: true,
maxAge: 900
}
},
state: {
name: '__Secure-authjs.state',
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: true,
maxAge: 900
}
},
nonce: {
name: '__Secure-authjs.nonce',
options: { httpOnly: true, sameSite: 'lax', path: '/', secure: true }
},
webauthnChallenge: {
name: '__Secure-authjs.challenge',
options: {
httpOnly: true,
sameSite: 'lax',
path: '/',
secure: true,
maxAge: 900
}
}
}
the pkce codeVerifier is undefined which is what causes the final error.
🛡️ ~ @auth / oauth / checks / pkce / codeVerifier: undefined
[31m[auth][error][0m InvalidCheck: PKCE code_verifier cookie was missing.. Read more at [https://errors.authjs.dev#invalidcheck](https://errors.authjs.dev/#invalidcheck)
Interestingly, and possibly another bug?
If I try the same login in incognito I get a different error:
[31m[auth][error][0m UnknownAction: Unsupported action. Read more at [https://errors.authjs.dev#unknownaction](https://errors.authjs.dev/#unknownaction)
I'm not writing or calling any actions myself besides the signIn function from next-auth. So I'm not sure how/why that's happening. But only happens in incognito mode.
Today I attempted to downgrade to next-auth@4 to see if that worked, or at least provided more information.
Downgrading to next-auth@4 did resolve the PKCE code_verifier cookie was missing.. error. However it's still not functional with the oidc login through a 3rd party.
New behavior:
- ✅ User logs in through 3rd party via keycloak oidc
- ✅ User gets redirected to app
- ✅ App redirects user to
/auth/signin(a route where I call thesignInfunction client-side from next-auth) - 🛑
next-authredirects the user to the keycloak login page. (The user is already logged in and have an active session with keycloak for this realm, the user should never be redirected to the login page, and the users here don't even have a direct login for the keycloak page)
@balazsorban44 Any idea why this happens?
@MarkLyck in the browser, do you see the cookies being set successfully before the redirection to Keycloak? (Using next-auth@beta)
@ThangHuuVu Thank you for the response! And sorry the reply is a bit late, we can only deploy on Tuesdays and Thursdays to test this.
Here are the browser cookies that get set for me with next-auth@beta (v5)
This screenshot is taken from the /api/auth/error?error=Configuration route after the PKCE missing cookie error is shown.
@ThangHuuVu @balazsorban44 Hmmm I believe we might have figured out the reason for this.
For the affected clients, we have an HTTP proxy that opens a new HTTP connection to Vercel but still preserving the URL hostname so Vercel knows who it is. This is required due to their custom SSL certificates.
So when next-auth used the VERCEL domain name ENV variable, it's actually using the wrong domain name for the authentication.
Sadly we cannot use the AUTH_URL environment variable either, since we have multi-tenancy and different clients have different domains all pointing to the same Vercel deployment.
Is there any way to dynamically set the auth URL? It would be easy if we could just set it in the config. But I didn't see this in the docs.
I'm going to attempt dynamically setting the redirectProxyUrl to the actual hostname, instead of the domain Vercel thinks the user is on with our next deployment.
@ThangHuuVu @balazsorban44 The above attempt failed.
I tried both setting the redirectProxyUrl to the real domain, and setting redirectTo to the https://realdomain.com/api/auth
But the user is still getting redirected back to the Vercel domain after it authenticates with Keycloak 😞
Found a thread with a similar looking issue: https://github.com/nextauthjs/next-auth/issues/10928
I tried implementing the suggest workaround, and that does make next-auth redirect to the correct URL. But the URLs used in the internal requests within next-auth, including getAuthorizationUrl which I think is used for the PKCE code verifier cookie, is still the incorrect VERCEL domain URL.
// In src/app/api/[...nextauth]/route.ts
import { handlers } from "@/auth"
import { NextRequest } from "next/server"
const reqWithTrustedOrigin = (req: NextRequest): NextRequest => {
if (process.env.AUTH_TRUST_HOST !== 'true') return req
const proto = req.headers.get('x-forwarded-proto')
const host = req.headers.get('x-forwarded-host')
if (!proto || !host) {
console.warn("Missing x-forwarded-proto or x-forwarded-host headers.")
return req
}
const envOrigin = `${proto}://${host}`
const { href, origin } = req.nextUrl
return new NextRequest(href.replace(origin, envOrigin), req)
}
export const GET = (req: NextRequest) => {
return handlers.GET(reqWithTrustedOrigin(req))
}
export const POST = (req: NextRequest) => {
return handlers.POST(reqWithTrustedOrigin(req))
}
So while the URLs are not correct in the browser, the PKCE code verifier cookie missing error still remains.
@ThangHuuVu @balazsorban44 any other ideas we can try to get this working with next-auth? I'm pretty sure the issue is that next-auth uses the wrong URL based on the Vercel environment variable, but I don't see any way to dynamically override this for multi tenancy sites.
I managed to do something similar:
- I have an application that is configured using environment variables
- I am deployed to cloudflare pages, using their preview branches/deployments functionality
To get the auth redirect working, I removed NEXTAUTH_URL etc and explicitly set a different one, PRODUCTION_URL to the root URL of the "stable" production deployment. This URL is configured as an allowed callback URL in the OIDC provider (cognito, in my case).
I set redirectProxyUrl on all deployments (master/preview):
redirectProxyUrl: process.env.PRODUCTION_URL + '/api/auth',
And then as part of the configuration:
providers: [
{
id: `cognito`,
name: `cognito`,
type: 'oidc',
clientId: COGNITO_CLIENT_ID,
clientSecret: COGNITO_CLIENT_SECRET,
issuer: COGNITO_ISSUER,
authorization: {
url: `${COGNITO_DOMAIN}/oauth2/authorize`,
params: {
response_type: 'code',
client_id: COGNITO_CLIENT_ID,
// NOTE used here
redirect_uri: `${process.env.PRODUCTION_URL}/api/auth/callback/cognito`,
},
},
token: {
url: `${COGNITO_DOMAIN}/oauth2/token`,
},
userinfo: {
url: `${COGNITO_DOMAIN}/oauth2/userInfo`,
},
This appears to work just fine when using signIn('cognito') in the client - no additional params required.
The key difference from what i understood the documentation to infer was that all deployments need the redirectProxyUrl set, or it is trying to "consume" your login request on the main deployment instead of forwarding it to the callbackUrl provided in the state parameter.
I understand this is not exactly your use case but i do hope it helps.
@jack828 Thanks for the comment Jack. We did try using the redirectProxyUrl but it didn't work in our case. The URL which does the handshake with keycloak is still using the incorrect Vercel domain instead of the domain the user is authenticated for and actually viewing the website from.
We're still stuck on this. I've pretty much given up trying to get next-auth working, and attempting to get it working with the keycloak-js library and handling the server-side ourselves.
cc @ThangHuuVu @balazsorban44 Would still really love some input here if you have time 😞 if only it was possible to override the URL next-auth uses in the config or provider config, I believe it would work.
@jack828 Thanks for the comment Jack. We did try using the
redirectProxyUrlbut it didn't work in our case. The URL which does the handshake with keycloak is still using the incorrect Vercel domain instead of the domain the user is authenticated for and actually viewing the website from.We're still stuck on this. I've pretty much given up trying to get next-auth working, and attempting to get it working with the
keycloak-jslibrary and handling the server-side ourselves.cc @ThangHuuVu @balazsorban44 Would still really love some input here if you have time 😞 if only it was possible to override the URL next-auth uses in the config or provider config, I believe it would work.
@MarkLyck If you come up with an app router solution using keycloak-js, I'd appreciate a gist or something !! I'm also stuck on this.
@dclark27 We're still struggling with this as well. Likewise, if you find a solution please post it. 🙏
We did a test with keycloak-js today, but that also failed, and redirected the user to the keycloak login screen when they should already be authenticated via OIDC.
I've been working on this for a few weeks as well, if anybody has made any progress with either next-auth or keycloak-js, I would love to learn more about it.
I'm on [email protected] and I am seeing the same error, occassionally! Logging in and logging out works, though sometimes (and I can't really tell what triggers this) we are observing the same error
[auth][error] InvalidCheck: pkceCodeVerifier value could not be parsed. Read more at https://errors.authjs.dev#invalidcheck
at i4 (/app/apps/web-v2/.next/server/chunks/991.js:368:26741)
at Object.use (/app/apps/web-v2/.next/server/chunks/991.js:368:27149)
at os (/app/apps/web-v2/.next/server/chunks/991.js:368:33213)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async ob (/app/apps/web-v2/.next/server/chunks/991.js:368:40282)
at async oP (/app/apps/web-v2/.next/server/chunks/991.js:368:51845)
at async oC (/app/apps/web-v2/.next/server/chunks/991.js:368:56536)
at async te.do (/app/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:18:17826)
at async te.handle (/app/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:18:22492)
at async doRender (/app/node_modules/next/dist/server/base-server.js:1455:42)
This is our auth.ts file
export const { auth, handlers, signIn, signOut } = NextAuth({
trustHost: true,
providers: [
KeycloakProvider({
clientId: process.env.KEYCLOAK_CLIENT_ID!,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
issuer: process.env.KEYCLOAK_ISSUER!,
}),
],
pages: {
signIn: '/auth/signin',
signOut: '/auth/signout',
},
session: {
strategy: 'jwt',
maxAge: 60 * 5, // every 5 minutes we need to refresh the token
updateAge: 60 * 4,
},
callbacks: {
async jwt({ token, account }) {
token = await tryRefreshToken(token, account);
return { ...token };
},
async session({ session, token }) {
session.accessToken = token.accessToken;
session.error = token.error;
session.idToken = token.idToken;
return session;
},
async redirect({ url, baseUrl }) {
return url.startsWith(baseUrl) ? url : baseUrl + '/app/dashboard';
},
async authorized({ request, auth }) {
if (!auth?.user) {
return NextResponse.redirect(`${request.nextUrl.origin}/auth/signin`);
}
return true;
},
},
events: {
signIn({ account }) {
const accessToken = account?.access_token;
const userApi = getApi(UserApi, accessToken);
return userApi.userControllerEnsureUser();
},
},
});
I did not include the tryRefreshToken for convenience, highly doubt that it is related to that.
stuck in the same boat with 5.0.0-beta.18, this error pops up every once and a while in the logs with no specific reproduction steps sadly.
[auth][error] InvalidCheck: PKCE code_verifier cookie was missing.. Read more at https://errors.authjs.dev#invalidcheck
at Object.use (/app/apps/cloud-portal/.next/server/chunks/350.js:393:22651)
at t2 (/app/apps/cloud-portal/.next/server/chunks/350.js:393:27019)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async ri (/app/apps/cloud-portal/.next/server/chunks/350.js:393:34270)
at async rh (/app/apps/cloud-portal/.next/server/chunks/350.js:393:45642)
at async rw (/app/apps/cloud-portal/.next/server/chunks/350.js:393:50554)
at async /app/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:34666
at async eS.execute (/app/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:25813)
at async eS.handle (/app/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/compiled/next-server/app-route.runtime.prod.js:6:35920)
at async doRender (/app/node_modules/.pnpm/[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/next/dist/server/base-server.js:1377:42)
and this is our auth.ts:
import NextAuth from 'next-auth';
import 'next-auth/jwt';
import { JWT } from 'next-auth/jwt';
import Keycloak from 'next-auth/providers/keycloak';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Keycloak({
clientId: global.process?.env.ID,
clientSecret: global.process?.env.SECRET,
issuer: global.process?.env.ISSUER,
authorization: {
params: {
scope: 'openid profile email offline_access',
},
},
}),
],
callbacks: {
async jwt({ token, account }) {
if (account) {
// First login, save the `access_token`, `refresh_token`, and other
// details into the JWT
return {
...token,
id_token: account.id_token,
access_token: account.access_token,
access_token_expires: account.expires_at,
refresh_token: account.refresh_token,
};
} else if (Date.now() < (token?.access_token_expires ?? 0) * 1000) {
// Subsequent logins, if the `access_token` is still valid, return the JWT
return token;
} else {
// Subsequent logins, if the `access_token` has expired, try to refresh it
if (!token.refresh_token) throw new Error('Missing refresh token');
try {
// The `token_endpoint` can be found in the provider's documentation. Or if they support OIDC,
// at their `/.well-known/openid-configuration` endpoint.
const response = await fetch(
`${global.process.env.ISSUER}/protocol/openid-connect/token`,
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: global.process.env.ID as string,
client_secret: global.process.env.SECRET as string,
grant_type: 'refresh_token',
refresh_token: token.refresh_token as string,
}),
method: 'POST',
},
);
const responseTokens = await response.json();
if (!response.ok) throw responseTokens;
return {
// Keep the previous token properties
...token,
access_token: responseTokens.access_token,
access_token_expires: Math.floor(
Date.now() / 1000 + (responseTokens.expires_in as number),
),
// Fall back to old refresh token, but note that
// many providers may only allow using a refresh token once.
refresh_token: responseTokens.refresh_token ?? token.refresh_token,
};
} catch (error) {
console.error('Error refreshing access token', error);
// The error property can be used client-side to handle the refresh token error
return { ...token, error: 'RefreshAccessTokenError' as const };
}
}
},
async session({ session, token }) {
session.access_token = token.access_token;
session.error = token.error;
return session;
},
},
events: {
async signOut(session) {
const idToken = (session as { token: JWT }).token.id_token;
const logoutURL = new URL(
`${global.process.env.ISSUER}/protocol/openid-connect/logout?id_token_hint=${idToken}&post_logout_redirect_uri=${global.process.env.AUTH_URL}`,
);
await fetch(logoutURL).catch((err) => {
console.error('SIGNOUT ERROR: ', err);
});
},
},
});
declare module 'next-auth/jwt' {
interface JWT {
id_token?: string;
access_token?: string;
access_token_expires?: number;
refresh_token?: string;
error?: 'RefreshAccessTokenError';
}
}
declare module 'next-auth' {
interface Session {
error?: 'RefreshAccessTokenError';
access_token?: string;
}
}
also we have been seeing another issuer occur that we are unsure if it is related where a user may randomly get sent to the Server Error page. Aside from that, is this PKCE error perhaps related to the keycloak provider?
facing this same error:
[31m[auth][error] [0m InvalidCheck: pkceCodeVerifier value could not be parsed. Read more at https://errors.authjs.dev#invalidcheck
everything works fine on dekstop/laptop but on mobile it sometime gives this error and on mobile while desktop mode enabled in chrome it does not give the error
it seems this error is related to __Secure-authjs.pkce.code_verifier not been set in cookies while signing google