11/ How to handle errors in the NextAuth authorize function / CredentialsProvider

Peter Jacxsens - Apr 1 - - Dev Community

All the code for this chapter is available on github: branch credentialssignin.

The app in it's current state actually works, as long as you enter the correct email/username + password. But, we haven't done any error handling. So, what happens when we enter an incorrect password? Let's try it out, we run the app and enter an incorrect password.



http://localhost:3000/authError?error=Cannot%20read%20properties%20of%20undefined%20(reading%20%27username%27)


Enter fullscreen mode Exit fullscreen mode

We are redirect to the /authError route with an error searchParam: 'Cannot read properties of undefined (reading username)'.

What is going on here? Firstly, the /authError page is the custom NextAuth page that we setup in an earlier chapter. NextAuth only uses it in limited cases. This case seems to be one of those. Of course, this is not a good way to give our user some feedback. We will be handling these error ourself so this custom NextAuth error page won't get shown anymore. Still, it's nice to see that NextAuth has our back.

The error 'Cannot read properties of undefined (reading username)' comes from the authorize function. The line where we return the user to be exact:



return {
  name: data.user.username,
  //...
};


Enter fullscreen mode Exit fullscreen mode

Inside authorize we make a request to Strapi, passing our incorrect credentials. Strapi says nope and returns a strapiError object:



// frontend/src/types/strapi/StrapiError.d.ts
export type StrapiErrorT = {
  data: null;
  error: {
    status: number;
    name: string;
    message: string;
  };
};


Enter fullscreen mode Exit fullscreen mode

Data is null. So, when we are trying to do this data.user.username, we try to read username from user but user is undefined since data is null. This is what the error is telling us: 'Cannot read properties of undefined (reading username)'. Let's fix this.

Error handling in NextAuth authorize function

In a previous chapter we had to handle errors in the jwt callback with the GoogleProvider. There too we had to make a Strapi auth request that could return an strapiError. Inside our jwt callback we handled this error by throwing an error. We then had a very limited option to handle this in our front-end UI by reading our the error searchParam on the /signin or /authError pages. We've seen this and I won't repeat it. The point here is that we now need to do something very similar. Again, error handling on a Strapi auth request inside a callback function.

As inside our jwt callback, we can throw an error to interrupt the auth flow. When throwing an error inside authorize, the auth flow will be stopped (no more callbacks will be called and the user won't get signed in) and we get an error to work with in our UI.

But there is a different way to stop the auth flow from inside authorize: by returning null. Returning null also stops the auth flow but it will send back one of the default NextAuth error codes that we covered in a previous chapter. How does it send these back, via the error searchParam that we know from the jwt callback.

Let me demonstrate this. We temporarily edit our authorize function:



async authorize(credentials, req) {
  return null;
}


Enter fullscreen mode Exit fullscreen mode

When we sign in using credentials, authorize will always return null. This will:

  1. Stop the auth flow (the user won't get signed in).
  2. Return one of the internal NextAuth errors via an error searchParam.

Testing this, confirms this:

NextAuth authorize return null

We are not signed in. We are not redirected. Our url now has an error searchParam:



http://localhost:3000/signin?error=CredentialsSignin (edited)


Enter fullscreen mode Exit fullscreen mode

The value of this error searchParam is one the the NextAuth error codes: CredentialsSignin. On top of this, the error handling we initially setup for GoogleProvider works here too and will display the error below the form.

Quick recap, we can interrupt the auth flow in the authorize function either by throwing an error or by returning null. By returning null Nextauth will use it's internal error handling that we know from error handling inside the jwt callback. This means that returning null inside authorize kinda equals throwing an error inside the jwt callback.

Throwing an error inside authorize works completely different from throwing an error inside jwt callback. Let's try this out:



async authorize(credentials, req) {
  throw new Error('foobar');
}


Enter fullscreen mode Exit fullscreen mode

And we try to sign in:

NextAuth authorize throw error

We are redirected to /authError and have an error searchParam: 'foobar'. Note that this is the same process we got in the beginning of this chapter.



http://localhost:3000/authError?error=foobar


Enter fullscreen mode Exit fullscreen mode

Here is the cool thing. We can handle this error differently, manually.

Using NextAuth signIn function to handle errors

We can prevent NextAuth from redirecting when we throw an error inside authorize. How? By setting an extra parameter redirect: false in the signIn function:



signIn('credentials', {
  identifier: data.identifier,
  password: data.password,
  redirect: false,
});


Enter fullscreen mode Exit fullscreen mode

This does not only change the auth flow (by not redirecting), it also changes the behavior of signIn. signIn will now return a value:



{
  /**
   * Will be different error codes,
   * depending on the type of error.
   */
  error: string | undefined;
  /**
   * HTTP status code,
   * hints the kind of error that happened.
   */
  status: number;
  /**
   * `true` if the signin was successful
   */
  ok: boolean;
  /**
   * `null` if there was an error,
   * otherwise the url the user
   * should have been redirected to.
   */
  url: string | null;
}


Enter fullscreen mode Exit fullscreen mode

Where error will be the error we throw in authorize. The means that we now have direct access in our frontend UI to the errors we throw in our authorize function. We will have to update signIn to await the response and update our handleSubmit to be async:



const signInResponse = await signIn('signin', {
  identifier: validatedFields.data.identifier,
  password: validatedFields.data.password,
  redirect: false,
});
console.log('signInResponse', signInResponse);


Enter fullscreen mode Exit fullscreen mode

In our current example signInResponse will then look like this:



// this will only log in our browser console
{
  "error": "foobar",
  "status": 401,
  "ok": false,
  "url": null
}


Enter fullscreen mode Exit fullscreen mode

Recap

Let me recap this entire process. We need to handle a possible Strapi error inside our authorize function. There are 3 ways to stop the authorize function/callback:

  1. return null will trigger the internal NextAuth error handling. We will stay on the sign in page and an error searchParam is added to the url.
  2. throw new Error will triggers another internal NextAuth error handling process. We are redirected to authError (a custom NextAuth error page), again get an error searchParam but this time, the value of said param equals the value of the error we threw inside authorize.
  3. throw new Error + add redirect: false option in the signIn function will skip the redirect and the error searchParam. Instead, signIn will now return an object. We can then use this return value to handle the error inside our form component.

I took a long time to explain this but I do hope this makes sense. It's all a bit confusing but I hope I cleared everything up.

A note before we go on. Can we use this same process for GoogleProvider? No, the redirect option only works for the email and credentials providers.

Coding time

It's time to actually write our code. We do the Strapi error handling inside the authorize function first and then we'll look at the form component. This is where we left of:



async authorize(credentials, req) {
  const strapiResponse = await fetch(
    `${process.env.STRAPI_BACKEND_URL}/api/auth/local`,
    {
      method: 'POST',
      headers: {
        'Content-type': 'application/json',
      },
      body: JSON.stringify({
        identifier: credentials!.identifier,
        password: credentials!.password,
      }),
    }
  );

  const data: StrapiLoginResponseT = await strapiResponse.json();
  return {
    name: data.user.username,
    email: data.user.email,
    id: data.user.id.toString(),
    strapiUserId: data.user.id,
    blocked: data.user.blocked,
    strapiToken: data.jwt,
  };
},


Enter fullscreen mode Exit fullscreen mode

Since we do a fetch, we will wrap it inside a try catch block. Then, we will check if strapiResponse is ok or not and handle the not ok part. (The ok part is already done) Here is our updated function:



async authorize(credentials, req) {
  try {
    const strapiResponse = await fetch(
      `${process.env.STRAPI_BACKEND_URL}/api/auth/local`,
      {
        method: 'POST',
        headers: {
          'Content-type': 'application/json',
        },
        body: JSON.stringify({
          identifier: credentials!.identifier,
          password: credentials!.password,
        }),
      }
    );

    if (!strapiResponse.ok) {
      // return error to signIn callback
      const contentType = strapiResponse.headers.get('content-type');
      if (contentType === 'application/json; charset=utf-8') {
        const data: StrapiErrorT = await strapiResponse.json();
        throw new Error(data.error.message);
      } else {
        throw new Error(strapiResponse.statusText);
      }
    }

    // success
    const data: StrapiLoginResponseT = await strapiResponse.json();
    return {
      name: data.user.username,
      email: data.user.email,
      id: data.user.id.toString(),
      strapiUserId: data.user.id,
      blocked: data.user.blocked,
      strapiToken: data.jwt,
    };
  } catch (error) {
    // Catch errors in try but also f.e. connection fails
    throw error;
  }
},


Enter fullscreen mode Exit fullscreen mode

This should all make sense. We will also add one more line before the try catch block where we test if there are credentials. We will validate this in our form component before we call signIn but this is just an extra precaution:



// make sure the are credentials
if (!credentials || !credentials.identifier || !credentials.password) {
  return null;
}


Enter fullscreen mode Exit fullscreen mode

Why do we return null here? Because it is very unlikely to happen due to our form validation beforehand and we just let NextAuth handle it.

In the next chapter we will handle our form component.


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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .