Before we start coding there's a problem we need to handle with GoogleProvider and then we need to determine how we will setup the sign up flow.
This code for this example is available on github: branch credentialssignup.
GoogleProvider limitations
When using the GoogleProvider with NextAuth
there is only one flow. If the user has never connected to the app before, said user will be automatically added in the Strapi
DB by our NextAuth
setup. This means that there is no difference between the sign in and sign up flow for the GoogleProvider. One button signs in and up.
But we will have 2 pages, sign in and up. Logically we would then just use the one button on both pages, yes? No, there is a problem with this. We implemented error handling for our GoogleProvider:
In most cases
NextAuth
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 customNextAuth
error page, also with an error code.
This means that when there is an error in our GoogleProvider on our sign up page, NextAuth
will redirect us to the sign in page and display the error there. On top of that, NextAuth
does not have a sign up page that we can register. (pages option in our authOptions object)
In other words, we can only use the sign in with Google button on the sign in page. If we use it on another page like sign up, the error handling won't work as desired - we would like error message on the sign up page, not be redirected to the sign in page.
So ... this is a problem. Worse yet, we can't fix it.
GoogleProvider solution?
We haven't really defined an end goal for our app: a specific auth setup we want to build. This is a problem I stumbled into. It's a limitation, an unpleasant one, that NextAuth
imposes. For me, in this situation, it is not the biggest problem because I'm just making a tutorial. But, if you're working for a client who has specific demands, this will be an issue! The point here is: know that your carefully crafted UX prototypes may run into these kind of issues. There will be limitations.
Possible solutions here:
- GoogleProvider errors will be rare, so just go ahead? No, bad UX.
- Remove GoogleProvider error handling? No, will resort to defaults.
- Leave out the sign in with Google button on the sign up page?
The most elegant solution I came across was to rework the sign in page using tabs.
One tab for sign in and one for sign up. Below that the sign in with Google button that's always visible. This would be our sign in page so the GoogleProvider error handling would work.
But, this all is beyond the scope of this series. We will be building a separate sign up page while leaving out the sign in with Google button. Our focus is the show how to build a sign up flow for the CredentialsProvider.
Sign up flow in Strapi
Strapi
has an endpoint for signing up new users with credentials.
const strapiResponse = await fetch(
process.env.STRAPI_BACKEND_URL + '/api/auth/local/register',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, email, password }),
cache: 'no-cache',
}
);
So, a POST request to /api/auth/local/register
that passes the username, email and password in an object. The Strapi
response depends on your Strapi
settings. If you have email confirmation disabled, then Strapi
will
- Add this user into the database (Authorized)
- Return the DB user with a
Strapi JWT token
.
If you have email confirmation enabled, Strapi
will
- Add this user into the database (not confirmed)
- Send an email (if you made the correct setup) to this user asking for confirmation
- Return the DB user without a token!
{
"user": {
"id": 4,
"username": "Peter ",
"email": "someEmail",
"provider": "local",
"confirmed": false,
"blocked": false,
"createdAt": "2024-03-19T14:23:42.478Z",
"updatedAt": "2024-03-19T14:23:42.478Z"
}
}
A quick reminder: when signing in, we put this Strapi token
into our NextAuth token
. We can then make requests to Strapi
endpoints for non public data by passing along the Strapi token
that we read from our NextAuth token
using useSession
or getServerSession
. No token, no authorization to make requests. This makes sense. We will deal with the implications this has for NextAuth
in a bit.
It's also possible that Strapi
returns an error. For example when the username or email has already been taken. Strapi
then returns an error in the the standard Strapi
error format that we saw before:
{
"data": null,
"error": {
"status": 400,
"name": "ApplicationError",
"message": "Email or Username are already taken",
"details": {}
}
}
The confirmation email flow will be handled later. We now know how the Strapi
signup endpoint works.
CredentialsProvider sign up flow
There is a wide range of UX patterns on how to do a sign up. Each of these requires a different setup and I can't cover them all. This is the approach we will take here:
- User goes to sign up page
- Enters credentials: username, email and password.
- Signing up does not sign you in.
- Signing up creates an unauthorized user in
Strapi
. -
Strapi
sends an auth email. - User clicks link and goes to confirmation page
-
Strapi
auths the user. - User is prompted to sign in.
And then somewhere in between these we also need to add an option for the user to request a new confirmation email.
It's not a bad flow but it has some problems. I know this is probably not the flow you want to build. But, you can learn from this and refactor it into your own flow.
NextAuth
How does NextAuth
fit into this sign up flow? We briefly mentioned using a second CredentialsProvider but that is for a different auth flow. NextAuth
is basically for setting and reading out a NextAuth token
.
However, in our auth flow, signing up won't sign you in. Confirming your email won't sign you in either. The only thing that will sign you in is signing in, using the sign in page. Therefore, we won't need NextAuth
.
This will make full sense once we started building the actual sign up components.
Strapi setup
Run your backend and open up Strapi
admin:
settings > Users & Permissions plugin > Advanced settings
- Default role for authenticated users -> authenticated
- Enable email confirmation -> true
- Redirection url -> http://localhost:3000/confirmEmail (this is for later)
- Save
Sign up page
We will be reusing the structure and styling from our sign in page as much as possible. Remember, we will be leaving out the sign in with Google button. Create a page, a <SignUp />
and <SignUpForm />
component.
// frontend/src/app/(auth)/signup/page.tsx
import SignUp from '@/components/auth/signup/SignUp';
export default function SignUpPage() {
return <SignUp />;
}
<SignUp />
is a server component. It just contains some html and tailwind, you can see it on github. Inside <SignUp />
we will import and render our <SignUpForm />
component where all the action happens. Strapi
requires 3 input fields for signing up a frontend user: username, email and password. So, we will have a form with these 3 fields and a button. This will be our starting point.
// frontend/src/components/auth/signup/SignUpForm.tsx
export default function SignUpForm() {
return (
<form className='my-8'>
<div className='mb-3'>
<label htmlFor='username' className='block mb-1'>
Username *
</label>
<input
type='text'
id='username'
name='username'
required
className='border border-gray-300 w-full rounded-sm px-2 py-1'
/>
</div>
<div className='mb-3'>
<label htmlFor='email' className='block mb-1'>
Email *
</label>
<input
type='email'
id='email'
name='email'
required
className='bg-white border border-zinc-300 w-full rounded-sm p-2'
/>
</div>
<div className='mb-3'>
<label htmlFor='password' className='block mb-1'>
Password *
</label>
<input
type='password'
id='password'
name='password'
required
className='bg-white border border-zinc-300 w-full rounded-sm p-2'
/>
</div>
<div className='mb-3'>
<button
type='submit'
className='bg-blue-400 px-4 py-2 rounded-md disabled:bg-sky-200 disabled:text-gray-400 disabled:cursor-wait'
>
sign up
</button>
</div>
</form>
);
}
We continue in the next chapter.
If you want to support my writing, you can donate with paypal.