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)
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,
//...
};
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;
};
};
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;
}
When we sign in using credentials, authorize
will always return null. This will:
- Stop the auth flow (the user won't get signed in).
- Return one of the internal
NextAuth
errors via an error searchParam.
Testing this, confirms this:
We are not signed in. We are not redirected. Our url now has an error searchParam:
http://localhost:3000/signin?error=CredentialsSignin (edited)
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');
}
And we try to sign in:
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
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,
});
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;
}
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);
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
}
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:
-
return null
will trigger the internalNextAuth
error handling. We will stay on the sign in page and an error searchParam is added to the url. -
throw new Error
will triggers another internalNextAuth
error handling process. We are redirected toauthError
(a customNextAuth
error page), again get an error searchParam but this time, the value of said param equals the value of the error we threw insideauthorize
. -
throw new Error
+ addredirect: false
option in thesignIn
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,
};
},
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;
}
},
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;
}
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.