DEV Community

Peter Jacxsens
Peter Jacxsens

Posted on • Updated on

9/ How to add error handling to NextAuth GoogleProvider

This chapter is a bit tricky because we need to introduce some new concepts. On top of that, NextAuth offers very minimal error feedback. This is a bit confusing. We will start with some slightly of topic points and then return to error handling.

The final code for this chapter is available on github (branch callbacksForGoogleProvider).

Next error boundary

We start by adding some error handling in the Next frontend by adding error.tsx in our root. We use the basic example from the docs:

// frontend/src/app/error.tsx

'use client'; // Error components must be Client Components

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string },
  reset: () => void,
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error);
  }, [error]);

  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>Root error: {error.message}</p>
      <button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

When something goes wrong and there is an uncaught error, this will catch it. This is off topic because NextAuth with GoogleProvider doesn't produce uncaught errors. But, since this chapter is about errors, we added it here.

NextAuth signIn callback

We already briefly mentioned this callback in a previous chapter: this callback lets you control if a user is allowed to sign in.

async signIn({ user, account, profile, email, credentials }) {
  return true
},
Enter fullscreen mode Exit fullscreen mode

Why would we need this? Suppose a user made a Google account but didn't verify the account. We would not want this user to connect with our app using this unverified account. Our signIn callback has a lot of arguments that we covered before. One of them is profile: the raw user data Google sent back. Profile has a property email_verified. We could then do this:

async signIn({ user, account, profile, email, credentials }) {
  if(!profile.email_verified) return false;
  return true
},
Enter fullscreen mode Exit fullscreen mode

Returning false stops the entire auth flow for this user. But, on top of that, it will redirect the user to the default or custom NextAuth error page.

Creating a custom error page in NextAuth

Besides having a default sign in page (the ugly one from before that we replaced with our custom one), NextAuth also has default error page. We can and will replace this default page with a custom error page. Note that this is not equal to the error.tsx we made earlier.

When does NextAuth use this page? The docs state that the user will be redirected to this error page when there is a:

  • Configuration error in authOptions.
  • AccessDenied error: when you restricted access through the signIn callback, or redirect callback.
  • Verification error: has to do with the email provider.
  • Default error: some other cases ... (dunno)

We just saw the signIn callback. When it returns false, we are redirected to the error page. So, let's try that, we add this callback:

// frontend/src/api/auth/[...nextAuth]/authOptions.ts
{
  async signIn({ user, account, profile, email, credentials }) {
    return false;
  }
}
Enter fullscreen mode Exit fullscreen mode

We just want to see the error page so we temporarily return false on everything and try signing in. As expected, we are redirect to the default error page:

NextAuth default error page

And it is in the same memorable style we saw from the default sign in page. But, aren't we missing the error message? No, NextAuth put that in the url as an error searchParam: ?error=AccessDenied.

http://localhost:3000/api/auth/error?error=AccessDenied
Enter fullscreen mode Exit fullscreen mode

Where is the rest of the message? Nope that's all. We will come back to this. First we finish the section on this error page.

Custom error page

As with the sign in page, we can also create a custom error page. Create a page:

// frontend/src/app/(auth)/authError/page.tsx

type Props = {
  searchParams: {
    error?: string,
  },
};

export default function AuthErrorPage({ searchParams }: Props) {
  return (
    <div className='bg-zinc-100 rounded-sm p-4 mb-4'>
      AuthError: {searchParams.error}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

And then tell NextAuth about it in authOptions.page:

// frontend/src/api/auth/[...nextAuth]/authOptions.ts
  pages: {
    signIn: '/signin',
    error: '/authError',
  },
Enter fullscreen mode Exit fullscreen mode

NextAuth custom error page

We test it out and everything works. NextAuth now uses our custom error page. As said, we will deal with the error message later.

Recap

Let's do a quick recap. We found out that NextAuth has an error page. We get redirected to this page when:

  1. There is a configuration error (in authOptions) (/authError?error=Configuration)
  2. We use signIn or redirect callbacks (read about redirect in the docs) (/authError?error=AccessDenied)
  3. There is a verification error (/authError?error=Verification)
  4. There are other errors (?) (/authError?error=Default)

We replaced the default error page with a custom one. So, we've handled some of the errors we can encounter in NextAuth. Before we move on, we need to mention a few more points.

In our signIn callback, we returned false when we didn't want the user to be able to authenticate. But, there is another option. We can also return a relative path. This will override the error page and redirect the user to said relative path. Meaning you can redirect them to another route, f.e. specially made for this purpose.

Lastly, let us actually implement the case I gave as an example earlier, with the unverified Google account. We update the signIn callback:

async signIn({ user, account, profile }) {
  // console.log('singIn callback', { account, profile, user });
  if (
    account &&
    account.provider === 'google' &&
    profile &&
    'email_verified' in profile
  ) {
    if (!profile.email_verified) return false;
  }
  return true;
},
Enter fullscreen mode Exit fullscreen mode

(We had to add some type checks because TypeScript was yelling at us.)

Other errors

Every request we make has the potential to fail or return an error. We should account for this. In our sign in flow thus far, we use 4 requests:

  1. call sign in
  2. call sign out
  3. call Google
  4. call Strapi

Calling sign in and sign out

Internally, NextAuth uses a REST api endpoint to handle all of it's flow. This means that there are request to this endpoint and potential errors.

I tried to cause an error, f.e. signIn('foobar', {...}) but nothing happened. No error in the console or terminal and also no error parameter in the url. This let me to conclude that you can safely call signIn and signOut without trying to catch errors.

Google OAuth request

When signing in with the GoogleProvider, NextAuth makes a request to Google OAuth at a certain point. As it's a request, it can go wrong. These OAuth errors can include things like:

  • Unverified app
  • Invalid token
  • Incorrect callback

Just, errors. But, how do we handle these? First, let's create an error. In authOptions, we replace the clientSecret with a random string and see what happens:

  //clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '',
  clientSecret: 'foobar',
Enter fullscreen mode Exit fullscreen mode

We run the app and do a sign in. Result is nothing at first glance. We're not logged in, no error pop up, our app doesn't crash and we don't get redirected. But there are some things that happened. Firstly, our url now looks like this:

  http://localhost:3000/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2F&error=OAuthCallback
  # after decodeURIComponent
  http://localhost:3000/signin?callbackUrl=http://localhost:3000/&error=OAuthCallback
Enter fullscreen mode Exit fullscreen mode

So, the base url is http://localhost:3000/signin and we have 2 searchParams: callbackUrl and error. The value of error is 'OAuthCallback'. Secondly, in our frontend terminal, we have a full error showing:

[next-auth][error][OAUTH_CALLBACK_ERROR]
https://next-auth.js.org/errors#oauth_callback_error invalid_client (Unauthorized) {
  error: OPError: invalid_client (Unauthorized)
      at processResponse (webpack-internal:///(rsc)/./node_modules/openid-client/lib/helpers/process_response.js:35:19)
      ...
      EDITED: more urls
      ...
      at async Server.requestListener (D:\projecten\dev.to\next-strapi-nextauth\frontend\node_modules\next\dist\server\lib\start-server.js:140:13) {
    name: 'OAuthCallbackError',
    code: undefined
  },
  providerId: 'google',
  message: 'invalid_client (Unauthorized)'
}
Enter fullscreen mode Exit fullscreen mode

So, NextAuth logged the error as OAUTH_CALLBACK_ERROR while the original Google OAuth error was probably 'invalid_client (Unauthorized)'. That's in the terminal. In our client (browser), NextAuth gave us the error parameter: error=OAuthCallback. (no error logs in the browser console!)

NextAuth errors and error codes

NextAuth makes a difference between errors and error codes. OAUTH_CALLBACK_ERROR is a NextAuth error and gets logged in the terminal. ?error=OAuthCallback is a NextAuth error code and it gets put into the callback url as a searchParam.

Any problem that can occur in an auth flow will be caught by NextAuth and categorized into a NextAuth error. Full list of them can be found in the docs.

On error codes, NextAuth says this:

We purposefully restrict the returned error codes for increased security.

NextAuth passes errors as searchParams to 2 pages:

  1. The default of custom login page.
  2. The default of custom error page.

Above example was an error send as searchParam to our custom sign in page. We made a custom sign in page (/signin) and it received an error: /signin?error=OAuthCallback. Earlier, we already talked about the errors that are passed to the NextAuth error page (f.e. Configuration, AccessDenied).

The NextAuth docs provide us with a full list of all error codes, split by page (login or error).

Minimal error messages

So, NextAuth errors (terminal) are pretty detailed but we can't use them for user feedback (by design). What is their use then? I'm not sure. To solve problems while developing. To solve problems in production by checking the logs.

Yet, we need some user feedback. NextAuth provides us with one word, an error code like OAuthCallback, callback or AccessDenied. So, as a developer, you will have to come up with some clever error messages for each code. f.e.

const errorMap = {
  'OAuthCallback': 'A very clever and UX friendly message here.'
  'callback': 'And another one',
  'AccessDenied': 'Even more?',
}

// call them in the frontend
{errorMap[searchParams.error]}
Enter fullscreen mode Exit fullscreen mode

So, good luck with that.

Handling sign in errors in NextAuth

We still need to actually display the errors on the sign in page. Make a new client component:

// frontend/src/components/auth/signin/GoogleSignInError.tsx

'use client';

import { useSearchParams } from 'next/navigation';

export default function GoogleSignInError() {
  const searchParams = useSearchParams();
  const error = searchParams.get('error');
  if (!error) return null;
  return (
    <div className='text-center p-2 text-red-600 my-2'>
      Something went wrong! {error}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Note that we don't bother writing a better error message, that's outside the scope of this series. We add this component inside the <SignIn /> component and we are done:

//...
  <GoogleSignInButton />
  <GoogleSignInError />
// ...
Enter fullscreen mode Exit fullscreen mode

sign in error in NExtAuth

Handling Strapi errors in NextAuth

There is one thing we haven't checked. Earlier in this article, we mentioned we make 4 requests in our app: sign in and out, Google OAuth and Strapi. We are yet to test if a Strapi error is handled.

In authOptions, GoogleProvider, remove the incorrect googleClient that we set to 'foobar' earlier to provoke an error. Then, to provoke an error from Strapi, we add a random string as the access_token (that we normally get from Google OAuth).

// frontend/src/api/auth/[...nextAuth]/authOptions.ts

//`${process.env.STRAPI_BACKEND_URL}/api/auth/${account.provider}/callback?access_token=${account.access_token}`,
`${process.env.STRAPI_BACKEND_URL}/api/auth/${account.provider}/callback?access_token=foobar`,
Enter fullscreen mode Exit fullscreen mode

What do we expect? Some kind of NextAuth error code in the url. We run the app and try signing in. As expected an error in our url: error=Callback. Looking this up in the docs:

Callback: Error in the OAuth callback handler route

Besides this, we also get the real error in the terminal:

[next-auth][error][OAUTH_CALLBACK_HANDLER_ERROR]
https://next-auth.js.org/errors#oauth_callback_handler_error 400 Bad Request {
  message: '400 Bad Request',
  ...
}
Enter fullscreen mode Exit fullscreen mode

[OAUTH_CALLBACK_HANDLER_ERROR] is how NextAuth handled this error. '400 Bad Request' comes from Strapi: we threw it here throw new Error(strapiError.error.message);.

So, we handled potential Strapi errors.

Conclusion

We learned how NextAuth handles errors. In most cases it will just add an error code in the url of the sign in page: ?error=. In some cases it will redirect to a default or custom NextAuth error page, also with an error code. You can look up these code in the docs and use the error code to give some error feedback to the user.

Besides this, NextAuth logs a more detailed error in the terminal of your Next server. These also have specific error names that you can look up in the docs. However, you cannot use these for user feedback.

If we step back from the more practical aspects, it is clear that NextAuth handles all errors. We don't have to catch anything. This is great. NextAuth intentionally limited the error information in the frontend. This can be a bit frustrating and will require an effort to handle. But, in the end, NextAuth is very stable. Getting errors using GoogleProvider should be rare and are now handled.

This concludes our work with GoogleProvider. In the rest of this series we will work with the credentials provider.


If you want to support my writing, you can donate with paypal.

Top comments (0)