Express user authentication using passport

Posted on Fri Mar 08 2019

In this article, I'm going to teach you how to implement user authentication in your Nodejs Express server using Passport.

Express user authentication using passport

What is Passport?

Passport is an authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped into any Express-based web application.

It provides different strategies to authenticate users. These strategies need to be installed side by side with the passport package.

There are 502 available strategies at the time this article was written, they include basic types of token authentication like Bearer and JWT, auth0 authentication, Google, Facebook, Instagram, LinkedIn and many more.

What we will be building?

In this article, we will create a simple express server with two routes, the first one is for authenticating users and the second one is for retrieving the logged-in user information. The second route will be protected by passport and only accessible by authenticated users. We are going to use JWT as our authentication strategy.

To test our server, we will build a simple website using HTML, CSS and vanilla JavaScript. The website will have two pages, a login page, and a profile page. To access the profile page, a user must be logged in.

To follow along with this tutorial you must have NodeJs installed on your system.

The express server

We're going to start by creating a new Node project. Create a folder, name it whatever you want (this will be the root directory of the project), open your terminal, and run the following commands:

# replace the path with the one of the folder you just created
cd /path/to/project/folder
npm init -y

Next, install express and some other dependencies we will need.

npm install --save express body-parser compression

Body parser is an express middleware used to parse incoming request bodies, we will use it to parse data to json.

Compression is also an express middleware. It compresses the express response before sending it back, which makes our API calls faster and consumes less data.

After that, create a new file called index.js inside your project folder and paste the following code inside it:

const express = require('express');
const bodyParser = require('body-parser');
const compression = require('compression');

const app = express();

app.use(bodyParser.json());
app.use(compression());

app.post('/api/authenticate', (req, res) => {
    // handle user authentication
});

app.get('/api/profile', (req, res) => {
    // return user info
});

app.use('*', (req, res) => res.sendStatus(404));

app.listen(3000, () => console.log('Server started on port 3000'));

The server can handle two requests until now:

  1. A post request to hostname:3000/api/authenticate
  2. A get request to hostname:3000/api/profile

Any other request will receive a 404 status code.

And you also have to add a start script inside the package.json file like so:

{
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "start": "node index.js"
      }
}

That's it for the express part, we can now start working on implementing Passport authentication.

Adding Passport authentication

First we need to install passport, the JWT strategy and the jsonwebtoken packages.

npm install --save passport passport-jwt jsonwebtoken

Passport and passport-jwt will handle the authentication middleware while jsonwebtoken is responsible for generating a JWT token for our user.

Generating JWT tokens

Before we start implementing the authentication route let's create a user object to simulate a user fetched from a database. Add the following code to the index.js file

const user = {
    username: 'hassansaleh31',
    password: 'abcdefgh',
    name: 'Hassan Saleh',
    email: 'hassansaleh31@gmail.com',
    picture: 'https://picsum.photos/512',
    bio: 'I\'m a fullstack web developer from Lebanon'
}

We also need a secret key to use for encoding and decoding tokens.

const secret = 'mySecretKey';

Next we will create a function called authenticateUser that takes a username and a password as arguments, checks if they are correct and returns a token like so:

const authenticateUser = (username, password) => {
    if (!username || !password)
        return null
    // here you would normally fetch the user from your database
    let authUser = { ...user };
    if (authUser.username.toLowerCase() != username.toLowerCase() || authUser.password != password)
        return null
    const token = sign({ username: authUser.username }, secret, { expiresIn: 60 * 60 * 24 });
    return token;
}

Tip: the spread operator ... spreads a javascript object inside another object, or an array inside another array. It's useful for cloning objects and arrays without pointing at the original. Also useful for adding values when cloning like

const authUser = { ...user, lastSeen: new Date() };
const numbers = [1, 2, 3];
const realNumbers = [0, ...numbers];
// 0, 1 , 2, 3

First we make a copy of the user object we created, then we check if the username or password match those of the our user. If they don't match, the function returns a null value. Else we create a new token by using the sign function from jsonwebtoken.

Next we should import the sign function at the top of the file for this code to work.

const { sign } = require('jsonwebtoken');

And finally we will call this function inside the the authenticate route we created before

app.post('/authenticate', (req, res) => {
    // handle user authentication
    const username = req.body.username;
    const password = req.body.password;
    const token = authenticateUser(username, password);
    if (!token)
        return res.json({ success: false, msg: 'Wrong username or password' });
    return res.json({ success: true, token });
});

What we did is we extracted the username and password from the request body, called the authenticateUser function with these constants and stored the output in a constant named token. Then we test to see if the token is null, if it is, we send a json object that includes a message else we send back the token.

In JavaScript, the object { token } is equivalent to { token: token }

You can now save the file and start the server by running the following command

npm start

If everything is working correctly you should see the message Server started on port 3000 in your terminal.

To test this route, I used an application called postman. If you don't have the app or any similar API testing app, don't worry we will create a small website that will send requests to the server. If you do have it, send a post request to http://localhost:3000/authenticate with the header Content-Type set to application/json and in the body send the following raw json string:

{
    "username": "hassansaleh31",
    "password": "abcdefgh"
}

Authenticating User

As you can see, our server works and we received a token.

Protecting routes with passport

Now that we are generating tokens, we can start protecting routes using passport.

First, we have to import passport, and two methods from passport-jwt, ExtractJwt and Strategy.

const passport = require('passport');
const { ExtractJwt, Strategy } = require('passport-jwt');

Next, we have to configure passport to use the JWT strategy. But first, let's create the strategy.

// options for our strategy
const passportOptions = {
    jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('jwt'),
    secretOrKey: secret
};

const jwtStrategy = new Strategy(passportOptions, (jwt_payload, done) => {
    const username = jwt_payload.username;
    // here you would normally fetch the user from your database
    let authUser = { ...user };
    if (authUser.username !== username) {
        done(new Error('User not found'), null)
    } else {
        delete authUser.password;
        done(null, authUser)
    }
});

Let me explain what we did here.

First we created a new object to hold the options for our new strategy. This object contains the jwt token which we extracted from the header using the method ExtractJwt, and the same secret we used to sign the token.

Then we created a new strategy, passed it the options and a callback function that has the token payload and another callback function called done. The token payload has the user's username as we set it in the authenticateUser function, and done is callback function that takes two arguments, the first one is an error and the second one is the user.

We then check if the user exists, if not call done with an error, else we call it with the user.

After that we have to tell passport to use this new strategy, and then initialize passport.

passport.use(jwtStrategy);
app.use(passport.initialize());

Now we are ready to protect any route that requires a user to be logged in, like the profile route. All we have to do is add a simple middleware function like so:

app.get('/api/profile', passport.authenticate('jwt', { session: false }), (req, res) => {
    // return user info
    res.json({ user: req.user });
});

passport.authenticate is a middleware function that takes a strategy name as the first argument, and an optional object that has the options as a second argument. In our case we're using the jwt strategy and we don't want any sessions because we will send the token with each request inside the headers.

By using the passport middleware on a route, we will have access to the user who is sending the request, which means you no longer have to extract the username from a request body and fetch the user from the database on each request because passport is doing this for you.

Save the file and close the server if it's still running ( control + C or command + C on MacOS ), then start it again using npm start.

Try to send a get request to http://localhost:3000/api/profile and you will get Unauthorized as a response.

Now authenticate the user as we did before using postman to get a token, copy the whole token to your clipboard and set the Authorization header to JWT followed by a space followed by the token like so:

JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imhhc3NhbnNhbGVoMzEiLCJpYXQiOjE1NTIwMDEyMjIsImV4cCI6MTU1MjA4NzYyMn0.MQJ4p3HRaqghVzHUuNao5-PftiJzK1JLMtztEpmwEM8

Send the request and now you have a response with the user info.

Retrieving User Info

The frontend website

Okay, so now we have a server and an api with user authentication. Let's make it useful by creating a small website that uses this api.

First, we need to tell express in which directory are we going to place our website. Add the following line in the index.js file:

app.use(express.static('public'));

Create a new folder inside the project directory, name it public and create three files inside of it, index.html, login.html, and index.js.

Open index.js with your code editor and paste the following code.

function getToken() {
    const token = localStorage.getItem('token')
    return `JWT ${token}`;
}

async function apiRequest(url, method, body) {
    const token = getToken()
    const res = await fetch(url, {
        method,
        body: JSON.stringify(body),
        headers: {
            'Authorization': token,
            'Content-Type': 'application/json'
        }
    })
    if (res.status === 401)
        return window.location = '/login.html'
    const json = await res.json()
    return json;
}

In the code above we have two functions. The first one is a simple function that returns the token saved in localstorage.

The second function is what we will use to send requests to the server. It is asynchronous, and takes three parameters, the url, method and body of the request. When we get a response, we first check if it's status code is 401 (UNAUTHORIZED), if it is we redirect the user to the login page, else we parse the fetch response to json and return it (You might also want to check for different status codes before parsing).

That's it for the index.js now let's create the home page. Open index.html and paste the following code:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Profile</title>
</head>

<body>
    <div id="user-box"></div>
    <button id="logout-btn">Logout</button>
    <script src="index.js"></script>
    <script>
        apiRequest('/api/profile', 'GET')
            .then(res => displayUser(res.user))
            .catch(err => console.error(err))

        function displayUser(user) {
            const userBox = document.getElementById('user-box');

            const name = document.createElement('h1');
            name.innerText = user.name;
            userBox.appendChild(name);

            const bio = document.createElement('p');
            bio.innerText = user.bio;
            userBox.appendChild(bio);

            const image = document.createElement('img')
            image.src = user.picture;
            userBox.appendChild(image);
        }

        document.getElementById('logout-btn').addEventListener('click', () => {
            localStorage.clear();
            window.location = '/login.html';
        })
    </script>
</body>

</html>

This page has two HTML elements, an empty div with the id user-box and a logout button with the id logout-btn. We import the index.js file and create a new script below it.

Inside the script we call the apiRequest function and send a get request to the profile endpoint we defined before. When the response arrives, we then call a function called displayUser and pass the user as an argument.

This function will create three HTML elements containing the name, bio and image of the user, then it will append them to the user-box div.

Lastly, we attached an event listener to the logout button in order to listen for a click event. When the button is clicked, we will clear the localStorage where the user token is stored and redirect the user to the login page.

Now open the login.html file and paste the following code:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Login</title>
</head>

<body>
    <h1>Login Page</h1>
    <form>
        <input type="text" name="username" id="username" placeholder="Username" required>
        <input type="password" name="password" id="password" placeholder="Password" required>
        <input type="submit" value="Login">
    </form>
    <script src="index.js"></script>
    <script>
        const form = document.querySelector('form');
        form.addEventListener('submit', (e) => {
            e.preventDefault();
            const username = document.getElementById('username').value
            const password = document.getElementById('password').value
            apiRequest('/api/authenticate', 'POST', { username, password })
                .then(res => {
                    if (res.success) {
                        localStorage.setItem('token', res.token);
                        window.location = '/'
                    } else {
                        alert(res.msg)
                    }
                })
                .catch(err => {
                    alert('An error occurred')
                })
        })
    </script>
</body>

</html>

In this page we have a form with two inputs and a submit button. We attached an event listener on the form to listen for a submit event. When the form is submitted, we prevent the default behavior, get the value of the username and password from the inputs and create a post request to the authentication route.

When we get a response back, we check for the success value, if it's true we will set the token in the localStorage to be the token received from the server and redirect the user to the profile page. Else, we will show an alert with the message received from the server.

Save all your files, restart the server, open your browser and navigate to localhost:3000.

User Authentication with Passport

Thank you for reading, I hope that you found this article useful. If you have any question don't hesitate to contact me.

The source code for this project is on GitHub.

If you feel that you have learned anything from this article, don't forget to share it and help other people learn.