auth0 / nextjs-auth0

Next.js SDK for signing in with Auth0
MIT License
2.07k stars 389 forks source link

HandleLogin Override Login Handler Not working In Next.js 14.1.4 App Router #1796

Open ymonye opened 1 week ago

ymonye commented 1 week ago

Checklist

Description

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

The official docs state this is explicitly possible, yet I cannot get it to work on App Router. https://auth0.github.io/nextjs-auth0/types/handlers_login.HandleLogin.html

From the docs:

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);
    }
  }
});

Can't upgrade Next.js from 14.1.4 to 14.2.* due to unrelated issues here which breaks my & other users' code: https://github.com/auth0/nextjs-auth0/issues/1776. However this issue is on a different subject.

Reproduction

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 7

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 7

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

Within this route I have the HandleLogin export with the override explicitly stated here: https://auth0.github.io/nextjs-auth0/types/handlers_login.HandleLogin.html

I am attempting to convert the login override example from Page Router to App Router, yet this returns HTTP ERROR 500. Perhaps this is where things are breaking & could easily be solved instead of progressing to attempts 3 - 7.

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

export const dynamic = 'force-dynamic';

export const GET = handleAuth({
  login: handleLogin((req) => {
    return {
      authorizationParams: { connection: 'google-oauth2' }
    };
  }),
});

Attempt 3 out of 7

Remove handleAuth

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 4 out of 7

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 5 out of 7

The suggestion is to manually 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 6 out of 7

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 7 out of 7

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!

Additional context

No response

nextjs-auth0 version

3.5.0

Next.js version

14.1.4

Node.js version

20.15.1

ymonye commented 1 week ago

cc @guabu