Token Auth with JWTs Part 1 - Server Setup

Note: If you haven't yet read through the Authentication Basics post, you should go read through it now before moving on.

Intro

What we'll be working with is a simple todo list organizer and adding user authentication on it so that a user will only see his/her todo items instead of everyone's todo items.

How to follow along

We've created a repository with git branches you can use to follow along with this tutorial series. Each part in the series has a beginning and ending branch - the way the code should look before starting each part and the way it should look after completing that part of the series.

To get everything set up to start, first cd into a folder where you want this code to live, copy all the lines below at once, paste them all into the terminal, and hit enter. (No need to do them one at a time).

Note: We've created 2 branches of this series: one that uses Redux and one that uses React Context. Part 1 is the same for both, but there are two different repositories for following along. Below you'll find instructions for cloning the correct one depending on which technology you're planning on using

Using the React Context repo:
git clone -b part-1-server-auth-beginning https://github.com/VSchool/token-auth-series-react-context.git &&
cd token-auth-series-react-context
Using the Redux repo:
git clone -b part-1-server-auth-beginning https://github.com/VSchool/token-auth-series-react-redux.git &&
cd token-auth-series-react-redux
No matter which repo you chose:
npm i && 
npm i -g nodemon

Take a minute to look around the repository. Don't skip this step. It's important you get a feel for the current state of the code in order to understand the additions we'll be making. Some of the things you'll notice as you look around:

  • The project only has a server folder for now. We'll add the front-end stuff later.
  • The server-side files are organized like this:
token-auth-series/  # Main project root folder
  |_ models/
       |_ todo.js
  |_node_modules/
      |_ ... all the node modules here
  |_ routes/
       |_ todo.js
  |_ index.js  # Main server file
  |_ package.json
  |_ package-lock.json
  |_ .gitignore
  • Todos are very simple for now, only having title and completed properties.
  • Todo Routes are also very basic.

Once you've run the above set of commands to clone and run the project, you should now be able to use Postman to test these routes. Open Postman and make a GET request to http://localhost:5000/todo, which should return an empty array since you don't have any data in your local database yet:

Next, make a POST request and submit a new Todo item in the body of the request. Change GET to POST, click in the "Body" tab, click the "raw" option, and change the "Text" dropdown to "JSON (application/json)".

Type in a simple JSON object with a title property to post a single todo:

Submit a few of these to help populate our database, and make another GET request to /todo to make sure everything is coming through.

Yup, PonyCon is a thing.

Awesome! This is all we need before we jump into adding authentication to our app!


Add Users and Authentication to the Server

Hopefully you've been able to follow along up to this point. Everything we've done so far has been covered by other posts on this site and in class, so if you have questions about anything up until now, you should clear them up before moving on.

Some of the code you'll be writing for user authentication may be confusing and pretty new, but don't let it stop you from trying it out, breaking and fixing it again, researching it further, etc. We're going to be implementing JSON Web Tokens (JWTs, pronounced "jots") now, which is most likely a new/foreign concept to you. There's lots of great resources out there for better understanding them (as outlined in the Authentication Basics post), so feel free to take a look at those before moving on.

Add a user model

We'll create a new file called user.js in the models folder (models/user.js).

// models/user.js

const mongoose = require("mongoose");  
const Schema = mongoose.Schema;

const userSchema = new Schema({  
    username: {
        type: String,
        required: true,
        unique: true,
        lowercase: true
    },
    password: {
        type: String,
        required: true
    },
    // In case we need to distinguish types of users in the future
    isAdmin: {
        type: Boolean,
        default: false
    }
});

module.exports = mongoose.model("User", userSchema);

We'll be adding to/modifying this later, because right now the database will be storing the original passwords that the user submits, which is not good practice at all. We'll come back to this file later.

Install jsonwebtoken and set up environment variables

Before we build the route for authentication (which will include an API endpoint for creating a new user and for logging in an existing user), let's install the jsonwebtoken npm package, which is an implementation of the JSON Web token spec made in node:

npm install jsonwebtoken

After that's installed, We're going to set up a JWT secret in an environment variable. Environment variables are usually static information that is either unlikely to change, or should be kept in a separate file for security reasons.

Once configured, environment variables can be accessed inside of Node.js as a property on the built-in process.env object. They're typically defined in a SCREAMING_SNAKE_CASE format. E.g. below, we're creating an environment variable called SECRET, which we can access in our JavaScript code as process.env.SECRET.

The "secret" that we're adding here is like a password that verifies that a JWT was created by our server. If someone manages to intercept the JWT, they would be able to figure out any information in the payload (which is why it's important not to put any Personally Identifiable Information in the payload of the JWT.) However, the hacker would need the know the secret that was used to create the JWT if they ever wanted to pretend to be you using that token.

1. Install dotenv

In the terminal, from your project's root folder, run:

npm install dotenv
2. Create a file called .env

Create a new file simply called .env in your project root folder. This is where we'll set any environment variables we want.

Inside that file, add:

SECRET="correct horse battery staple"

You're welcome to choose any secret you want.

3. Add .env to your .gitignore file

Because this file contains our server's token secret, we definitely don't want this showing up in our open repository on Github. Make sure to add .env to your .gitignore file. Here's a sample .gitignore file with some common things you'll want to ignore:

node_modules/
.DS_Store
.log
.idea/
.vscode/
*.orig
.env

Important Note: For the sake of avoiding confusion, we've kept our .env in the repository for this tutorial so you don't have to re-create it every time to switch to the next branch in the series. However, in regular practice you will always want to ignore your .env files that have sensitive information in them, like the SECRET from the previous step.

4. Set up your server to configure environment variables

Near the top of your index.js file, add the following line:

require("dotenv").config();

We'll put it on the 3rd line of the file:

...
const app = express();
require("dotenv").config();
...

Build authentication routes - Login & Signup

Now that those things are set up, let's build the signup and login routes.

Create a new file, routes/auth.js. These are the routes we'll use to allow a user to create a new account (signup) and to receive a JWT (login). (We're also going to send a JWT when they sign up so they don't have to enter the same credentials twice.)

Make sure to read through the comments in the code for further explanation.

// routes/auth.js

const express = require("express")
const User = require("../models/user");
const authRouter = express.Router();
const jwt = require("jsonwebtoken");

//post a new user to user collection (signing up)
authRouter.post("/signup", (req, res, next) => {
    // try to find a user with the provided username. (If it already exists, we want to tell them
    // that the username is already taken.)
    User.findOne({username: req.body.username.toLowerCase()}, (err, existingUser) => {
        if (err) {
            res.status(500);
            return next(err);
        }
        // If the db doesn't return "null" it means there's already a user with that username.  Send the error object to the global error handler on your server file.
        if (existingUser !== null) {
            res.status(400);
            return next(new Error("That username already exists!"));
        }
        // If the function reaches this point and hasn't returned already, we're safe
        // to create the new user in the database.
        const newUser = new User(req.body);
        newUser.save((err, user) => {
            if (err) {
                res.status(500);
                return next(err);
            }
            
            // If the user signs up, we might as well give them a token right now
            // So they don't then immediately have to log in as well
            const token = jwt.sign(user.toObject(), process.env.SECRET);
            return res.status(201).send({success: true, user: user.toObject(), token});
        });
    });
});

authRouter.post("/login", (req, res, next) => {
    // Try to find the user with the submitted username (lowercased)
    User.findOne({username: req.body.username.toLowerCase()}, (err, user) => {
        if (err) {
            return next(err);
        };
        // If that user isn't in the database OR the password is wrong:
        if (!user || user.password !== req.body.password) {
           res.status(403);
           return next(new Error("Email or password are incorrect"));
        }

        // If username and password both match an entry in the database,
        // create a JWT! Add the user object as the payload and pass in the secret.
        // This secret is like a "password" for your JWT, so when you decode it
        // you'll pass the same secret used to create the JWT so that it knows
        // you're allowed to decode it.
        const token = jwt.sign(user.toObject(), process.env.SECRET);

        // Send the token back to the client app.
        return res.send({token: token, user: user.toObject(), success: true})
    });
});

module.exports = authRouter;

The login route should be pretty straightforward to understand—it's just creating a user with a unique username in the database. But let's take a deeper look at the login route:

First, as always, we handle the error if it exists. Here's a reminder why we jump through that loophole by convention in Node.js

Next, we check if (1) the username they submitted does not correspond with an existing user in our database, and (2) if the supplied password does not match our existing user.

Finally, if both the username and password match, we need to create a JWT using the jwt.sign() method. This JWT consists of a few different parts which we pass into the method, two of which are most important: the "payload" and the "secret".

JWT Payload

Typically, the "payload" will be the user object you grabbed from the database. This gets encoded into the JWT and makes it so the next time a request is made and the token is verified by the server, the server can then know who (which user) is making the request. Ideally, you'll remove any sensitive information from the user object before putting it in the payload, which we'll be doing in a future part in the series.

JWT Secret

The second part, the "secret", is like a password for your JWT. When a future request comes in to the server and we go through the process of verifying it, we use the secret to ensure that the token was originally created by this server. As stated in this great article about JWTs:

"It is important to understand that the purpose of using JWTs is NOT to hide or obscure data in any way. The reason why JWTs are used is to prove that the sent data was actually created by an authentic source."

Once the token is "signed" (created and encoded), we send it to the client so it can save it and add it to every request it makes.

Set up authentication routes

The last change we need to make is in our main server index.js file to have our app use the auth routes we just made. We'll set it up just like we did with our todo routes:

// index.js
...
app.use("/auth", require("./routes/auth"));

// This was here already
app.use("/todo", require("./routes/todo.js"));
...

Set up the server to require a token on specific routes

Now that we are sending the token to the client, we want to restrict certain routes from being reached unless there is a legitimate token included in the request. For example, we want to restrict all /todo routes so that only someone who is logged in can access them. At the same time, however, we don't want to restrict the authentication routes, because how would anyone ever receive a token if they need a token to log in or create a user?

The following code in index.js uses the express-jwt middleware package to handle this, and we can specify which routes it will require a token to access. So before entering this code, we'll need to install express-jwt from your server folder

npm install express-jwt

In index.js:

// near the top with the other imports
const expressJwt = require("express-jwt");

...

// Make the app use the express-jwt authentication middleware on anything starting with "/api"
app.use('/api', expressJwt({ secret: process.env.SECRET,  algorithms: ['HS256'] }))
// We'll give expressJwt a config object with a secret and a specified algorithm
...

// Add `/api` before your existing `app.use` of the todo routes.
// This way, it must go through the express-jwt middleware before
// accessing any todos, making sure we can reference the "currently
// logged-in user" in our todo routes.
app.use("/api/todo", require("./routes/todo"));

express-jwt is a nice middleware package because it will verify the token for us, and then set the decoded user object (that we put in the "payload" of the JWT) on the request object as req.user. Which means by the time we get to our todo routes, we have access to the information of the user who is making the current request by simply accessing req.user!

Update error handling function

One tricky thing about express-jwt is that it won't set the response status for you. We could include a new middleware, but for simplicity's sake, let's just add an if statement to our existing error handling function to set the status to 401 if the error is an instance of the native UnauthorizedError. In our main server file index.js:

...
app.use((err, req, res, next) => {
    console.error(err);
    if (err.name === "UnauthorizedError") {
        // express-jwt gives the 401 status to the err object for us
        res.status(err.status);
    }
    return res.send({ message: err.message });
});
...

Modify the Todo Schema to add a reference to Users

Our todos in the database still don't actually belong to anyone yet. We need to add a reference to the user that the todo belongs to so that instead of grabbing all todos in the database, we just grab those that belong to the user currently making the request.

// models/todo.js

...
const todoSchema = new Schema({
    ...
    
    // Add a reference to the user to whom this todo belongs
    user: {
        type: Schema.Types.ObjectId,
        ref: "User",
        required: true
    }
});

Now we need to have our todo routes add the currently-logged-in user to the user property of the todo objects that get created via a POST request:

// routes/todo.js

...
todoRouter.post("/", (req, res, next) {
    const todo = new Todo(req.body);

    // Set the user property of a todo to req.user._id (logged-in user's `_id` property)
    todo.user = req.user._id;

    // Same as before
    todo.save((err, newTodo) => {
        if (err) {
            res.status(500);
            return next(err);
        };
        return res.status(201).send(newTodo);
    })
});

Set up todo routes to only return todo items of the logged-in user

This is where it all comes together—the whole reason we even need authentication is so that our server can return user-specific data. Otherwise, they're getting a huge list of everyone's todo items, and that's a pretty useless todo app (unless you're literally the only user of this app.)

Since we're assuming the user is logged in, and express-jwt has decoded the token, figured out who the current user is, and put that user object on req.user, we can now simply change a tiny bit of our todo routes code to account for the logged-in user:

// routes/todo.js

const express = require("express");
const todoRouter = express.Router();
const Todo = require("../models/todo");

todoRouter.get("/", (req, res, next) => {

    // Addition: include filtering criteria to the find so that it only finds 
    // todo items with a 'user' property with the current user's id.
    Todo.find({user: req.user._id}, (err, todos) => {
        if (err) {
            res.status(500);
            return next(err);
        };
        return res.send(todos);
    });
});

todoRouter.post("/", (req, res, next) => {
    const todo = new Todo(req.body);

    // Addition: include the user property to this new Todo item
    todo.user = req.user._id;
    todo.save(function (err, newTodo) {
        if (err) {
            res.status(500);
            return next(err);
        };
        return res.status(201).send(newTodo);
    });
});

todoRouter.get("/:todoId", (req, res, next) => {

    // Addition: Change to findOne and include the search criteria for users
    Todo.findOne({_id: req.params.todoId, user: req.user._id}, (err, todo) => {
        if (err) {
            res.status(500);
            return next(err);
        };
        if (!todo) {
            res.status(404);
            return next(new Error("No todo item found."));
        };
        return res.send(todo);
    });
});

todoRouter.put("/:todoId", (req, res, next) => {

    // Addition: Change to findOneAndUpdate and include the query for users
    Todo.findOneAndUpdate(
        // Updated query to include user
        {_id: req.params.todoId, user: req.user._id},
        req.body,
        {new: true},
        (err, todo) => {
            if (err) {
                res.status(500)
                return next(err);
            };
            return res.send(todo);
        }
    );
});

todoRouter.delete("/:todoId", (req, res, next) => {

    // Addition: Change to findOneAndRemove and include the search criteria for users
    Todo.findOneAndRemove({_id: req.params.todoId, user: req.user._id}, (err, todo) => {
        if (err) {
            res.status(500);
            return next(err);
        }
        return res.send(todo);
    });
});

module.exports = todoRouter;

Now our server will automatically check for the user property on every call to the database so that it returns only a subset of Todo items - those with the userId of the currently-logged-in user inside their user property!


Test it!

All we have left to do is test our setup using Postman! This will require creating a couple different users through the /auth/signup API endpoint we set up, then logging in as that new user with the /auth/login API endpoint and getting a token. Then we can create a new todo item with a POST request to the /api/todo API endpoint, remembering to include the JWT in the Authorization Header of the request, then trying out our GET, PUT, and DELETE requests for different users. We should see that the todos we're dealing with aren't all the todos in the database anymore, but rather just the ones that were created by the user whose token is being sent along with the requests. Let's try it out.

Create 2 new users

In Postman, make a POST request to /auth/signup
1) {"username": "bob", "password": "bob123"}

2) {"username": "sarah", "password": "sarah123"}

These requests should return a token, so that when we're building the front end we don't have to require the user to login right after signing up. (That'd be a little annoying.)


Sign in as one of the users

This should also send a token back with the response. This endpoint is for when they've already signed up and are coming back to the site and need to log in.


Use the token in a request to create a new Todo item

First copy the token from the login response (don't include the quotation marks, just copy the string), then add it to a new Authorization header in the POST request to localhost:5000/api/todo. The key of the header is Authorization, and the value is Bearer <token>.

Don't forget to add the properties of the todo item you want to create.

Notice the todo item now has a user property which is the same as the _id of the logged-in user!

Repeat

At this point you should be able to repeat these steps to log in as your other user, create a new todo with a POST request, and start testing the GET, POST, PUT, and DELETE requests of each of the users. You'll see that their data is separated now, and any actions taken by one user don't affect another user's data!


Conclusion

We'll be revisiting the server later when it comes time to add some security features - right now our users' passwords are getting saved in plain text to the database, and that password is being sent back to the user and exists inside the token. This is no bueno, but it will do for now.

If you got a little lost while following along or can't figure out where something went wrong, take a look at the part-1-server-auth-end branch to compare your work with how it should look. You can either commit your changes and run git checkout part-1-server-auth-end or just take a look at the files in the Github repository

Next steps

You have the option to choose your own adventure from here! This tutorial series splits into 2 branches from here - one using Redux for state management and one using React's native Context API for state management.

Choose wisely!