Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Peter Jacxsens
Peter Jacxsens

Posted on • Edited on

     

10/ NextAuth CredentialsProvider: signing in

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 ongithub: branch credentialssignin.

Setup

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

Content Manager > Collection Types > User > Create a new entry
Enter fullscreen modeExit fullscreen mode

2 notes here:

  1. We're creating a frontend user, not a backend user (one that can access ourStrapi 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:

// 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'},},asyncauthorize(credentials,req){console.log('calling authorize');returnnull;},}),],}
Enter fullscreen modeExit fullscreen mode

We have name, credentials and authorize properties. The name and credentials properties are actually mostly useless. These serve to populate the defaultsign in page thatNextAuth generates. Since we use a customsign 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 tohttp://localhost:3000/api/auth/signin:

NextAuth default signin credentials

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 theasync 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 customsign in page has the same keys (form names and id's).

Finally, theauthorize 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 />:

// frontend/src/components/auth/signin/SignInForm.tsxexportdefaultfunctionSignInForm(){return(<formmethod='post'className='my-8'><divclassName='mb-3'><labelhtmlFor='identifier'className='block mb-1'>          Email or username *</label><inputtype='text'id='identifier'name='identifier'requiredclassName='bg-white border border-zinc-300 w-full rounded-sm p-2'/></div><divclassName='mb-3'><labelhtmlFor='password'className='block mb-1'>          Password *</label><inputtype='password'id='password'name='password'requiredclassName='bg-white border border-zinc-300 w-full rounded-sm p-2'/></div><divclassName='mb-3'><buttontype='submit'className='bg-blue-400 px-4 py-2 rounded-md disabled:bg-sky-200 disabled:text-gray-500'>          sign in</button></div></form>);}
Enter fullscreen modeExit fullscreen mode

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

Calling NextAuth signIn function

We know what to do next because we did the same with oursign in with Google button. We have to call ourNextAuthsignIn function with some arguments:

signIn('credentials',{identifier:'...',password:'...',});
Enter fullscreen modeExit fullscreen mode

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

At this point, you may be thinking about using a server action to handle our form submit. This not possible becausesignIn 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:

// add initialStateconstinitialState={identifier:'',password:'',};// set stateconst[data,setData]=useState(initialState);// create an event handlerfunctionhandleChange(e:React.ChangeEvent<HTMLInputElement>){setData({...data,[e.target.name]:e.target.value,});}
Enter fullscreen modeExit fullscreen mode

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

Next, create an onsubmit handler and call thesignIn function:

functionhandleSubmit(e:React.FormEvent<HTMLFormElement>){e.preventDefault();signIn('credentials',{identifier:data.identifier,password:data.password,});}
Enter fullscreen modeExit fullscreen mode

At this point, our request leaves our form component and theauthorize andcallback 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

Theauthorize 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 askStrapi if these data are correct via an api call.

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

It's best to look atauthorize as another of theNextAuth callback functions because it actually is a callback function. The return value fromauthorize is passed into the other callbacks as the user argument. The user argument in thejwt callback is the return value from theauthorize function.

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

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

Strapi API

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

conststrapiResponse=awaitfetch(`${process.env.STRAPI_BACKEND_URL}/api/auth/local`,{method:'POST',headers:{'Content-type':'application/json',},body:JSON.stringify({identifier:credentials.identifier,password:credentials.password,}),});
Enter fullscreen modeExit fullscreen mode

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

asyncauthorize(credentials,req){conststrapiResponse=awaitfetch(`${process.env.STRAPI_BACKEND_URL}/api/auth/local`,{method:'POST',headers:{'Content-type':'application/json',},body:JSON.stringify({identifier:credentials!.identifier,password:credentials!.password,}),});constdata:StrapiLoginResponseT=awaitstrapiResponse.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 modeExit fullscreen mode

Inside ourNextAuth flow, once we return data fromauthorize, the callbacks get called. ThesignIn callback will just return true and is not relevant. Thejwt callback will be called next.

Customizing the NextAuth jwt callback for the CredentialsProvider

From working with GoogleProvider, we learned that when we sign in, thejwt 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 fromauthorize and account is this:

account:{providerAccountId:undefined,type:'credentials',provider:'credentials'},
Enter fullscreen modeExit fullscreen mode

Again, similarly to what we did using the GoogleProvider we listen for account.provider inside thejwt callback. Why? When account is defined andaccount.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 updatedjwt callback:

asyncjwt({token,trigger,account,user,session}){if(account){if(account.provider==='google'){// we now know we are doing a sign in using GoogleProvidertry{conststrapiResponse=awaitfetch(`${process.env.STRAPI_BACKEND_URL}/api/auth/${account.provider}/callback?access_token=${account.access_token}`,{cache:'no-cache'});if(!strapiResponse.ok){conststrapiError:StrapiErrorT=awaitstrapiResponse.json();thrownewError(strapiError.error.message);}conststrapiLoginResponse:StrapiLoginResponseT=awaitstrapiResponse.json();// customize token// name and email will already be on heretoken.strapiToken=strapiLoginResponse.jwt;token.strapiUserId=strapiLoginResponse.user.id;token.provider=account.provider;token.blocked=strapiLoginResponse.user.blocked;}catch(error){throwerror;}}if(account.provider==='credentials'){// for credentials, not google provider// name and email are taken care of by next-auth or authorizetoken.strapiToken=user.strapiToken;token.strapiUserId=user.strapiUserId;token.provider=account.provider;token.blocked=user.blocked;}}returntoken;},
Enter fullscreen modeExit fullscreen mode

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. Whenaccount.provider === 'google', we make an api call toStrapi inside thejwt callback. We then use the strapiResponse to populate the token. Whenaccount.provider === 'credentials', we already made the api call toStrapi inside theauthorize function. This data then gets send along to thejwt callback via the user object. We then use this user object to populate the token. That is why the credentials part of thejwt callback is so simple.

Customizing the NextAuth session callback for the CredentialsProvider

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

asyncsession({token,session}){session.strapiToken=token.strapiToken;session.provider=token.provider;session.user.strapiUserId=token.strapiUserId;session.user.blocked=token.blocked;returnsession;},
Enter fullscreen modeExit fullscreen mode

Shapes and Types

I just stated that I carefully and intentionally shaped all the callback arguments and theauthorize 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 alreadystruggled with setting up types. 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 inNextAuth has some properties: name and email (optional) but also an id (required). That is why in theauthorize function, I returned an id property:id: data.user.id.toString(). This is ourStrapi 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 thejwt callback, we grab strapiToken and strapiUserId from the strapiResponse. But, when using the CredentialsProvider, we make this api call in theauthorize 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:

// frontend/src/types/nextauth/next-auth.d.tsinterfaceUserextendsDefaultSession['user']{// not setting this will throw ts error in authorize functionstrapiUserId?:number;strapiToken?:string;blocked?:boolean;}
Enter fullscreen modeExit fullscreen mode

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 thesignIn function and passed the credentials. This leads to theauthorize function that is defined in the CredentialsProvider.

Authorize is an integrated part of the callbacks inNextAuth when using credentials. Insideauthorize, we retrieve our user data fromStrapi and then return these (edited) data. This return value equals the user argument in our callbacks. To finalize the flow, we updated ourjwt callback.

Our app as it stands right now, will work with credentials. When we run it, sign in with credentials and loguseSession orgetServerSession we get what we expect:

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

But, we skipped over a lot of things. In theauthorize function we have no error handling for theStrapi 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 candonate with paypal.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Front-end developer
  • Location
    Belgium
  • Education
    self taught
  • Joined

More fromPeter Jacxsens

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp