How To Create A Password Reset Flow in NodeJS with AdonisJS

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.

Published
Nov 20, 22
Duration
26m 17s

Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.

Adocasts

Burlington, KY

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:
00:00 - Intro
00:15 - Setup Token & Mail
03:50 - Creating Token Methods
08:49 - Forgot Password (1st half of flow)
17:50 - Testing Forgot Password
19:44 - Reset Password (2nd half of flow)
24:00 - Testing Reset Password
25:55 - Outro


📜 Transcript:

0:00

in this lesson we're going to learn how

0:01

to implement a password reset flow and

0:03

in order to do this we're going to use

0:04

our user role project we've been working

0:06

with the past couple of lessons so to

0:08

start with what we're going to want to

0:09

do is add a token migration and model to

0:12

our application so let's go ahead and

0:13

dive into our terminal here and let's do

0:15

node Ace make model and let's call this

0:18

token and let's do hyphen M to also

0:20

create a migration for that while we're

0:21

here within our terminal let's go ahead

0:22

and install and configure the mail

0:24

provider within Adonis so npmi adonisjs

0:28

slash mail and then node Ace configure

0:31

at adonisjs slash mail so we're actually

0:34

going to use the dotsjs mail to send out

0:36

the password reset link to our users so

0:39

I'm going to be using SMTP utilizing

0:41

mailtramp you feel free to select

0:42

whatever driver is applicable to you and

0:45

then I'll take my instructions in the

0:46

terminal I'm going to go ahead and copy

0:48

the SMTP driver emvts variables here and

0:52

I'm going to jump into my project under

0:53

emv.ts and plop those right in there

0:55

next I'm going to go into mailtrap and

0:57

actually grab those properties so I'm

0:59

going to jump into my browser hit show

1:01

credentials I'm going to wipe this

1:03

bucket out after I am done with this

1:05

lesson so I'm not worried about these

1:06

getting out and then I'm going to jump

1:08

into my DOT EMV and let's see SMTP host

1:11

should be

1:13

smtp.mail trap.io and you want to grab

1:16

these instructions from whatever service

1:17

you are utilizing since I'm using

1:19

mailtrap that's why my host will be

1:21

towards mailtrap.io grab my username and

1:24

my password and once we have that in

1:26

there we should be good to go next let's

1:28

define our tokens migration so we're

1:30

going to want a couple of additional

1:31

columns here so we'll do table dot

1:33

integer let's bind this to a user so

1:35

user ID unsigned references ID in table

1:39

users and then on delete let's go ahead

1:43

and Cascade next let's go ahead and do

1:45

table dot string and allow a type to be

1:47

defined I'm going to set this to not

1:49

nullable and then table dot string and

1:51

this will be our actual token value and

1:53

let's limit that to 64 characters long

1:55

and set that they're not nullable as

1:57

well lastly let's go ahead and do table

1:59

Dot timestamp expires at use TZ is true

2:03

and we'll leave that as nullable so this

2:05

is actually fairly close to the API

2:06

tokens table that Adonis will generate

2:09

should you select API within your off

2:11

configuration step so if you already

2:13

have an API tokens table you can utilize

2:15

that table instead of creating your own

2:17

here but this will serve us since we are

2:18

not utilizing the API token structure

2:20

within our application next let's go

2:22

ahead and jump into our model and let's

2:24

define those columns so at column public

2:27

user ID that'll be a number or it will

2:31

be null let's go ahead and Define that

2:32

relationship as well so at belongs to

2:36

user public user is belongs to type of

2:41

user then we have a column public type

2:44

of string and then a column

2:47

public token of string and lastly let's

2:50

bind this token to our user from our

2:52

user model as well so underneath our

2:54

belongs to for our role let's do has

2:56

many

2:58

token

2:59

public tokens

3:01

has many

3:03

type of token and then to actually make

3:05

this token relationship usable for other

3:07

things Beyond just the password reset

3:09

let's go ahead and add in an additional

3:11

relationship definition so it has many

3:14

token let's add in an additional

3:17

configuration here for on query and

3:19

we'll get back our query and query.where

3:21

the type is specifically password reset

3:25

so anytime that we actually create a

3:27

password reset token we will give the

3:28

type a value of password reset so by

3:31

limiting our on query to those

3:33

particular tokens we can now do public

3:36

password reset tokens is has many

3:42

type of

3:44

token so now we can reach directly for

3:46

this password reset token relationship

3:47

to create delete and what have you for

3:50

our password reset specific tokens all

3:52

right awesome so now we have a token

3:53

relationship set up to our user and vice

3:56

versa so now we can actually go ahead

3:57

and easily create tokens specifically

3:59

for password reset and to make this even

4:01

easier for us let's jump into our token

4:03

model here and let's add a couple of

4:05

static methods onto this model so let's

4:06

do public static async

4:09

generate

4:11

password reset

4:13

token and let's accept a user of type

4:16

user

4:17

or null let's also jump up to the top of

4:19

our file here and import the string

4:21

utilities from at ioc Adonis core

4:26

helpers and we can utilize one of these

4:28

string utilities to actually generate

4:30

out our crypto safe random string so we

4:32

can do const token equals string dot

4:35

Generate random which will generate a

4:37

cryptography safe random string we can

4:39

give this a size of 64. and so now we

4:41

actually have the token value that we'll

4:43

save to our token record anytime that we

4:45

want to generate out a password reset

4:46

token so next let's do if not user so if

4:49

a user is not provided and we'll just

4:51

return back this token otherwise if a

4:53

user is provided then we will actually

4:54

go ahead and persist our token to our

4:56

user so let's do const record equals 08

4:59

user dot related and since it doesn't

5:01

matter whether or not we have that

5:02

additional wear statement for our

5:03

password reset tokens versus just our

5:05

tokens we can go ahead and just specify

5:06

tokens here

5:07

and then create provided our type of

5:10

password reset set the ex oh we forgot

5:13

to actually Define this on the model so

5:15

let's do at column date time public

5:18

expires at date time or null and so for

5:22

all of our password reset tokens that we

5:23

create we'll set them to expire at date

5:26

time dot now dot plus hour one so have

5:30

them expire one hour from whatever time

5:31

the tokens generated at and then we'll

5:33

go ahead and actually provide the token

5:34

value once we have that we can return

5:36

our record dot token so we're returning

5:39

back just the actual generated random

5:40

string token value instead of the entire

5:43

token record because at the end of the

5:44

day we're not going to actually need the

5:46

entire record just the token value in

5:47

addition to this before we actually

5:49

generate out a token we can expire any

5:50

pre-existing tokens so we can either do

5:53

that via deleting the token itself or we

5:55

can update its expires app it's up to

5:57

you on whether or not you actually want

5:58

to persist those records into your

6:00

database it's easier to delete so we'll

6:01

go ahead and cover that a little bit

6:02

harder approach by updating our expires

6:04

app so let's do public static async

6:07

expire

6:08

password

6:10

reset tokens we'll accept our user at

6:12

this point we can verify that we only

6:13

have a user and it will be null then we

6:15

can do awaituser.related password reset

6:18

tokens dot query dot update and we can

6:22

set the expires at to date time dot now

6:25

so we're getting all of the password

6:26

reset tokens for this user and then

6:28

we're specifying that we want to update

6:30

their expires at column to right now so

6:32

essentially since right now we'll be in

6:34

the past one millisecond from now this

6:36

is essentially expiring them if you want

6:37

to be extra safe you could set this to

6:39

one second in the past but this will

6:40

suffice and then let's come back up into

6:42

our generate password reset token and

6:44

right before we actually generate out

6:45

our new token let's await token dot

6:49

expired password reset tokens or the

6:52

provided user next we'll add in a method

6:54

to actually grab the user from the token

6:56

so this will be the opposite of our

6:57

generate password reset token this will

6:59

take in the token instead of the user

7:01

and then instead of returning back the

7:02

token it will return back the user that

7:04

the token belongs to so let's do public

7:05

so static async git password reset user

7:10

take in the token that'll be of type

7:12

string and then we can do const record

7:14

equals await token.query Dot and let's

7:18

preload the user from the relationship

7:20

where token is token where expires at is

7:26

greater than our date time dot now and

7:29

let's specify that to be to SQL just for

7:32

safety sake let's order by created at

7:35

descending and we can grab the first

7:37

matching record if there is one and then

7:39

we'll return record

7:41

and the user off of that using knowledge

7:43

coalesce just in case a record is not

7:45

found and then lastly we can go ahead

7:46

and add in a verification method so that

7:48

we can take in a token and verify

7:50

whether or not it belongs to a user

7:51

exists and whether or not it's expired

7:53

so let's do public static async verify

7:56

this will be specific to password reset

7:58

tokens so we can just name it a little

8:00

bit more vague as just verified take on

8:02

a token of type string and let's do

8:04

const record equals awaits token dot

8:07

query where expires at is greater than

8:12

datetime.now.to SQL dot where token is

8:16

the token value and then grab the first

8:18

matching record if there is one and then

8:20

let's return let's cast it to a Boolean

8:22

using bang bang and then return back the

8:24

record okay so now we have everything

8:25

done within our token model this is

8:27

pretty much all of the apis that we'll

8:28

need to actually generate out verify and

8:31

then get back our user from our password

8:33

reset tokens now if you have a lot of

8:35

different variations of tokens that

8:36

you'll want to create within this token

8:39

model it might behoove you to move these

8:41

methods off into a service instead of

8:43

directly on the model but for right now

8:45

these will suffice for our use case and

8:47

it makes the API just to look a little

8:49

bit simple let's go ahead and rig up a

8:50

controller to actually start

8:51

implementing this let's jump back into

8:53

our terminal let's clear this out and

8:54

let's do node Ace make controller and

8:57

let's call this password reset

8:59

controller pass in hyphen e to make sure

9:02

that that naming is exact because it

9:04

will want to rename it to password

9:05

resets controller all right there we go

9:07

let's jump into here and let's start off

9:08

with our Imports because we're going to

9:10

need to import a couple of things here

9:11

so let's import mail from ioc Adonis

9:16

add-ons

9:17

mail let's import route from at ioc

9:21

Adonis core route let's import EnV from

9:26

at ioc Adonis core EMV and then the rest

9:31

should Auto Import without issue it does

9:33

look like typescript is not picking up

9:35

that we have configured our mail driver

9:37

so let's go ahead and just restart the

9:38

typescript server there and there we go

9:40

okay no more red squiggly So within this

9:41

controller we're going to want four

9:42

different methods so let's do public

9:44

async forgot assign this to our HTTP

9:46

context contract and let's copy this so

9:48

we'll want one for send another for

9:50

reset and the last one for actually

9:52

storing the reset so this will be send

9:54

to send out the forgot password email

9:56

reset to display the reset form and then

9:59

store to actually store the password

10:00

reset change you could also name that

10:02

update if you feel that's a little bit

10:03

more applicable so let's start at the

10:05

top here forever forgot this is going to

10:06

be in charge of actually displaying our

10:08

forgot password form so this is where

10:09

the user will insert their email all we

10:11

need for this is our view and then we

10:13

can return view dot render password dot

10:16

forgot and then let's go create that

10:19

view so let's jump into our terminal

10:20

node Ace make view password slash

10:24

forgot while we're here let's also

10:26

create one for reset so node Ace make

10:28

view password slash reset all right

10:31

let's jump back into our code base jump

10:33

down into those files and let's do at

10:35

layout layout slash app at section is

10:40

content at end section do a div class

10:44

column we'll create that class here

10:46

momentarily we'll have an H1 reset your

10:50

password enter your email below and

10:54

we'll send you a password reset link and

10:59

for this we will have a form method will

11:01

be post action we'll rig this up here in

11:04

a second but this will be route

11:07

passwords.send and within here we will

11:09

have a single input type equals email

11:11

name equals email

11:14

and we'll do a placeholder of enter your

11:17

email then we'll have a button type

11:19

equals submit and reset password as the

11:23

text for that so we can give that a save

11:24

we can copy all of this and then jump

11:26

into our reset.edge file and paste all

11:29

of this and then we can do enter your

11:31

new password below

11:35

we'll change email here to password new

11:38

password there we go and then we're also

11:41

going to want to actually append in a

11:43

token into this form as well so the

11:45

input type equals hidden name of token

11:48

and value of and we'll provide this into

11:51

the view here momentarily token and we

11:54

can give that a save as well okay so

11:56

let's jump back into our password reset

11:57

controller and talk about the flow of

11:59

this so as you might have guessed first

12:01

the user will reach our actual login

12:02

page where they'll realize that they

12:04

forgot their password they'll hit the

12:06

forgot password link and then they'll

12:07

jump into our password forgot page right

12:09

here where they'll enter in their email

12:11

address they'll kick this off to our

12:13

password send route which will be within

12:14

our password reset controller again

12:16

where we will generate out that token

12:18

persist it to the user if once found for

12:20

the email that they provided and then

12:22

we'll kick off that email where they

12:23

will then click the link in the email

12:25

jump back into this reset file where we

12:27

will then verify that the token is still

12:29

valid and then if it's valid we'll show

12:31

them a form where they can enter in

12:32

their new password that will also send

12:33

up the token along with that form to the

12:35

store file where we will lastly verify

12:37

the token one more time and then make

12:39

the change onto the user's password so

12:40

we have all of the apis rigged up for

12:42

the actual token generation verification

12:43

and all that fun stuff within our token

12:45

model already so all that we need to do

12:47

is really rig it up so so within our

12:49

send and I can't believe this but I

12:51

forgot to import we'll need to import

12:52

the validator as well so we'll do schema

12:54

and rules from at ioc Adonis core

12:58

validator and then we will want to

13:00

validate that the user is actually

13:02

sending in a valid email so we'll do

13:04

request response and session out of our

13:07

HTTP context we'll do const email schema

13:10

equals schema.create email schema.string

13:13

rules of just email now you don't want

13:16

to verify that the email actually exists

13:18

here because you want to be very vague

13:19

about whether or not the email exists

13:21

within your system we'll attempt to find

13:23

a user via the email that they've

13:24

provided and if we can't we won't let

13:26

them know because at the end of this

13:27

request all that we're going to do is do

13:29

a session.flash we'll give them a

13:31

success message regardless and we'll say

13:33

if an account match inches the provided

13:38

email you will receive a password reset

13:43

link shortly and then return response

13:46

dot redirect then back and the reason

13:49

you want to be vague here is because you

13:50

don't want people guessing at what

13:51

emails you have within your system

13:53

because you were telling the user

13:55

whether or not an email existed within

13:56

your system they could try out any old

13:58

email like tested apple.com find out

14:00

whether or not that's within your system

14:02

if it is then they could try to Brute

14:03

Force log in a little bit more easily so

14:05

being very vague about this here gives a

14:07

Shadow of Doubt on whether or not that

14:08

email actually exists within your system

14:10

so once we have the email schema we'll

14:12

want to validate it so we can do const

14:13

to get back the validated email equals

14:16

awaiterequest dot validate schema of

14:20

email schema once we have the email we

14:23

can do const user equals await user Dot

14:26

bindby

14:27

attempt to find them by that email and

14:29

it will generate out a token for this

14:31

regardless so token equals awaits token

14:34

and then we'll call generate password

14:36

reset token and provide it in the user

14:38

now remember within this method we are

14:39

only actually persisting the token to

14:41

our database if that user was found if

14:44

not all we're doing is generating out

14:45

any old random token and returning it

14:47

back at the end of the day we're not

14:48

creating any records within our database

14:50

if the user was not found and then we'll

14:52

want to generate out a link to our

14:54

password reset page so let's do const

14:55

reset link equals Rel dot make URL you

14:59

can make this assigned URL if you wish

15:00

password dot reset and provide it in

15:03

that token and then if we found the user

15:05

then we want to await mail dot send or

15:09

send later whichever you wish message

15:10

equals message dot ROM wherever is

15:14

applicable for your use case so I'm

15:15

going to do no reply at atticast.com to

15:19

user.email with the subject reset your

15:22

password and then provide it the HTML

15:25

you can do this as an HTML view if you

15:27

have a more structured HTML that you

15:29

want to send but I'm just going to send

15:30

reset your password by clicking actually

15:34

we'll put clicking here within the link

15:36

so we'll do a href clicking here and

15:39

then end that anchor and for our actual

15:41

href we're going to want the full path

15:43

not just a relative path so we'll want

15:45

our domain so to make this a little bit

15:47

easier on us let's jump into our EMV and

15:49

add in a domain property and set this to

15:51

http semicolon slash we'll grab our host

15:54

colon and then our Port so it will be

15:58

http000 and then our Port of 3333 or

16:02

whatever Port our server ends up booting

16:04

up with now let's also add that within

16:05

our emv.ts down here so domain Envy

16:08

schema string give that a save jump back

16:11

into our password reset controller and

16:13

now for our href we can grab

16:15

env.get domain and then provide it our

16:18

reset link and that's all that we should

16:20

need for our send method and we can

16:22

actually try that out before we move on

16:23

any further to make sure that we have

16:25

thus far working okay before we do that

16:27

let's jump into our lab dot app and add

16:29

in that column class that we added in

16:30

earlier display Flex

16:33

Flex flow of column just going to make

16:35

it go into a column instead of a row and

16:37

then let's also jump into our

16:38

welcome.edge and add a link to our

16:40

password reset page so where we have our

16:42

login form I'm going to put this inside

16:44

of a div and we do a href route password

16:48

dot forgot forget your password and

16:52

there we go okay and then I don't

16:54

actually think we defined our routes yet

16:55

so let's go ahead and Define those two

16:56

so routes no we did not all right so we

16:59

have

17:00

route.getpassword forgot password reset

17:03

controller dot forgot is password forgot

17:08

I'm going to copy this because I'm

17:09

having a little bit of trouble typing

17:10

password you can also put this inside of

17:12

a group so we have this one here will be

17:15

a post for send and it will be kicked

17:18

off to send as password.send and then

17:21

we'll have reset reset and reset and

17:24

then we'll have store store and store

17:27

again you can name that update or

17:29

whatever you find applicable for that

17:31

but we also need to make this a post or

17:32

a pack or put whatever you find

17:34

applicable for that I'll just stick with

17:35

a post to make things simpler all right

17:37

and then let's go ahead and migrate our

17:39

database so let's do node Ace migration

17:43

and now you can do migrate rowback

17:45

refresh reset I'm going to do fresh to

17:47

just clear everything out and then

17:49

re-migrate what we have so we'll just

17:51

drop all the tables and then run a fresh

17:52

migration set and the reason that we

17:54

wanted to run that was to pick up our

17:55

tokens migration that we created at the

17:57

start of this lesson so once we have

17:58

that let's go ahead and test out what we

17:59

have so far so let's do npm run Dev to

18:01

boot up our server and then let's open

18:03

that up and then since we just ran our

18:05

fresh migration if you've cleared

18:06

everything out you'll need to create a

18:07

new user so let's do Test Plus user

18:09

gmail.com give it some password I

18:11

actually don't know what I just typed in

18:12

so hopefully this does work so let's go

18:14

forget your password uh let's enter in

18:16

our email so Test Plus user gmail.com

18:19

send it off and actually let's jump into

18:22

that view because we didn't give

18:23

ourselves any indication on whether or

18:25

not we got a validation error or

18:26

anything of the sort and let's do flash

18:28

messages dot all and let's just inspect

18:31

that let's also add that to our reset

18:32

page we're thinking about that so flash

18:35

messages.all okay good jump back into

18:37

our browser here let's try kicking that

18:38

off one more time just to make sure that

18:39

we get what we wanted back at gmail.com

18:42

all right good remember we're sending

18:44

this back regardless of what happens

18:45

it's a little bit hard to read there but

18:47

it does say if an account matches your

18:48

provided email you will receive the

18:50

password reset link shortly the only way

18:52

that this would not be received is if we

18:53

failed the validation for the email

18:55

check that we did so let's go check out

18:56

mailtrap and we do have our two emails

18:58

the most latest one is right here so we

19:00

have reset your password by clicking

19:01

here and it does go back to our

19:03

localhost 3333 password slash reset you

19:05

can actually click on that to verify

19:06

that we don't have this route set up yet

19:08

so we get nothing and now since we've

19:10

sent this twice we should actually be

19:12

able to verify that our previous token

19:13

was nullified successfully so if we

19:16

actually jump into our table plus here

19:17

take a look at our tokens the first

19:19

token that we generated right here

19:20

should actually expire at the present

19:23

point in time so you'll have to take my

19:25

word for it but it is 502 or it's 503

19:27

now so this is now expired out and the

19:29

other one is valid until 602. so we gen

19:32

generated those pretty close back to

19:34

back there was definitely not an hour

19:35

within there so it did successfully

19:37

expire out our first token that we

19:39

generated so that is working A-Okay so

19:41

it looks like everything's going good so

19:42

far so let's go ahead and finish what we

19:44

have so let's jump back into our

19:45

password reset controller and let's

19:47

finish up our reset and store so for

19:49

reset we will want our view and our

19:51

params from our HTTP context we'll want

19:53

to do const token equals params dot

19:56

token and then const is valid to check

19:59

whether or not our tokens valid as a

20:00

weight

20:02

token.verify and then provide it in the

20:04

token so remember whenever we set this

20:05

up this will check whether or not the

20:06

token actually exists within our

20:07

database and whether or not it is still

20:09

valid so this is just a nice little

20:11

pre-check for our user to see whether or

20:12

not their token has expired before they

20:14

actually attempt to reset their password

20:16

now there is always that chance that the

20:17

token will expire between these two

20:18

steps if you want to you can give them a

20:20

little bit of buffer with your

20:21

verification step but that's completely

20:23

up to you so once we have these two

20:24

we'll just return view.render our

20:26

password slash reset page and provided

20:29

that is valid check as well as the token

20:31

itself and then for that reset page

20:32

we'll want to wrap this up in a if to

20:35

check whether or not it is valid so at

20:37

if is valid at else

20:39

and then end area so if it is valid

20:41

that's where we want to display our

20:43

password reset form if it is invalid

20:45

then we'll want to give them some type

20:46

of notification that their token is

20:48

invalid and you'll need to run through

20:49

the flow again so we'll do your token is

20:51

invalid or expired please try again

20:55

we'll do that for right now a href route

20:57

password dot forgot to send them back to

21:00

the start of this flow and then we can

21:01

actually put please try again I guess

21:02

within that link content okay so now we

21:05

won't see this at all if our token is

21:06

valid and we will only see this if our

21:08

token is valid all right so now we

21:10

should be able to jump back into

21:11

mailtrap here and give this a clicker

21:13

room to test and see oops okay so our

21:16

token's actually not being appended onto

21:18

this URL at all so that's not good let's

21:20

go fix that up so I actually think

21:22

that's just a param issue so whenever we

21:24

Define the routes I did indeed forget

21:26

the token so password reset is the URL

21:28

that's being generated within our send

21:30

method so this is the one that will want

21:31

token to be appended on as a route

21:33

parameter so if we give that a save now

21:35

we'll need to run through the flow again

21:36

ourselves so let's jump back into this

21:38

page do test plus user gmail.com

21:42

send that off we should now have a fresh

21:44

password email where we now have yep

21:47

there we go that one's got a token on it

21:48

so now we can click on this we can get

21:50

this page here and now you can see that

21:52

this tokens valid so if we actually run

21:54

through that flow one more time test

21:56

plus user gmail.com there we go and

21:59

click on the newly generated URL this

22:01

one will be valid but if we refresh the

22:03

one that we had previously just tested

22:05

it should now be invalid and there you

22:07

go your tokens invalid or expired so

22:08

again we've just checked and verified

22:10

that our tokens are being expired

22:12

whenever a new one is generated so last

22:14

thing that we need to do is verify that

22:15

the token's still valid at the time that

22:17

this form here is kicked off and

22:19

actually persist our users password

22:21

change

22:22

so let's jump back into our password

22:24

reset controller underneath store here

22:26

let's grab our request response session

22:29

and auth out of our HTTP context so

22:32

let's do const password schema equals

22:35

schema dot create token so we want to

22:38

verify that the token is actually being

22:39

provided and it is still a string and

22:42

then we want to verify that our user has

22:43

submitted a password so schema.string

22:45

and then you want to make sure that you

22:46

use the same rules that you're using for

22:47

your actual registration step for your

22:49

password Here I believe all that we're

22:50

doing is a Min length check of verifying

22:53

that it's at least eight characters so

22:54

we should be good there and we do const

22:56

token and password out of our request

23:00

validate schema as password schema and

23:04

then const user equals await and we can

23:06

reach for our token again to get

23:09

password reset user and provided in the

23:12

token so we'll get the actual user that

23:14

belongs to this token and then we can do

23:16

if not user so if we did not get a user

23:18

back for that token then we want to

23:20

session.flash error token X expired or

23:23

Associated user could not be found

23:26

return response redirect and redirect

23:29

them back otherwise we can await user

23:31

dot merge merge in their password change

23:34

and then save that off to the user

23:36

record and then this is completely up to

23:37

you you can send them back through their

23:39

login flow if you wish but since we've

23:40

already gone through the step of making

23:41

them go into their email click the link

23:43

to get back into here to reset their

23:45

password if they can reset their

23:46

password I think I'm okay to say they

23:48

can go ahead and be logged in so let's

23:49

do auth.log in and provided that user

23:51

record and then return response redirect

23:54

to path and point them to the home page

23:56

so we give that a save and now we should

23:58

have our complete password reset flow

23:59

rigged up so let's go ahead and dive

24:01

back into here and let's try finish

24:03

testing the second half of this flow and

24:04

let's give them some password

24:06

and oh we got there let's see so we got

24:09

the token we got the password email okay

24:11

so it's I bet you this is rigged up to

24:13

email still because we copy and pasted

24:15

it so let's dive into our reset.h page

24:17

take a look and we have a name of

24:19

password oh it's the action okay so

24:21

password.send needs to be

24:24

password.store give that a save all

24:26

right now we should be good let's dive

24:28

it back into here and let's let's give

24:30

it a refresh and then let's try

24:32

resetting our password again so there we

24:33

go all right there we go so now we're

24:35

actually logged in as our user after we

24:37

successfully reset our password so there

24:39

is actually one last thing that we're

24:40

going to want to do before we call this

24:42

lesson a day so let's jump back into our

24:44

mail trap here and let's click on the

24:45

most recent password reset link that we

24:47

just did and you can see that it's still

24:49

coming through we're not getting the

24:50

this tokens invalid or anything like

24:52

that type of message so we could

24:53

actually utilize this token again to

24:55

reset our password and everything would

24:57

go through just fine so what we need to

24:58

do is invalidate this token after it's

25:00

used so we can easily do that by jumping

25:02

back into our store method here after we

25:04

actually update their password we can do

25:06

another a weight and we can utilize our

25:08

expired password reset tokens and

25:10

provided in the user and so this will go

25:12

through and actually expire out all

25:13

password reset tokens not just the

25:15

latest one to make sure that everything

25:16

is actually expired so it will

25:19

invalidate all tokens that the user has

25:20

for their password reset flow instead of

25:22

just the one so if we come back in here

25:24

we can just use this one again to reset

25:26

the password all right so we're logged

25:27

back in now if we log out come back into

25:30

here click on the password reset link

25:32

one more time now we get that your

25:34

password's invalid or expired message

25:35

because we've gone through and

25:37

invalidated all of their password reset

25:39

tokens so now that should be everything

25:40

that we need to do if you want to you

25:42

can send them an email similar to how we

25:43

did with the password.send route to let

25:45

them know that their password has been

25:47

successfully changed that's pretty

25:48

typical within the password reset flow

25:50

but that is going to be pretty much

25:52

identical to what we did right on up

25:54

here just changing the actual HTML

25:55

content so I'll leave that up to you

25:57

alright so that should do it that is how

25:58

you can actually go about adding in a

26:00

password reset flow to your application

26:05

[Music]

Join The Discussion! (9 Comments)

Please sign in or sign up for free to join in on the dicussion.

  1. Commented 1 year ago

    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 set expires_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 the tokens table.

    I had to recreate all the missing database fields for my tokens migrations in order to get it working.

    table.string('_zone')
    table.string('loc')
    table.string('invalid')
    table.string('weekData')
    table.string('c')
    table.string('o')
    table.string('isLuxonDateTime')
    table.string('ts')
    Copied!

    I was hoping you could provide some insights as to why these fields are required.

    Thanks and keep up the amazing work.

    0

    Please sign in or sign up for free to reply

    1. Commented 1 year ago

      Hi Kreek!

      You shouldn't need to define those columns in your database, did you remember to include the { useTz: true } option to set the expires_at column to store the timezone as well? Full column definition would be:

      table.timestamp('expires_at', { useTz: true })

      Another place to look is to ensure you've set the model type to DateTime:

      @column.dateTime()
      public expiresAt: DateTime | null

      Hope this helps!

      0

      Please sign in or sign up for free to reply

      1. Commented 1 year ago

        I have the same issue here when trying to register a new user or reseting the password on a fresh clone of your repository.

        0

        Please sign in or sign up for free to reply

        1. Commented 1 year ago

          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.

          0

          Please sign in or sign up for free to reply

    2. Commented 1 year ago

      I've found a way to correct this bug

        public static async expireTokens(user: User) {
          await user.related('passwordResetTokens').query().update({
            expiresAt: DateTime.now() //REPLACE THIS TO expiresAt: DateTime.now().toFormat('yyyy-MM-dd HH:mm:ss')
          })
        }
      Copied!

      0

      Please sign in or sign up for free to reply

  2. Commented 6 months ago

    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?

     //This works
        await Token.create({
          userId: user.id,
          type: 'PASSWORD_RESET',
          token: 'random-token-value',
          expiresAt: DateTime.now(),
        })
    
    //This works
        const token = await Token.findOrFail(1)
        token.expiresAt = DateTime.now().plus({ month: 1 })
        await token.save()
    
    //But this fails with update `tokens` set `expires_at` = 2024-04-06 08:48:15.182 +00:00 where `user_id` = 1 - Unknown column '_zone' in 'field list'
        await user.related('tokens').query().update({
          expiresAt: DateTime.now(),
        })
    
    //But this works
        await user.related('tokens').query().update({
          expiresAt: new Date(),
        })
    ``
    Copied!
    ````````````
    Copied!
    1

    Please sign in or sign up for free to reply

    1. Commented 6 months ago

      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.

      await user.related('tokens').query().update({
        // provide date time as string without timestamp offset
        expiresAt: DateTime.now().toSQL({ includeOffset: false }),
      })
      Copied!

      Hope this helps!!

      1

      Please sign in or sign up for free to reply

      1. Commented 6 months ago

        Thanks Tom.

        1

        Please sign in or sign up for free to reply

        1. Commented 6 months ago

          Anytime!

          0

          Please sign in or sign up for free to reply