arrow

Guide to Overriding Auth0 Callback for Next.js Authentication with Error Handling

Aashit Kothari

Aashit Kothari

|
Jul 26, 2024
|
book

8 mins read

cover-image

Auth0 provides an SDK @auth0/nextjs-auth0, to effectively implement authentication in Next.js SPA. This SDK internally handles token acquisition but also provides ability to extend/override auth0 callback to customise the user session and post authentication behaviour.

Prerequisites

You'll need to configure your Next.js application with your auth0 service to enable Single-SignOn with auth0. Refer this interactive quick-start guide to configure the setup. Use this auth0-next.js sample project for quick implementation and demo purposes.

Once the setup is complete, you should be able to login to your app using auth0.

What is auth0 callback? What is a callback handler?

When authenticating any user via auth0, on successful authentication auth0 redirects users to a specified redirect URI also known as callback route of the app. When directly using auth0 endpoints we need to create user session from the token endpoint.
But while using Auth0 SDK for Next.js, after successful authentication, it creates user session and hits the callback route /api/auth/callback which is an API route. The callback route then redirects user back to the app's home screen or a specified post login redirectUri. In case of customising/overriding these API routes from Auth0 SDK, Next.js provides something called route handlers, and one such route handler available with this SDK is handleAuth, where we can add different handler functions for auth0 authentication. The handler function used to override the callback route is called callback handler.

Note that handleAuth runs on server-side in Next.js, making its handlers like callback more secure to carry out sensitive operations.

Why do we need to override Auth0 callback?

Though we can extend callback handler for customizations like redirectUri, authorizationParams, afterCallback, etc. there are few scenarios where we need to override callback handler completely. Like,

  • Redirecting authorized users to different routes based on session data.

  • Restricting unauthorised users from accessing app.

  • Graceful error handling to surface error messages from auth0.

All these scenarios are covered later in this article, but before that we will see how to override the auth0 callback.

How to override Auth0 callback for Next.js?

To override auth0 callback, first of all we need to implement auth0's route handler handleAuth. This depends on which router we are using,

Let's see how to implement handleAuth to override auth0 callback, when using Pages Router

// pages/api/auth/[auth0].js import { handleAuth, handleCallback } from "@auth0/nextjs-auth0"; export default handleAuth({ callback: async (req, res) => { try { const response = await handleCallback(req, res, options); return response; } catch (error) { console.error(error); } } });

In the above code-snippet,
req stands for NextApiRequest, res stands for NextApiResponse and options stands for CallbackOptions which is an optional parameter.

When using App Router, here is how to implement handleAuth to override auth0 callback,

// app/api/auth/[auth0]/route.js import { handleAuth, handleCallback } from "@auth0/nextjs-auth0"; export const GET = handleAuth({ callback: async (req, ctx) => { try { const response = await handleCallback(req, ctx, options); return response; } catch (error) { console.error(error); } } });

In the above code-snippet,
req is NextRequest, ctx is AppRouteHandlerFnContext and options stands for CallbackOptions which is an optional parameter.

In both the examples above, it invokes handleCallback which is inbuilt callback handler of Auth0 SDK for Next.js. If we do not override auth0 callback, it still runs handleCallback by default to create user session after successful authentication.

In our further examples we'll use App Router, but same approach can also be implemented using Pages Router as well.

Now that we know how to override auth0 callback, one-by-one let's take a look at implementing all the scenarios mentioned earlier.

Redirecting authorized users to different routes based on session data

As we know, in an application there can be different users with access to different area of the app. For example, admin user can have access to few pages that a normal user cannot have. But what happens when we want to have different landing page for different types of users. This is something we can handle by overriding auth0 callback handler.

A token can consist of different user claims like name, email, roles, etc. Based on these claims we can decide where to redirect the users, from the callback handler.

import { handleAuth, handleCallback, getSession } from "@auth0/nextjs-auth0"; export default handleAuth({ callback: async (req, res) => { try { const response = await handleCallback(req, res); const { user } = await getSession(req, res); if ( user.roles.includes("super_admin")) { response.headers.set("location", "/super-admin") } else if ( user.roles.includes("admin")) { response.headers.set("location", "/admin") } return response; } catch (error) { console.error(error); } } });

In the above example, we have override callback to land different types of users, Super-Admin, Admin and Normal user, on different routes after login.
Once the user tries to login and is authenticated successfully with auth0, above code is executed, where it invokes handleCallback first so user session is created. Then it gets user object from the session which consists of all the claims we have defined for the users, in our auth0 service. Given that there is a claim named roles, a list of different roles assigned to a user, we check which role is present for logged-in user and based on that we redirect user to their authorised landing route, Super-Admin lands on "/super-admin", Admin lands on "/admin", and Normal user lands on "/" that is app's home page.

Notice that we set "location" inside "response.headers" instead of redirecting using "NextResponse.redirect" because, "NextResponse.redirect" does not consist of all the other fields present in "response" we got from handleCallback, hence it we result in incomplete execution of auth0 callback and user session won't be stored on client side.

Here we are using try...catch block to handle any uncaught exceptions from callback handler. Later we'll see how to handle errors in catch block.

Now what happens if we want to deny access to a user if they have any specific role assigned. Let's take a look at the scenario below.

Restricting unauthorised users from accessing app

Say your application has few users that are no longer active and you want to restrict them from accessing your app and want to show error page instead if they try to login. This can also be handled by overriding auth0 callback handler.

import { handleAuth, handleCallback, getSession } from "@auth0/nextjs-auth0"; import { NextResponse } from "next/server"; export default handleAuth({ callback: async (req, res) => { try { const response = await handleCallback(req, res); const { user } = await getSession(req, res); const errorRoute = "/error"; // this can be route to your app's error page if ( user.roles.includes("inactive_user")) { return NextResponse.redirect(new URL("https://{{your-auth0-domain}}/v2/logout?client_id={{your-client-id}}&returnTo={{errorRoute}}")); } return response; } catch (error) { console.error(error); } } });

Similar to last example, in the example above we override callback handler where it invokes handleCallback to create session and then gets user object from it. Then we check whether the logged-in user has role inactive_user, if yes, we redirect to auth0's logout URL directly using "NextResponse.redirect" to logout user and then app returns to /error route, our app's error page.

Let's see how our error page component should look like,

// app/error/page.jsx "use client"; export default const ErrorPageComponent = () => { return ( //... <p>Sorry, you do not have access to the application.</p> //... ); };

Here we do not use logout API api/auth/logout to logout user, since user is not completely logged-in to the SDK. Refer this github discussion for more details.

Now instead of static error message, what if we want to show error messages from auth0 on screen. Let's see how to handle auth0 error messages gracefully.

Graceful error handling to surface error messages from auth0

Whenever any error occurs in /api/auth/callback API route, we get a blank page on browser. What if we want to surface this error messages to users on screen. We can handle this in catch block of auth0 callback handler by overriding it.

We need to get error message from auth0 error and somehow pass it to our error page where we can show it on screen in our error page component.

import { handleAuth, handleCallback } from "@auth0/nextjs-auth0"; import { NextResponse } from "next/server"; export default handleAuth({ callback: async (req, res) => { try { const response = await handleCallback(req, res); // Write your custom logic here, if needed... return response; } catch (error) { const { cause } = error; const redirectURL = new URL("/error", req.url); redirectURL.searchParams.append("error_description", cause.errorDescription); return NextResponse.redirect(redirectURL); } } });

As we already know, overriding auth0 callback handler needs us to invoke handleCallback to create session. In the above example we call default callback handler and return the response, we can customise the behaviour if needed and then return the response from callback. If any error occurs in handleCallback or the custom logic afterwards, it will bring execution to catch block where we can get error details like code, message, cause, etc.
Here cause consists of "errorDescription" that will have error message sent from auth0.

Given that our app's error page route is /error, we can append it with errorDescription as shown above and redirect to error page. This will take us to URL https://{{your-app-domain}}/error?error_description={{error-message}} where we can showcase error message in our error page component.

Let's see how our error page component should look like,

// app/error/page.jsx "use client"; import { useSearchParams, usePathname, useRouter } from "next/navigation"; export default const ErrorPageComponent = () => { const searchParams = useSearchParams(); return ( //... <p>{searchParams.get("error_description")?.toString() || "{{Some fallback error message.}}"}</p> //... ); };

Here we can get error message text from search params and show it on screen.

Conclusion

Auth0 callback handler from Next.js SDK allows us to perform many different actions on server-side that helps us keep the client-side components clean and secure. Contact us to discuss further.

Resource 💡