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
andcompleted
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 theSECRET
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!