In this lesson, we'll cover, from start to finish, how to create your own password reset (or forgot password) flow in your NodeJS application with AdonisJS.
🔓 Learn How To Implement Authentication in 15 Minutes
🥁 Subscribe To Stay Notified For New Lessons
📺 Watch on YouTube
👨💻 Find The Code For This Lesson Here
📚 Chapters:
- Intro
- Setup Token & Mail
- Creating Token Methods
- Forgot Password (1st half of flow)
- Testing Forgot Password
- Reset Password (2nd half of flow)
- Testing Reset Password
- Outro
📜 Transcript:
in this lesson we're going to learn how
to implement a password reset flow and
in order to do this we're going to use
our user role project we've been working
with the past couple of lessons so to
start with what we're going to want to
do is add a token migration and model to
our application so let's go ahead and
dive into our terminal here and let's do
node Ace make model and let's call this
token and let's do hyphen M to also
create a migration for that while we're
here within our terminal let's go ahead
and install and configure the mail
provider within Adonis so npmi adonisjs
slash mail and then node Ace configure
at adonisjs slash mail so we're actually
going to use the dotsjs mail to send out
the password reset link to our users so
I'm going to be using SMTP utilizing
mailtramp you feel free to select
whatever driver is applicable to you and
then I'll take my instructions in the
terminal I'm going to go ahead and copy
the SMTP driver emvts variables here and
I'm going to jump into my project under
emv.ts and plop those right in there
next I'm going to go into mailtrap and
actually grab those properties so I'm
going to jump into my browser hit show
credentials I'm going to wipe this
bucket out after I am done with this
lesson so I'm not worried about these
getting out and then I'm going to jump
into my DOT EMV and let's see SMTP host
should be
smtp.mail trap.io and you want to grab
these instructions from whatever service
you are utilizing since I'm using
mailtrap that's why my host will be
towards mailtrap.io grab my username and
my password and once we have that in
there we should be good to go next let's
define our tokens migration so we're
going to want a couple of additional
columns here so we'll do table dot
integer let's bind this to a user so
user ID unsigned references ID in table
users and then on delete let's go ahead
and Cascade next let's go ahead and do
table dot string and allow a type to be
defined I'm going to set this to not
nullable and then table dot string and
this will be our actual token value and
let's limit that to 64 characters long
and set that they're not nullable as
well lastly let's go ahead and do table
Dot timestamp expires at use TZ is true
and we'll leave that as nullable so this
is actually fairly close to the API
tokens table that Adonis will generate
should you select API within your off
configuration step so if you already
have an API tokens table you can utilize
that table instead of creating your own
here but this will serve us since we are
not utilizing the API token structure
within our application next let's go
ahead and jump into our model and let's
define those columns so at column public
user ID that'll be a number or it will
be null let's go ahead and Define that
relationship as well so at belongs to
user public user is belongs to type of
user then we have a column public type
of string and then a column
public token of string and lastly let's
bind this token to our user from our
user model as well so underneath our
belongs to for our role let's do has
many
token
public tokens
has many
type of token and then to actually make
this token relationship usable for other
things Beyond just the password reset
let's go ahead and add in an additional
relationship definition so it has many
token let's add in an additional
configuration here for on query and
we'll get back our query and query.where
the type is specifically password reset
so anytime that we actually create a
password reset token we will give the
type a value of password reset so by
limiting our on query to those
particular tokens we can now do public
password reset tokens is has many
type of
token so now we can reach directly for
this password reset token relationship
to create delete and what have you for
our password reset specific tokens all
right awesome so now we have a token
relationship set up to our user and vice
versa so now we can actually go ahead
and easily create tokens specifically
for password reset and to make this even
easier for us let's jump into our token
model here and let's add a couple of
static methods onto this model so let's
do public static async
generate
password reset
token and let's accept a user of type
user
or null let's also jump up to the top of
our file here and import the string
utilities from at ioc Adonis core
helpers and we can utilize one of these
string utilities to actually generate
out our crypto safe random string so we
can do const token equals string dot
Generate random which will generate a
cryptography safe random string we can
give this a size of 64. and so now we
actually have the token value that we'll
save to our token record anytime that we
want to generate out a password reset
token so next let's do if not user so if
a user is not provided and we'll just
return back this token otherwise if a
user is provided then we will actually
go ahead and persist our token to our
user so let's do const record equals 08
user dot related and since it doesn't
matter whether or not we have that
additional wear statement for our
password reset tokens versus just our
tokens we can go ahead and just specify
tokens here
and then create provided our type of
password reset set the ex oh we forgot
to actually Define this on the model so
let's do at column date time public
expires at date time or null and so for
all of our password reset tokens that we
create we'll set them to expire at date
time dot now dot plus hour one so have
them expire one hour from whatever time
the tokens generated at and then we'll
go ahead and actually provide the token
value once we have that we can return
our record dot token so we're returning
back just the actual generated random
string token value instead of the entire
token record because at the end of the
day we're not going to actually need the
entire record just the token value in
addition to this before we actually
generate out a token we can expire any
pre-existing tokens so we can either do
that via deleting the token itself or we
can update its expires app it's up to
you on whether or not you actually want
to persist those records into your
database it's easier to delete so we'll
go ahead and cover that a little bit
harder approach by updating our expires
app so let's do public static async
expire
password
reset tokens we'll accept our user at
this point we can verify that we only
have a user and it will be null then we
can do awaituser.related password reset
tokens dot query dot update and we can
set the expires at to date time dot now
so we're getting all of the password
reset tokens for this user and then
we're specifying that we want to update
their expires at column to right now so
essentially since right now we'll be in
the past one millisecond from now this
is essentially expiring them if you want
to be extra safe you could set this to
one second in the past but this will
suffice and then let's come back up into
our generate password reset token and
right before we actually generate out
our new token let's await token dot
expired password reset tokens or the
provided user next we'll add in a method
to actually grab the user from the token
so this will be the opposite of our
generate password reset token this will
take in the token instead of the user
and then instead of returning back the
token it will return back the user that
the token belongs to so let's do public
so static async git password reset user
take in the token that'll be of type
string and then we can do const record
equals await token.query Dot and let's
preload the user from the relationship
where token is token where expires at is
greater than our date time dot now and
let's specify that to be to SQL just for
safety sake let's order by created at
descending and we can grab the first
matching record if there is one and then
we'll return record
and the user off of that using knowledge
coalesce just in case a record is not
found and then lastly we can go ahead
and add in a verification method so that
we can take in a token and verify
whether or not it belongs to a user
exists and whether or not it's expired
so let's do public static async verify
this will be specific to password reset
tokens so we can just name it a little
bit more vague as just verified take on
a token of type string and let's do
const record equals awaits token dot
query where expires at is greater than
datetime.now.to SQL dot where token is
the token value and then grab the first
matching record if there is one and then
let's return let's cast it to a Boolean
using bang bang and then return back the
record okay so now we have everything
done within our token model this is
pretty much all of the apis that we'll
need to actually generate out verify and
then get back our user from our password
reset tokens now if you have a lot of
different variations of tokens that
you'll want to create within this token
model it might behoove you to move these
methods off into a service instead of
directly on the model but for right now
these will suffice for our use case and
it makes the API just to look a little
bit simple let's go ahead and rig up a
controller to actually start
implementing this let's jump back into
our terminal let's clear this out and
let's do node Ace make controller and
let's call this password reset
controller pass in hyphen e to make sure
that that naming is exact because it
will want to rename it to password
resets controller all right there we go
let's jump into here and let's start off
with our Imports because we're going to
need to import a couple of things here
so let's import mail from ioc Adonis
add-ons
mail let's import route from at ioc
Adonis core route let's import EnV from
at ioc Adonis core EMV and then the rest
should Auto Import without issue it does
look like typescript is not picking up
that we have configured our mail driver
so let's go ahead and just restart the
typescript server there and there we go
okay no more red squiggly So within this
controller we're going to want four
different methods so let's do public
async forgot assign this to our HTTP
context contract and let's copy this so
we'll want one for send another for
reset and the last one for actually
storing the reset so this will be send
to send out the forgot password email
reset to display the reset form and then
store to actually store the password
reset change you could also name that
update if you feel that's a little bit
more applicable so let's start at the
top here forever forgot this is going to
be in charge of actually displaying our
forgot password form so this is where
the user will insert their email all we
need for this is our view and then we
can return view dot render password dot
forgot and then let's go create that
view so let's jump into our terminal
node Ace make view password slash
forgot while we're here let's also
create one for reset so node Ace make
view password slash reset all right
let's jump back into our code base jump
down into those files and let's do at
layout layout slash app at section is
content at end section do a div class
column we'll create that class here
momentarily we'll have an H1 reset your
password enter your email below and
we'll send you a password reset link and
for this we will have a form method will
be post action we'll rig this up here in
a second but this will be route
passwords.send and within here we will
have a single input type equals email
name equals email
and we'll do a placeholder of enter your
email then we'll have a button type
equals submit and reset password as the
text for that so we can give that a save
we can copy all of this and then jump
into our reset.edge file and paste all
of this and then we can do enter your
new password below
we'll change email here to password new
password there we go and then we're also
going to want to actually append in a
token into this form as well so the
input type equals hidden name of token
and value of and we'll provide this into
the view here momentarily token and we
can give that a save as well okay so
let's jump back into our password reset
controller and talk about the flow of
this so as you might have guessed first
the user will reach our actual login
page where they'll realize that they
forgot their password they'll hit the
forgot password link and then they'll
jump into our password forgot page right
here where they'll enter in their email
address they'll kick this off to our
password send route which will be within
our password reset controller again
where we will generate out that token
persist it to the user if once found for
the email that they provided and then
we'll kick off that email where they
will then click the link in the email
jump back into this reset file where we
will then verify that the token is still
valid and then if it's valid we'll show
them a form where they can enter in
their new password that will also send
up the token along with that form to the
store file where we will lastly verify
the token one more time and then make
the change onto the user's password so
we have all of the apis rigged up for
the actual token generation verification
and all that fun stuff within our token
model already so all that we need to do
is really rig it up so so within our
send and I can't believe this but I
forgot to import we'll need to import
the validator as well so we'll do schema
and rules from at ioc Adonis core
validator and then we will want to
validate that the user is actually
sending in a valid email so we'll do
request response and session out of our
HTTP context we'll do const email schema
equals schema.create email schema.string
rules of just email now you don't want
to verify that the email actually exists
here because you want to be very vague
about whether or not the email exists
within your system we'll attempt to find
a user via the email that they've
provided and if we can't we won't let
them know because at the end of this
request all that we're going to do is do
a session.flash we'll give them a
success message regardless and we'll say
if an account match inches the provided
email you will receive a password reset
link shortly and then return response
dot redirect then back and the reason
you want to be vague here is because you
don't want people guessing at what
emails you have within your system
because you were telling the user
whether or not an email existed within
your system they could try out any old
email like tested apple.com find out
whether or not that's within your system
if it is then they could try to Brute
Force log in a little bit more easily so
being very vague about this here gives a
Shadow of Doubt on whether or not that
email actually exists within your system
so once we have the email schema we'll
want to validate it so we can do const
to get back the validated email equals
awaiterequest dot validate schema of
email schema once we have the email we
can do const user equals await user Dot
bindby
attempt to find them by that email and
it will generate out a token for this
regardless so token equals awaits token
and then we'll call generate password
reset token and provide it in the user
now remember within this method we are
only actually persisting the token to
our database if that user was found if
not all we're doing is generating out
any old random token and returning it
back at the end of the day we're not
creating any records within our database
if the user was not found and then we'll
want to generate out a link to our
password reset page so let's do const
reset link equals Rel dot make URL you
can make this assigned URL if you wish
password dot reset and provide it in
that token and then if we found the user
then we want to await mail dot send or
send later whichever you wish message
equals message dot ROM wherever is
applicable for your use case so I'm
going to do no reply at atticast.com to
user.email with the subject reset your
password and then provide it the HTML
you can do this as an HTML view if you
have a more structured HTML that you
want to send but I'm just going to send
reset your password by clicking actually
we'll put clicking here within the link
so we'll do a href clicking here and
then end that anchor and for our actual
href we're going to want the full path
not just a relative path so we'll want
our domain so to make this a little bit
easier on us let's jump into our EMV and
add in a domain property and set this to
http semicolon slash we'll grab our host
colon and then our Port so it will be
http000 and then our Port of 3333 or
whatever Port our server ends up booting
up with now let's also add that within
our emv.ts down here so domain Envy
schema string give that a save jump back
into our password reset controller and
now for our href we can grab
env.get domain and then provide it our
reset link and that's all that we should
need for our send method and we can
actually try that out before we move on
any further to make sure that we have
thus far working okay before we do that
let's jump into our lab dot app and add
in that column class that we added in
earlier display Flex
Flex flow of column just going to make
it go into a column instead of a row and
then let's also jump into our
welcome.edge and add a link to our
password reset page so where we have our
login form I'm going to put this inside
of a div and we do a href route password
dot forgot forget your password and
there we go okay and then I don't
actually think we defined our routes yet
so let's go ahead and Define those two
so routes no we did not all right so we
have
route.getpassword forgot password reset
controller dot forgot is password forgot
I'm going to copy this because I'm
having a little bit of trouble typing
password you can also put this inside of
a group so we have this one here will be
a post for send and it will be kicked
off to send as password.send and then
we'll have reset reset and reset and
then we'll have store store and store
again you can name that update or
whatever you find applicable for that
but we also need to make this a post or
a pack or put whatever you find
applicable for that I'll just stick with
a post to make things simpler all right
and then let's go ahead and migrate our
database so let's do node Ace migration
and now you can do migrate rowback
refresh reset I'm going to do fresh to
just clear everything out and then
re-migrate what we have so we'll just
drop all the tables and then run a fresh
migration set and the reason that we
wanted to run that was to pick up our
tokens migration that we created at the
start of this lesson so once we have
that let's go ahead and test out what we
have so far so let's do npm run Dev to
boot up our server and then let's open
that up and then since we just ran our
fresh migration if you've cleared
everything out you'll need to create a
new user so let's do Test Plus user
gmail.com give it some password I
actually don't know what I just typed in
so hopefully this does work so let's go
forget your password uh let's enter in
our email so Test Plus user gmail.com
send it off and actually let's jump into
that view because we didn't give
ourselves any indication on whether or
not we got a validation error or
anything of the sort and let's do flash
messages dot all and let's just inspect
that let's also add that to our reset
page we're thinking about that so flash
messages.all okay good jump back into
our browser here let's try kicking that
off one more time just to make sure that
we get what we wanted back at gmail.com
all right good remember we're sending
this back regardless of what happens
it's a little bit hard to read there but
it does say if an account matches your
provided email you will receive the
password reset link shortly the only way
that this would not be received is if we
failed the validation for the email
check that we did so let's go check out
mailtrap and we do have our two emails
the most latest one is right here so we
have reset your password by clicking
here and it does go back to our
localhost 3333 password slash reset you
can actually click on that to verify
that we don't have this route set up yet
so we get nothing and now since we've
sent this twice we should actually be
able to verify that our previous token
was nullified successfully so if we
actually jump into our table plus here
take a look at our tokens the first
token that we generated right here
should actually expire at the present
point in time so you'll have to take my
word for it but it is 502 or it's 503
now so this is now expired out and the
other one is valid until 602. so we gen
generated those pretty close back to
back there was definitely not an hour
within there so it did successfully
expire out our first token that we
generated so that is working A-Okay so
it looks like everything's going good so
far so let's go ahead and finish what we
have so let's jump back into our
password reset controller and let's
finish up our reset and store so for
reset we will want our view and our
params from our HTTP context we'll want
to do const token equals params dot
token and then const is valid to check
whether or not our tokens valid as a
weight
token.verify and then provide it in the
token so remember whenever we set this
up this will check whether or not the
token actually exists within our
database and whether or not it is still
valid so this is just a nice little
pre-check for our user to see whether or
not their token has expired before they
actually attempt to reset their password
now there is always that chance that the
token will expire between these two
steps if you want to you can give them a
little bit of buffer with your
verification step but that's completely
up to you so once we have these two
we'll just return view.render our
password slash reset page and provided
that is valid check as well as the token
itself and then for that reset page
we'll want to wrap this up in a if to
check whether or not it is valid so at
if is valid at else
and then end area so if it is valid
that's where we want to display our
password reset form if it is invalid
then we'll want to give them some type
of notification that their token is
invalid and you'll need to run through
the flow again so we'll do your token is
invalid or expired please try again
we'll do that for right now a href route
password dot forgot to send them back to
the start of this flow and then we can
actually put please try again I guess
within that link content okay so now we
won't see this at all if our token is
valid and we will only see this if our
token is valid all right so now we
should be able to jump back into
mailtrap here and give this a clicker
room to test and see oops okay so our
token's actually not being appended onto
this URL at all so that's not good let's
go fix that up so I actually think
that's just a param issue so whenever we
Define the routes I did indeed forget
the token so password reset is the URL
that's being generated within our send
method so this is the one that will want
token to be appended on as a route
parameter so if we give that a save now
we'll need to run through the flow again
ourselves so let's jump back into this
page do test plus user gmail.com
send that off we should now have a fresh
password email where we now have yep
there we go that one's got a token on it
so now we can click on this we can get
this page here and now you can see that
this tokens valid so if we actually run
through that flow one more time test
plus user gmail.com there we go and
click on the newly generated URL this
one will be valid but if we refresh the
one that we had previously just tested
it should now be invalid and there you
go your tokens invalid or expired so
again we've just checked and verified
that our tokens are being expired
whenever a new one is generated so last
thing that we need to do is verify that
the token's still valid at the time that
this form here is kicked off and
actually persist our users password
change
so let's jump back into our password
reset controller underneath store here
let's grab our request response session
and auth out of our HTTP context so
let's do const password schema equals
schema dot create token so we want to
verify that the token is actually being
provided and it is still a string and
then we want to verify that our user has
submitted a password so schema.string
and then you want to make sure that you
use the same rules that you're using for
your actual registration step for your
password Here I believe all that we're
doing is a Min length check of verifying
that it's at least eight characters so
we should be good there and we do const
token and password out of our request
validate schema as password schema and
then const user equals await and we can
reach for our token again to get
password reset user and provided in the
token so we'll get the actual user that
belongs to this token and then we can do
if not user so if we did not get a user
back for that token then we want to
session.flash error token X expired or
Associated user could not be found
return response redirect and redirect
them back otherwise we can await user
dot merge merge in their password change
and then save that off to the user
record and then this is completely up to
you you can send them back through their
login flow if you wish but since we've
already gone through the step of making
them go into their email click the link
to get back into here to reset their
password if they can reset their
password I think I'm okay to say they
can go ahead and be logged in so let's
do auth.log in and provided that user
record and then return response redirect
to path and point them to the home page
so we give that a save and now we should
have our complete password reset flow
rigged up so let's go ahead and dive
back into here and let's try finish
testing the second half of this flow and
let's give them some password
and oh we got there let's see so we got
the token we got the password email okay
so it's I bet you this is rigged up to
email still because we copy and pasted
it so let's dive into our reset.h page
take a look and we have a name of
password oh it's the action okay so
password.send needs to be
password.store give that a save all
right now we should be good let's dive
it back into here and let's let's give
it a refresh and then let's try
resetting our password again so there we
go all right there we go so now we're
actually logged in as our user after we
successfully reset our password so there
is actually one last thing that we're
going to want to do before we call this
lesson a day so let's jump back into our
mail trap here and let's click on the
most recent password reset link that we
just did and you can see that it's still
coming through we're not getting the
this tokens invalid or anything like
that type of message so we could
actually utilize this token again to
reset our password and everything would
go through just fine so what we need to
do is invalidate this token after it's
used so we can easily do that by jumping
back into our store method here after we
actually update their password we can do
another a weight and we can utilize our
expired password reset tokens and
provided in the user and so this will go
through and actually expire out all
password reset tokens not just the
latest one to make sure that everything
is actually expired so it will
invalidate all tokens that the user has
for their password reset flow instead of
just the one so if we come back in here
we can just use this one again to reset
the password all right so we're logged
back in now if we log out come back into
here click on the password reset link
one more time now we get that your
password's invalid or expired message
because we've gone through and
invalidated all of their password reset
tokens so now that should be everything
that we need to do if you want to you
can send them an email similar to how we
did with the password.send route to let
them know that their password has been
successfully changed that's pretty
typical within the password reset flow
but that is going to be pretty much
identical to what we did right on up
here just changing the actual HTML
content so I'll leave that up to you
alright so that should do it that is how
you can actually go about adding in a
password reset flow to your application
[Music]
Join The Discussion! (9 Comments)
Please sign in or sign up for free to join in on the dicussion.
kreek
Hi Tom,
First of all great tutorial. We really appreciate your content.
I encountered a problem while implementing the code in this tutorial.
On the forgot password page, when you input an email to receive the reset link, the
generatePasswordResetToken() fails with the following error:
update
tokens
setexpires_at
= 2022-12-12 19:58:34.371 +00:00 where (`type` = 'PASSWORD_RESET') and (`user_id` = 1) - Unknown column '_zone' in 'field list'It looks like the code is trying to update the column called
expires_at
with a value that includes a time zone offset, but there is no such column in thetokens
table.I had to recreate all the missing database fields for my tokens migrations in order to get it working.
I was hoping you could provide some insights as to why these fields are required.
Thanks and keep up the amazing work.
Please sign in or sign up for free to reply
tomgobich
Hi Kreek!
You shouldn't need to define those columns in your database, did you remember to include the
{ useTz: true }
option to set theexpires_at
column to store the timezone as well? Full column definition would be:Another place to look is to ensure you've set the model type to
DateTime
:Hope this helps!
Please sign in or sign up for free to reply
belovah
I have the same issue here when trying to register a new user or reseting the password on a fresh clone of your repository.
Please sign in or sign up for free to reply
tomgobich
Hmm… I just tried using a fresh clone of the repo and everything is working okay for me, apart from a few missing variables in the
env.example
.Are you using PostgreSQL? If not, you'll need to make sure you re-configure Lucid for the database driver you're using.
Please sign in or sign up for free to reply
theo-lge
I've found a way to correct this bug
Please sign in or sign up for free to reply
cbernard
Hi Tom,
I've been porting what you did here to Adonis 6, but I'm seeing some behaviour that is consistent with what Kreek said a year ago. Update seems to behave differently to create. Any ideas what I'm doing wrong?
Please sign in or sign up for free to reply
tomgobich
Hi cbernard!!
Yeah, assuming you're using MySQL, it appears it doesn't accept zone information and that's the default way Luxon is serialized. It seems like AdonisJS takes care of this when the create/update flow goes through the model, but the model query builder looks to just be passing the update value directly through to KnexJS's update; hence why that particular attempt fails.
So, you should be able to fix your query builder update call by providing a valid MySQL date time format into the
expiresAt
value.Hope this helps!!
Please sign in or sign up for free to reply
cbernard
Thanks Tom.
Please sign in or sign up for free to reply
tomgobich
Anytime!
Please sign in or sign up for free to reply