DEV Community

Peter Jacxsens
Peter Jacxsens

Posted on • Updated on

13/ Signing up with the NextAuth CredentialsProvider

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 custom NextAuth 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:

  1. GoogleProvider errors will be rare, so just go ahead? No, bad UX.
  2. Remove GoogleProvider error handling? No, will resort to defaults.
  3. 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.

wireframe

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',
  }
);
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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": {}
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. User goes to sign up page
  2. Enters credentials: username, email and password.
  3. Signing up does not sign you in.
  4. Signing up creates an unauthorized user in Strapi.
  5. Strapi sends an auth email.
  6. User clicks link and goes to confirmation page
  7. Strapi auths the user.
  8. 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
Enter fullscreen mode Exit fullscreen mode

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 />;
}
Enter fullscreen mode Exit fullscreen mode

<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.

signup page

// 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

We continue in the next chapter.


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

Top comments (0)