DEV Community

Cover image for User authentication in an Express app
mech2dev
mech2dev

Posted on

User authentication in an Express app

Overview

In this article we will delve into authentication using json web token (JWT). User authentication is the process of verifying a user attempting access to an application or part of an application.

In the API we will build, we will implement authentication by building middleware and using the middleware to protect the routes we need to limit access to.

Authentication in an Express app

  • Implementing user authentication in an express app.

Technologies used

  • Authentication done using jwt (json web token). Passwords are hashed using bcrypt. The API uses a MongoDB database.

Endpoints

URL METHOD DESCRIPTION
api/users/ POST (public) register a user
api/users/login POST (public) logs in a user
api/users/me GET (private) gets the info of the logged in user

How to run

  • Clone the repository on your local computer
  • Do npm run install to install the required dependencies
  • Set up your MongoDB collection
  • Do npm run dev to start the API

Test the API with Postman




How does JWT work

Let's see how the JWT approach to authentication works.

Say a user gives the correct credentials and logs in. The server will generate a token and send it back in the response. Subsequent requests will have the token in the authorization header.

If the token is missing or expired, the user will not be able to continue accessing protected endpoints.

The token consists of three parts i.e. the header, payload and the signature. You can read more about what each of the parts of a jwt do here.

Below is an overview of the JSON web authentication process

jwt auth

We will also use the package bcrypt.

Project set up

To show how to implement jwt authentication, we will use a simple API that implements login functionality for the users of a blog. Here are the endpoints of the API:

URL METHOD DESCRIPTION
api/users/ POST (public) register a user
api/users/login POST (public) logs in a user
api/users/me GET (private) gets the info of the logged in user

As described above, we don't need to implement authentication to register a user and to login the user. We however need to ensure that when returning user info, we only do so for the current logged-in user. This will require some form of authentication.

We will create a folder named jwt_auth. This is where all our API code will reside.

Lets create the config (where our models reside), routes (where we define the various routes in our API), middleware
(where we will build error handling and authentication middleware) and controllers (where we will define the callback functions fired when the routes in the Routes directory are matched) directories.

To set up the whole project you can refer to my articles on how to build a REST API and how to set up MongoDB for your project.

Here's the structure of our project at the end:

auth structure

Implementing bcrypt and jwt

Now that we've seen our project structure, we will implement our authentication in 3 steps:

  • Encrypt password after logging in and generate token
  • Create middleware to check if token exists and its validity
  • Chain authentication middleware to the routes you need to protect

You can view the full project before and after adding authentication here (the version with authentication is on the branch 'authentication'):

REST API for a React FrontEnd

This is an express rest api for a simple blog app that enables one to manage a simple blog.

You can view the client app here .

Endpoints

URL METHOD DESCRIPTION
api/blogs/ GET fetches all blogs
api/blogs/{id} GET fetches a single blog
api/blogs/ POST post a blog
api/blogs/{id} DELETE delete a single blog



This project has been discussed over several blog posts hence we will only concern ourselves with implementing some authentication

Let's view our jwt_auth/controllers/userController.js file and see how we hash our passwords and generate our tokens.

const asyncHandler = require('express-async-handler')
const User = require('../models/userModel')
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')

const registerUser = asyncHandler(async (req, res) => {
    const { name, email, password } = req.body

    if(!name || !email || !password){
        res.status(400)
        throw new Error ('Please fill in all fields')
    }

    const userExists = await User.findOne({ email })

    if(userExists){
        res.status(400)
        throw new Error ('User already exists')
    }


    const salt = await bcrypt.genSalt(10)
    const hashedPassword = await bcrypt.hash(password, salt)

    const user = await User.create({
        name,
        email,
        password:hashedPassword
    })

    if(user){
        res.status(201).json({
            _id:user.id,
            name:user.name, 
            email:user.email,
            token:generateToken(user._id)
        })
    } else{
        res.status(400)
        throw new Error ('Invalid user data')
    }

})

const loginUser = asyncHandler(async (req, res) => {
    const { email, password } = req.body

    const user = await User.findOne({email})

    if(user && await bcrypt.compare(password, user.password)){
        res.status(201).json({
            _id:user.id,
            name:user.name,
            email:user.email,
            token:generateToken(user._id)
        })
    }else{
        res.status(400)
        throw new Error ('Invalid credentials')
    }
})


const getThisUser = asyncHandler(async (req, res) => {
    res.status(200).json(req.user)
  })


const generateToken = (id) => {
    return jwt.sign({id}, process.env.JWT_SECRET, {
        expiresIn:'30d'
    })
}


module.exports = {
    registerUser,
    loginUser,
    getThisUser
}
Enter fullscreen mode Exit fullscreen mode

Encrypting the password with bcrypt is as follows:

const salt = await bcrypt.genSalt(10)
const hashedPassword = await bcrypt.hash(password, salt)
Enter fullscreen mode Exit fullscreen mode

We first create a salt with bcrypt's genSalt method and use this salt and the inputted password by the user to generate the hashedPassword. This is the password that will be stored in the database and not the user's real password. This is a security measure to prevent malicious parties from accessing passwords stored in a database.

One would then ask, how can we confirm a user's password if we don't have the raw password stored in the database? In our case, we will use bcrypt's handy compare() method. Let's see it in action in our userController.js file when we to log in a user.

const loginUser = asyncHandler(async (req, res) => {
    const { email, password } = req.body

    const user = await User.findOne({email})

    if(user && await bcrypt.compare(password, user.password)){
        res.status(201).json({
            _id:user.id,
            name:user.name,
            email:user.email,
            token:generateToken(user._id)
        })
    }else{
        res.status(400)
        throw new Error ('Invalid credentials')
    }
}
Enter fullscreen mode Exit fullscreen mode

Above is the loginUser function which is called when the route /api/users/login is matched. We first obtain the email and password from destructuring the request body. We then use the Mongoose method findOne() to find the exact user with the specified email address. To verify the user, we compare the inputted password and the password in the database using bcrypt.compare(). It takes the keyed in password and the password in the database as arguments and if the passwords match it returns true.

If the user exists in the database and the password is verified, loginUser returns the user's details including a token that we can use to access some restricted content.

Let's see how we generate the token:

const generateToken = (id) => {
    return jwt.sign({id}, process.env.JWT_SECRET, {
        expiresIn:'30d'
    })
}
Enter fullscreen mode Exit fullscreen mode

Our generateToken function takes in the user's id as an argument. We will then use jwt's sign() method to create and return the token. sign() takes in 3 arguments:

  • id: The id of the user logging in
  • secret: This is a private signing key that we store in our dotenv file. It confirms the origin of the token
  • expiring time: We can add an optional argument to specify when the token expires

We will now create our authentication middleware which will make sure that protected routes are accompanied with a valid token. This is how we implement authentication with jwt in express apps.

Authentication middleware

Some routes need to be accessed privately (as specified in the API definition) and the way we do so is by chaining some middleware to the route definition.

We will create our authentication middleware in our middleware directory in the file authMiddleware.js.

Let's take a look at our auth middleware:

const jwt = require('jsonwebtoken')
const asyncHandler = require('express-async-handler')
const User = require('../models/userModel')

const protect = asyncHandler(async(req, res, next) => {
    let token

    if(req.headers.authorization && req.headers.authorization.startsWith('Bearer')){
        try {
            //Get token from header
            token = req.headers.authorization.split(' ')[1]

            //Verify token
            const decoded = jwt.verify(token, process.env.JWT_SECRET)

            req.user = await User.findById(decoded.id).select('-password')

            next()
        } catch (error) {
            console.log(error)
            res.status(401)
            throw new Error ('Not authorized')
        }
    }
})

module.exports = {
    protect
}
Enter fullscreen mode Exit fullscreen mode

The code above might seem like alot but it's actually quite simple. We are simply using javascript methods to get a token from the request header and verifying this token. We also set the error message if token doesn't exist or isnt valid ('Not authorized').

At the end we export our authentication function as protect.

How can we the utilize this protect function to protect some routes?

Let's have a look at our routes file now:

const express = require('express')
const router = express.Router()
const { registerUser, loginUser, getThisUser } = require('../controllers/userControllers')
const { protect } = require('../middleware/authMiddleware')

router.post('/', registerUser)
router.post('/login', loginUser)
router.get('/me', protect, getThisUser)

module.exports = router
Enter fullscreen mode Exit fullscreen mode

The only route we need to protect is the api/users/me route
and we do so by including protect in the route definition as seen above. It's that easy!

Testing our authentication

Let's do a recap of what we have so far. We have defined several routes for a REST API that implements some login functionality. For one specific route that returns a specific user's info, we need to do some form of authentication to prevent unwanted access. To do so we implement some middleware and chain it to our route definition.

Now that we have added authentication to our project, lets test if it works

We will start by creating a new user called Daniel with the email address of daniel@yahoo.com and a password of 1234:

create user

Note that a token is generated when we create a user.

Now let's login using the user details we have created:

logon user

To confirm our error handling works, lets use a wrong password and see the response generated:

invalid credentials

We get the error message of "Invalid credentials" as desired

Let's now try accessing GET api/users/me without using our token:

not authorized

We get an error message that we are not authorized to access that particular route

Let's try accessing the same route but this time pass in the correct token:

authorized

As you can see, we have no error messages and we get the content we want back from a protected route. Success!

Conclusion

In this article, we implemented authentication using jwt. In the next article we will deploy our express app to the internet using Railway. You can read that article here.

Top comments (0)