Next.js 14.1.4 App Router HandleLogin Override to Gmail (google-oauth2) Social, State Mismatch

Hi, my Auth0 app is on Next.js 14.1.4 App Router. I am looking to create a login / signup button that takes my users directly to a social login. In my case the Google / Gmail (google-oauth2) screen, while bypassing the Auth0 login prompt, eliminating an extra step for Gmail users. I already have both (passwordless) email & Google authentication working 100% without any issues. Universal Login is configured, with the Identifier First login flow for the passwordless experience.

My application is already established for a few years since the Next.js 13 days, following the steps from Auth0’s docs below. Can’t upgrade from 14.1.4 to 14.2.* due to unrelated issues here which breaks my & other users’ code: Incompatibility with next ^14.2.4 · Issue #1776 · auth0/nextjs-auth0 · GitHub. However this thread is on a different subject.

My app router route is defined under app/api/auth/[auth0]/route.js as:

import { handleAuth } from '@auth0/nextjs-auth0';

export const GET = handleAuth();

As of now, the login feature is wrapped around a button as the below:
<a href="/api/auth/login"><button>Login</button></a>

Attempt #1 out of #6

My first attempt was appending this url with ?connection=google-oauth2, which still takes me directly to the Auth0 login page instead of directly to Gmail.

Attempt #2 out of #6

This involves creating a new route:
api/auth/login-google

Within this route I have the below:

import { handleLogin } from '@auth0/nextjs-auth0';

export const GET = handleLogin({
    authorizationParams: {
      connection: 'google-oauth2'
    }
});

Great, this works if I npm run dev my app, and I’m able to sign-in with Google & return to my app logged in. However this results in build errors:

src/app/api/auth/login-google/route.js
Type error: Route "src/app/api/auth/login-google/route.js" has an invalid "GET" export:
  Type "NextRequest | NextApiRequest" is not a valid type for the function's first argument.
    Expected "Request | NextRequest", got "NextRequest | NextApiRequest".
      Expected "Request | NextRequest", got "NextApiRequest".

Attempt #3 out of #6

I now modify my login-google/route.js with:

import { handleLogin } from '@auth0/nextjs-auth0';

export const dynamic = "force-dynamic";

export async function GET(request) {
  return handleLogin(request, {
    authorizationParams: {
      connection: 'google-oauth2'
    }
  });
}

This now directs me back to the Auth0 login page, instead of directly to Gmail. Not what I want.

Attempt #4 out of #6

The suggestion is to manualy build the login with query parameters:

export const dynamic = "force-dynamic";

export async function GET() {
  const auth0BaseUrl = process.env.AUTH0_BASE_URL;
  const auth0Callback = process.env.AUTH0_CALLBACK
  const auth0IssuerBaseUrl = process.env.AUTH0_ISSUER_BASE_URL;
  const clientId = process.env.AUTH0_CLIENT_ID;
  const redirectUri = `${auth0BaseUrl}${auth0Callback}`;

  const loginUrl = `${auth0IssuerBaseUrl}/authorize?` +
    new URLSearchParams({
      client_id: clientId,
      redirect_uri: redirectUri,
      response_type: 'code',
      connection: 'google-oauth2'
    });

  return new Response(null, {
    status: 302,
    headers: { Location: loginUrl.toString() },
  });
}

I can build my project without errors, and clicking my button takes me directly to my Google login, however, after logging in I receive an HTTP ERROR 400 at my api/auth0/callback route.

Attempt #5 out of #6

At this point I’m maybe doing to much and perhaps I should’ve asked here before more attempts, but now the suggestion is to redo my api/auth/[auth0]/route.js to properly catch the callback route instead of returning HTTP ERROR 400.

I change from:

import { handleAuth } from '@auth0/nextjs-auth0';

export const GET = handleAuth();

To:

import { handleAuth, handleLogin, handleLogout, handleCallback, handleProfile } from '@auth0/nextjs-auth0';

export const dynamic = 'force-dynamic';

export async function GET(request) {
  const url = new URL(request.url);
  const pathname = url.pathname;

  try {
    if (pathname.endsWith('/login')) {
      return await handleLogin(request, {
        authorizationParams: {
          connection: 'google-oauth2',
          scope: 'openid profile email',
        },
      });

    } else if (pathname.endsWith('/logout')) {
      return await handleLogout(request);

    } else if (pathname.endsWith('/callback')) {
      return await handleCallback(request);

    } else if (pathname.endsWith('/me')) {
      return await handleProfile(request);

    } else {
      return handleAuth()(request);
    }
  } catch (error) {
    return new Response(JSON.stringify({ error: error.message }), {
      status: error.status || 400,
      headers: { 'Content-Type': 'application/json' },
    });
  }
}

Ok, I can build without issues and login, but then the callback route returns the error:

{"error":"Callback handler failed. CAUSE: Missing state cookie from login request (check login URL, callback URL and cookie config)."}

Attempt #6 out of #6

I then rewrite my app/api/auth/login-google/route.js endpoint with:

import { randomBytes } from 'crypto';

export const dynamic = "force-dynamic";

export async function GET() {
  const auth0BaseUrl = process.env.AUTH0_BASE_URL;
  const auth0Callback = process.env.AUTH0_CALLBACK;
  const auth0IssuerBaseUrl = process.env.AUTH0_ISSUER_BASE_URL;
  const clientId = process.env.AUTH0_CLIENT_ID;
  const redirectUri = `${auth0BaseUrl}${auth0Callback}`;

  const desiredLength = 64;

  const state = randomBytes(Math.ceil(desiredLength / 2)).toString('hex').slice(0, desiredLength);

  const loginUrl = `${auth0IssuerBaseUrl}/authorize?` +
    new URLSearchParams({
      client_id: clientId,
      redirect_uri: redirectUri,
      response_type: 'code',
      connection: 'google-oauth2',
      state: state,
    }).toString();

  return new Response(null, {
    status: 302,
    headers: {
      Location: loginUrl,
      'Set-Cookie': `auth_state=${state}; HttpOnly; Secure; Path=/; SameSite=Lax`,
    },
  });
}

Returns the error:

{"error":"Callback handler failed. CAUSE: state mismatch, expected ********************************************************, got: ********************************************************"}

These are 2 different state codes btw. This is where I give up, perhaps there’s an Auth0 recommended way of generating the state within my login-google route, or maybe there’s even simpler overall code.

Any help would be greatly appreciated, thanks!

The Github for @auth0/nextjs-auth states there’s a way to override the login handler, & would’ve saved all my troubles, yet only has an example for Page Router & doesn’t work in App Router. Perhaps it’s a user error, how would you properly translate this to App Router?:

Example

Override the login handler

import { handleAuth, handleLogin } from '@auth0/nextjs-auth0';

export default handleAuth({
  login: async (req, res) => {
    try {
      await handleLogin(req, res, {
        authorizationParams: { connection: 'github' }
      });
    } catch (error) {
      console.error(error);
    }
  }
});