When you build an application there is a good chance that you'll want to restrict access to certain parts of it. You'll need to set up a user authentication system and protect certain routes in your app.
It is quite common to use the user's email address as a unique identifier during your registration process. As no two people can have the same email address that is a very valid approach. However, very few email addresses are kept in secrecy. So there is tons of people who know my or your email address which means anyone could use it to register anywhere.
I had multiple occasions recently when I thought that email verification could come in very handy. Doing some research for the best approach I found some useful comments on general appraoches to take but did not find a somehow straight forward guide. So I just started developing a solution for myself and thought: Why not share it?!
Why verify users' email addresses
I'm sure there is tons of reasons to verify your users' email addresses but there are two very basic ones that I found important when developing web applications recently:
- Make sure the email actually exists
- Make sure the person registering actually owns the email address
There are many different useful implications in terms of e.g. marketing, sales and legal, but let's just acknowledge that there are valid reasons to conduct email verification.
Prerequisites
I don't want to dive too deep into technology and framework dependent code. In my case I used Vue front-end application and node.js/Express on the backend (GitHub). I'll also try explaining the approach as high level as possible. However, a few things are needed to set-up the system:
- front-end for user interaction (I used Vue)
- a server on the backend (I'll refer to node.js/Express where necessary)
- a database that the server is able to communicate with (I used mongoDB with mongoose)
- an email service connected to and usable by your server (I used nodemailer with an email service provider I already use)
The register and login functionality should be set up already. This is where we get started.
Step by Step Guide
Step 1
Assign a status
property to your user model in the database. In my case where I am using mongoDB with mongoose and a 'users' collection:
status: {
type: String,
default: "pending",
},
Anyone who registers will get a default status of pending
until the user's email address is verified.
Step 2
When a user registers, create a secret code in your database which is connected to the user through e.g. the user's email address and is used to verify the email address later on. In my case I set up a 'codes' collection in the database and a mongoose schema as follows:
const secretCode = new Schema({
email: {
type: String,
required: true,
},
code: {
type: String,
required: true,
},
dateCreated: {
type: Date,
default: Date.now(),
expires: 600,
},
});
Note: The entry expires after 600 seconds (10 minutes) in order to limit the lifetime of the code.
Step 3
At the end of the server side registration process where you add the user to your database and built the secret code from (2.) send an email to the user's email address with an activation link (plus any additional information you want to provide in certain cases).
The activation link should point to a server's endpoint and include a part that can be uniquely connected to the user account (e.g. user's ID from your database) and the secret code from (2.).
In my case the link structure looks like this:
url: `${baseUrl}/api/auth/verification/verify-account/${user._id}/${secretCode}`
Note: As the secret code produced in (2.) expires after 10 minutes, you should provide a possibility for the user to resend an activation link at a later point. You could implement a button on the front end which appears as long as the user's status: "pending"
and hits a specified endpoint on your server taking care of the code generation and email sending.
Step 4
Redirect the user to a verification page on the front-end where they are informed to check their email inbox for the activation link.
Note: That could be a good place to put the mentioned 'Resend Activation Link' button. As long as the user's status: pending
they should be redirected to this page and should not have access to the private area of your application. It does not matter whether they register or login at that moment.
Step 5
On your server you need to set up an endpoint for the activation link.
The route could look like this:
"/api/auth/verification/verify-account/:userId/:secretCode"
where you (in case of node.js/Express) extract the parameters like this:
const { userId, secretCode } = req.params;
Now the only thing you need to do is:
- Get the email address connected to the
userId
from your database - Check if there is a secret code in your database that is connected to the user's email address
- Update the user's status to
status: "active"
Step 6
As the user's status has been updated successfully you can redirect the user to the protected private area of your application.
And finished you are! Of course this is only a very high level description and there is a lot more going on in terms of routing and route protection in the background.
You can view my full example on GitHub and even use it as a template if you wish.
Wrap Up
To conclude, here a short wrap up of the steps as laid out:
- Set up an initial user's status of
"pending"
- Create a secret code connected to the user when registering
- Send email to user including an activation link with user specific information (e.g. user ID) and the secret code
- Redirect user to verification page
- Set up server endpoint that is hit by the activation link, extracts and verifies the data provided through the link and updates the user's status to
"active"
Top comments (12)
Hi Chris! Nice post! I have a question. When you register in the app and not verify the email, how do you control when other user wants to register in the app but with the same email that you previously registered but not verified it?
If the email is "in use" but not verified, the email's user can the recover password or send another code activation to its email.
One thing I'll suggest is, once a user post BioData for account registration, check if mail exist :
`
One thing I'll suggest is, once a user post BioData for account registration, check if mail exist
If (mail_exist){
is_mail_verified ? {ask user to confirm identity, by sending verification link to mail} : { redirect to login }
}
else{
register user and verify email
}
One thing I'll suggest is, once a user post BioData for account registration, check if mail exist :
If (mail_exist)
is_mail_verified ? {ask user to confirm identity, by sending verification link to mail} : { redirect to login }
}
else{
register user and verify email
}
Thank you! I solved that problem very similar to what you say.
Shouldn't the token expiration date be longer than 10 minutes? Just in case the user does not want to verify their account right away?
That is, of course, as flexible as you need. At the end, I think the question is, why do you need the code to expire at all. I guess, the shorter the period, the safer the system in general. But it's totally up to you.
Btw I am sorry for the late reply. Somehow I didn't receive any notifications. Cheers
Thanks for your reply. I guess a good reason for the token to expire is just in case the user's email gets hacked. Either by a hacker or a disgruntled boyfriend or girlfriend. If any of those people happen to find the link and the original user has not yet activated their account, they would be able to activate the account for them. However, activating the account alone would not give them the login credentials. I suppose the hackers can choose to reset the password at that point by entering the email address but even if they manage to do that, chances are the account does not contain any sensitive data since the original user never even activated the account. In the end, I suppose you're right. The token does not need to expire since no real harm will come from it even if someone manages to hack their email.
Brilliant solution, implemented it yesterday and it works great, thanks Chris!
Great that it worked out for you!
Hi Chris! Thanks for the post!
Btw, what is the point to save all the codes in to a collection instead of adding a new field to user model?
Is there any need to save the historical verification code?