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>
);
}
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
},
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
},
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 thesignIn callback
, orredirect 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;
}
}
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:
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
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>
);
}
And then tell NextAuth
about it in authOptions.page:
// frontend/src/api/auth/[...nextAuth]/authOptions.ts
pages: {
signIn: '/signin',
error: '/authError',
},
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:
- There is a configuration error (in authOptions) (
/authError?error=Configuration
) - We use
signIn
orredirect callbacks
(read about redirect in the docs) (/authError?error=AccessDenied
) - There is a verification error (
/authError?error=Verification
) - 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;
},
(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:
- call sign in
- call sign out
- call Google
- 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',
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
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)'
}
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:
- The default or custom login page.
- The default or 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]}
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>
);
}
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 />
// ...
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`,
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',
...
}
[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.