Authenticating Users Node.JS
By: Johnson Kow
Tags: Mongoose, Authentication, Jwt, Nodejs, Software Development

In my very last project for the Flatiron school, I skipped out on authentication due to the lack of time before presenting but I kept it in mind as I created a fake authentication where I set my user in state inside my react application.

As I explore authentication, I’m understanding so much more about new material like JWT or JSON web tokens. This is the tool I’ll be using to generate different tokens and to verify existing tokens when users login. With a quick search you’ll find that to import jsonwebtoken and bcrypt, you’ll need the command line below.

npm i jsonwebtoken

npm i bcrypt

To access them in your file, you’ll just need to require them like most libraries.

const jwt = require('jsonwebtoken')

const bcrypt = require('bcrypt')

Breakdown

So what do we want to do?

Passwords:

If we think about a simple user schema, we can assume some things are required like first and last name, email, and password. When we look at any instance of a user inside our database we don’t want to see their password in plain text. Instead we want to see some form of encryption from the moment that our user is created.

Tokens:

In terms of authentication, if a user is being created we want them to be authenticated immediately. Likewise, when a user is logging in, we’d like for them to be authenticated. People can be authenticated on multiple devices, which is what enables us to run instagram on our phones, tablet, and computers.

Passwords

I’m using an MVC pattern for my application which means I’ve got separate folders for models, views, and controllers. Our file of interest is the file that holds the schema for my user which for me lives under /model/user.js.

To make things a bit nicer, I’ve cropped my user schema to only focus on the password. This schema is built using mongoose.Schema()

const userSchema = new mongoose.Schema({ ..., password:{ type: String, required: true, trim: true, minlength: 7, validate(value){ if(value.toLowerCase().includes('password')){ throw new Error('Password cannot be the word `password`') } } }

In my opinion, these are the first steps to validation. Making sure that passwords meet certain requirements like length, error handling passwords with the word ‘password’, and making the password imperative to the creation of a user by setting “required” equal to true.

In my controllers folder that I’ve called routes, I’ve created the endpoint for the user being created and it looks as such.

router.post('/users', async (req,res)=>{ const user = new User(req.body) try { await user.save() res.status(201).send({user}) } catch (e) { res.status(400).send(e) } })

Inside our try block, we await to save our user to our database before sending the response. We’ll develop some code that runs before .save() is called under the condition that the password is updated. Back in our schema file, we’ll write a function that encrypts our password when the password is modified or created.

const bcrypt = require('brcypt') userSchema.pre('save', async function (next){

const user = this; if(user.isModified('password')){ user.password = await bcrypt.hash(user.password,8) } next(); })

We have access to a neat method given to us by mongoose called .pre() which excepts a method and a callback function. In this case, when we write user.save(), this callback function will be called and it will check if the password has been modified.

Tokens

As mentioned before, users should be authenticated when they create a profile or login so we’ll use those endpoints. The endpoints for creating a user is the same as above with some additional logic.

const auth = require('</path to function >') router.post('/users', async (req,res)=>{ const user = new User(req.body) try { await user.save() const token = await user.generateAuthToken() res.status(201).send({user,token}) } catch (e) { res.status(400).send(e) } })

Once we’ve saved our user to the database, we create a token by calling a method ‘generateAuthToken()’ that I’ve also put in the user schema file. We’re also going to add to the User schema by adding an array of tokens. The array is what allows for multiple devices to be logged in. If it were only one token, you’d only be able to login from one device at a time. If you attempted to login from another, it would login from the attempted device and logout of the previous. Therefore, we add to our schema the array of tokens and make it required.

const userSchema = new mongoose.Schema({ ..., tokens:[{ token:{ type: String, required: true } }] })

This was cropped and only shows the tokens in the schema being added.

userSchema.methods.generateAuthToken = async function(){ const user = this const token = jwt.sign({_id:user.id.toString()},"thisismynewcourse") user.tokens = user.tokens.concat({token}) await user.save() return token }

This function uses ‘this’ to symbolize the specific instance of a user. From there we see that we append the token inside of the tokens attribute array of the user and then we save.

So now a user looks something like this inside of the database!

user:{ name: 'Johnson'

email: 'johnson@email.com'

password: 'GhyO@$dfs0fjasdfJF8Hf'

tokens: [fdaouqewkjlgd, fasdfioqpwef, fapsodifjef]

}

Now that the generateAuthToken() function has run, the router will continue with its code by sending back a status code.

That was for creating a user but what about logging in?

Let’s look at that router.

router.post('/users/login', async (req,res)=>{ try{ const user = await User.findByCredentials(req.body.email, req.body.password) const token = await user.generateAuthToken() res.send({user, token}) }catch (e){ res.status(400).send(e) } })

This is a bit similar to create with the only key difference being that instead of appending a new user to the database, you have to find them and make sure their email and passwords coincide with our input. We look for our user with another function that I added to my schema called findByCredentials().

userSchema.statics.findByCredentials = async (email, password ) =>{ const user = await User.findOne({email}) if(!user){ throw new Error('Unable to Login') } const isMatch = await bcrypt.compare(password, user.password) if(!isMatch){ throw new Error('Unable to Login') } return user }

The findby() method looks for the user in the database through the email provided in the argument. We do some quick error handling just in case we don’t find a user. Then we create this constant that essentially compares the password string with the encrypted string in the database. Luckily, bcrypt has a .compare() function that compares the user input with the encrypted password. Of course, we have some error handling if isMatch is false. Otherwise, we return the user.

Back in our router, we create a token again using the generateAuthToken() which we know appends a token to the users’s tokens attribute. If all goes well you should be getting back a json of your user and tokens.

As I move along with this stuff, my eyes are opening up to the material that I didn’t know about in regards to security and authentication. This is just the foundations. I’m sure there’s much more to learn.

I hope you guys enjoyed this walk through. As always, happy coding!