Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PKCE code_verifier cookie was missing.. on keycloak OIDC login #11641

Open
MarkLyck opened this issue Aug 20, 2024 · 13 comments
Open

PKCE code_verifier cookie was missing.. on keycloak OIDC login #11641

MarkLyck opened this issue Aug 20, 2024 · 13 comments
Labels
bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.

Comments

@MarkLyck
Copy link

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 next-auth@5.0.0-beta.18, 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 eas as 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 /

@MarkLyck MarkLyck added bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime. labels Aug 20, 2024
@MarkLyck
Copy link
Author

MarkLyck commented Aug 21, 2024

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)

@MarkLyck
Copy link
Author

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.

@MarkLyck
Copy link
Author

MarkLyck commented Aug 23, 2024

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 the signIn function client-side from next-auth)
  • 🛑 next-auth redirects 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?

@ThangHuuVu
Copy link
Member

@MarkLyck in the browser, do you see the cookies being set successfully before the redirection to Keycloak? (Using next-auth@beta)

@MarkLyck
Copy link
Author

MarkLyck commented Aug 28, 2024

@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)

Screenshot 2024-08-27 at 20 30 26

This screenshot is taken from the /api/auth/error?error=Configuration route after the PKCE missing cookie error is shown.

@MarkLyck
Copy link
Author

MarkLyck commented Aug 28, 2024

@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.

@MarkLyck
Copy link
Author

MarkLyck commented Aug 28, 2024

@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 😞

@MarkLyck
Copy link
Author

Found a thread with a similar looking issue: #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.

@jack828
Copy link

jack828 commented Sep 3, 2024

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.

@MarkLyck
Copy link
Author

MarkLyck commented Sep 13, 2024

@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.

@dclark27
Copy link

@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.

@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.

@MarkLyck
Copy link
Author

@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.

@JHansen2000
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working triage Unseen or unconfirmed by a maintainer yet. Provide extra information in the meantime.
Projects
None yet
Development

No branches or pull requests

5 participants