10/ NextAuth CredentialsProvider: signing in
Tags: nextauth, nextjs, credentialsprovider
Published: 2024-04-01 16:25:10

We will now add the credentials auth flow to our project. By credentials we mean the old school login method of email + password. `NextAuth` calls this the credentials provider. `Strapi` calls this local auth. Here is an overview of what we need:

1. Register page: create an unauthenticated user + send verification email
2. Verification page
3. Request a new verification email
4. Login page
5. Request password reset page (forgot password)
6. Reset password page
7. Change password page
8. Update user (me) page

In this chapter we are going to add credentials to our custom sign in page.

All the code for this chapter is available on [github: branch credentialssignin](https://github.com/peterlidee/NNAS/tree/credentialssignin).

## Setup

By default, the email provider is enabled in `Strapi`. So nothing to do here. We have to create a `Strapi user` so we have a user to test our credentials sign in that we will be building. In `Strapi admin`:

```
Content Manager > Collection Types > User > Create a new entry
```

2 notes here:

1. We're creating a frontend user, not a backend user (one that can access our `Strapi admin`).
2. If you're coding along, make sure to use an email that you own because we will be sending emails to this address later on.

Create a user:

- username: Bob
- email: bob@example.com
- password: 123456
- confirmed: true
- blocked: false
- role: authenticated

Save and we're done.

## Register our credentials provider with NextAuth

In our authOptions.providers we already have the GoogleProvider. We now import and add CredentialsProvider to the providers array. This is our starting setup:

```jsx
// frontend/src/app/api/auth/[...nextauth]/authOptions.ts
{
  providers: [
    //...
    CredentialsProvider({
      name: 'email and password',
      credentials: {
        identifier: {
          label: 'Email or username *',
          type: 'text',
        },
        password: { label: 'Password *', type: 'password' },
      },
      async authorize(credentials, req) {
        console.log('calling authorize');
        return null;
      },
    }),
  ],
}
```

We have name, credentials and authorize properties. The name and credentials properties are actually mostly useless. These serve to populate the default _sign in_ page that `NextAuth` generates. Since we use a custom _sign in_ page, things like name or the labels aren't used.

Let's quickly revert back to this default sign in page and see what we get. In authOptions.pages, comment out signin, start up the app and navigate to <http://localhost:3000/api/auth/signin>:

![NextAuth default signin credentials](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bbyodm7gh5voejyhwep6.png)

And we have everything we would expect. The Google sign in and the credentials sign in with email/username and password + a submit button. Note: `strapi` lets you sign in with either the email or username. We account for this by using an identifier field that can be a username or email. We won't use the default sign in page but it makes it clear what the credentials settings are for. **But**, there is more.

`NextAuth` also using these settings to infer Types. In the `async authorize(credentials, req) {}` function that we added, the credentials argument is of Type CredentialsProvider.credentials, so { identifier: string, password: string }. This means that we have to make sure that our custom _sign in_ page has the same keys (form names and id's).

Finally, the `authorize` function is where we will handle the submitted form, but more on that later. Let's revert the authOptions.pages.signin back to our custom page and move on.

## Creating a sign in form

We need a form, this is our next step. Create a new component `<SignInForm />`:

```jsx
// frontend/src/components/auth/signin/SignInForm.tsx

export default function SignInForm() {
  return (
    <form method='post' className='my-8'>
      <div className='mb-3'>
        <label htmlFor='identifier' className='block mb-1'>
          Email or username *
        </label>
        <input
          type='text'
          id='identifier'
          name='identifier'
          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-500'
        >
          sign in
        </button>
      </div>
    </form>
  );
}
```

We just added 2 inputs (identifier and password) and a button. We then load this form into our `<SignIn />` component and it looks like this:

![NextAuth sign in form with Credentials](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jsihqwwo8vv68iwv0bmi.png)

## Calling NextAuth signIn function

We know what to do next because we did the same with our `sign in with Google` button. We have to call our `NextAuth` `signIn` function with some arguments:

```jsx
signIn('credentials', {
  identifier: '...',
  password: '...',
});
```

Quick note here. It is possible to have multiple credential providers. In this case you would add an id property to each CredentialProvider and call `signIn` with this id.

At this point, you may be thinking about using a server action to handle our form submit. This not possible because `signIn` is a client side function. You cannot call if from the server side. Therefor, we must also put our inputs into state. We update our component:

```jsx
// add initialState
const initialState = {
  identifier: '',
  password: '',
};

// set state
const [data, setData] = useState(initialState);

// create an event handler
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  setData({
    ...data,
    [e.target.name]: e.target.value,
  });
}
```

And finally, we update our inputs with `value={data.identifier} onChange={handleChange}`. So, basically, we make the inputs controlled inputs. This should be clear.

Next, create an onsubmit handler and call the `signIn` function:

```jsx
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
  e.preventDefault();
  signIn('credentials', {
    identifier: data.identifier,
    password: data.password,
  });
}
```

At this point, our request leaves our form component and the `authorize` and `callback` functions come into play. We're not done with this form yet. We need error handling, input validation, loading state, ... But we will come back to that later.

## Writing the NextAuth Authorize function for the CredentialsProvider

The `authorize` function that we created inside our CredentialProvider earlier is the main workhorse of our credential auth flow. Let's think about where we are right now. The user submitted an identifier (email/username) and a password. What do we need to do with these? We have to ask `Strapi` if these data are correct via an api call.

On success, `Strapi` will return the user data and a `Strapi JWT token`. We will then put this token into the `NextAuth JWT token` using the `NextAuth` callbacks. When our api call to `Strapi` returns an error (f.e. incorrect password), we will have to handle this error somehow.

It's best to look at `authorize` as another of the `NextAuth` callback functions because it actually is a callback function. The return value from `authorize` is passed into the other callbacks as the user argument. The user argument in the `jwt callback` is the return value from the `authorize` function.

This makes sense. When using the GoogleProvider earlier, Google OAuth sends back data. This data is then used by `NextAuth` to populate account, profile and user. When using the CredentialsProvider there is no such data. You need to fetch this data yourself from `Strapi`.

`Authorize` has 2 parameters: credentials (identifier and password) and the actual request object. We will use these credentials and send them to `Strapi`.

## Strapi API

The `Strapi` api endpoint that we need is `/api/auth/local`. We need to make a POST request and send the credentials along as JSON:

```jsx
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,
    }),
  }
);
```

From this strapiResponse, we can then return the user data:

```jsx
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,
  };
},
```

Inside our `NextAuth` flow, once we return data from `authorize`, the callbacks get called. The `signIn` callback will just return true and is not relevant. The `jwt callback` will be called next.

## Customizing the NextAuth jwt callback for the CredentialsProvider

From working with GoogleProvider, we learned that when we sign in, the `jwt callback` arguments token, trigger, account, user and session will all be populated. When the user is already signed in, all these (except token) will be undefined.

When signing in with the CredentialsProvider we get something similar. Token, trigger, account and user will be populated. User is what we just returned from `authorize` and account is this:

```jsx
account: {
  providerAccountId: undefined,
  type: 'credentials',
  provider: 'credentials'
},
```

Again, similarly to what we did using the GoogleProvider we listen for account.provider inside the `jwt callback`. Why? When account is defined and `account.provider === 'credentials'`, we know that a user just signed in with credentials and we need to update the token with this data. This is our updated `jwt callback`:

```jsx
async jwt({ token, trigger, account, user, session }) {
  if (account) {
    if (account.provider === 'google') {
      // we now know we are doing a sign in using GoogleProvider
      try {
        const strapiResponse = await fetch(
          `${process.env.STRAPI_BACKEND_URL}/api/auth/${account.provider}/callback?access_token=${account.access_token}`,
          { cache: 'no-cache' }
        );
        if (!strapiResponse.ok) {
          const strapiError: StrapiErrorT = await strapiResponse.json();
          throw new Error(strapiError.error.message);
        }
        const strapiLoginResponse: StrapiLoginResponseT =
          await strapiResponse.json();
        // customize token
        // name and email will already be on here
        token.strapiToken = strapiLoginResponse.jwt;
        token.strapiUserId = strapiLoginResponse.user.id;
        token.provider = account.provider;
        token.blocked = strapiLoginResponse.user.blocked;
      } catch (error) {
        throw error;
      }
    }
    if (account.provider === 'credentials') {
      // for credentials, not google provider
      // name and email are taken care of by next-auth or authorize
      token.strapiToken = user.strapiToken;
      token.strapiUserId = user.strapiUserId;
      token.provider = account.provider;
      token.blocked = user.blocked;
    }
  }
  return token;
},
```

By default, `NextAuth` will populate token with name and email properties. We then manually set the other properties.

Try to feel the auth flow here. When `account.provider === 'google'`, we make an api call to `Strapi` inside the `jwt callback`. We then use the strapiResponse to populate the token. When `account.provider === 'credentials'`, we already made the api call to `Strapi` inside the `authorize` function. This data then gets send along to the `jwt callback` via the user object. We then use this user object to populate the token. That is why the credentials part of the `jwt callback` is so simple.

## Customizing the NextAuth session callback for the CredentialsProvider

As you can see above, the token object returned by the `jwt callback` has the same properties for both google as credentials provider. We carefully and intentionally made it so. This means that the `session callback` does not need to be updated.

```jsx
async session({ token, session }) {
  session.strapiToken = token.strapiToken;
  session.provider = token.provider;
  session.user.strapiUserId = token.strapiUserId;
  session.user.blocked = token.blocked;
  return session;
},
```

## Shapes and Types

I just stated that I carefully and intentionally shaped all the callback arguments and the `authorize` function. This is a messy process. The baseline is that we mirror all these arguments between our credentials and google provider.

When we were setting up our GoogleProvider we already [struggled with setting up types](https://dev.to/peterlidee/using-nextauth-googleprovider-to-put-a-user-into-the-strapi-database-3eeg). Adding our CredentialsProvider made everything a bit more complex. Here is a couple of things I had to do.

1. By default the user object in `NextAuth` has some properties: name and email (optional) but also an id (required). That is why in the `authorize` function, I returned an id property: `id: data.user.id.toString()`. This is our `Strapi user id` (number). `NextAuth user id` is a string so we converted it. We don't actually use this id but it does throw a TypeScript error if we don't add an id property. This was my solution. As I said, messy.

2. A second problem I encountered was with the user Type. When handling the GoogleProvider inside the `jwt callback`, we grab strapiToken and strapiUserId from the strapiResponse. But, when using the CredentialsProvider, we make this api call in the `authorize` function and return the data from this function as the user object. This means that we have to use our user object to pass the strapiToken and strapiUserId. To keep TypeScript happy, we have to update our user Type:

```ts
// frontend/src/types/nextauth/next-auth.d.ts

interface User extends DefaultSession['user'] {
  // not setting this will throw ts error in authorize function
  strapiUserId?: number;
  strapiToken?: string;
  blocked?: boolean;
}
```

This means that now, both our User and JWT interfaces have an optional strapiUserId and strapiToken property. There is no way around this (TypeScript keeps yelling) and it is messy. Don't worry if you don't fully understand this. Once you start coding this yourself, it will make sense.

## Summary

We started by coding a form with controlled input fields. Onsubmit we called the `signIn` function and passed the credentials. This leads to the `authorize` function that is defined in the CredentialsProvider.

`Authorize` is an integrated part of the callbacks in `NextAuth` when using credentials. Inside `authorize`, we retrieve our user data from `Strapi` and then return these (edited) data. This return value equals the user argument in our callbacks. To finalize the flow, we updated our `jwt callback`.

Our app as it stands right now, will work with credentials. When we run it, sign in with credentials and log `useSession` or `getServerSession` we get what we expect:

```jsx
{
  user: {
    name: 'Bob',
    email: 'bob@example.com',
    image: undefined,
    strapiUserId: 2,
    blocked: false
  },
  strapiToken: 'longtokenhere',
  provider: 'credentials'
}
```

But, we skipped over a lot of things. In the `authorize` function we have no error handling for the `Strapi` api call. On top of that, our client-side code (the form) is also unfinished: we need error and success handling, input validation and loading states. We will deal with this in the next chapter.

---

If you want to support my writing, you can [donate with paypal](https://www.paypal.com/donate/?hosted_button_id=4D78YQU4V5NEJ).