In Part 2 of this tutorial series, we continued working on our Express server. We did the following:
- Added the rest of the endpoints that will be needed for our server
- Created a user mongoose model for our database
- Created the connection for connecting to MongoDB
- Started updating the routes to send and retrieve data from MongoDB
In Part 3 of this tutorial series, we will continue working on our server by updating the rest of the endpoints for sending data to MongoDB, and we will add authentication to our endpoints.
You can download all of the files associated with the source code for Part 3 here.
If you didn’t complete Part 2 and would like to continue from there, you can find the code for it here.
Let’s get started!
Updating Score Routes
Currently, the /scores and /submit-score endpoints just have placeholder logic that will return a 200 response code. We will now update the /scores endpoint to get the top ten high scores from the database, and update the /submit-score endpoint to update a single user’s score. To do this, open routes/secure.js and replace all of the code in the file with the following:
const express = require('express');
const asyncMiddleware = require('../middleware/asyncMiddleware');
const UserModel = require('../models/userModel');
const router = express.Router();
router.post('/submit-score', asyncMiddleware(async (req, res, next) => {
const { email, score } = req.body;
await UserModel.updateOne({ email }, { highScore: score });
res.status(200).json({ status: 'ok' });
}));
router.get('/scores', asyncMiddleware(async (req, res, next) => {
const users = await UserModel.find({}, 'name highScore -_id').sort({ highScore: -1}).limit(10);
res.status(200).json(users);
}));
module.exports = router;
Let’s review the code we just added:
- First, we importedÂ
asyncMiddleware andUserModel, and then we wrapped both of the routes in theasyncMiddleware middleware we created in part two. - Next, in the
submit-score route, we grabbed theemail andscore values from the request body. We then used theupdateOne method on theUserModel to update a single document in the database where the providedemail value matches theemail property on the record. - Then, in the
/scores route, we used thefind method on theUserModel to search for documents in the database.- The
find method takes two arguments, the first is an object that is used for limiting the documents that are returned from the database. By leaving this as an empty object, all documents will be returned from the database. - The second argument is a string that allows us to control which fields we want to be returned on the results that are returned to us. This argument is optional, and if it is not provided then all fields will be returned. By default, the
_id field will always be returned, so to exclude it we need to use-_id in this argument.
- The
- We then called the
sort method to sort the results that are returned. This method allows you to specify the field you would like to sort by, and by setting that value to-1 the results will be sorted in descending order. - Finally, we called the
limit method to make sure we return 10 documents at max.
Now, if you save your code changes and start the server, you will be able to test the updated routes. To test the /submit-score endpoint, you will need to use curl, Postman, or some other method for sending a POST request. For the rest of this tutorial, we will be using curl.
In the terminal, open a new tab or window and enter the following code:
curl -X POST \
http://localhost:3000/submit-score \
-H 'Content-Type: application/json' \
-d '{
"email": "[email protected]",
"score": "100"
}'Once you submit the request, you should get the status ok message. To test the /scores endpoint, open a new tab in your browser and visit the following URL: http://localhost:3000/scores. You should see an array of objects that include the name and highscore fields.
Authentication
Now that the score endpoints are connected to our database, we will now start working on adding authentication to our server. For the authentication, we will be using passport.js along with passport-jwt. Passport is an authentication middleware for Nodejs that can easily be used with Express, and it supports many different types of authentication.
Along with passport, we will be using JSON web tokens for validating users. In addition to using a JWT for authenticating users, we will also be using a refresh token to allow a user to update their main JWT. The reason we are doing this is that we want the main JWT to be short-lived, and instead of requiring the player to have to keep re-logging in to get a new token, they can instead use their refresh token to update the main JWT, since it will be long-lived.
To get started, the first thing we need to do is install the required packages. In the terminal, run the following command:
npm install --save cookie-parser passport passport-local passport-jwt jsonwebtoken
This will install passport along with the two strategies we will need for our server. passport-local allows us to authenticate with a username and password on our server. passport-jwt allows us to authenticate with a JSON web token. Lastly, cookie-parser allows us to parse the cookie header and populate,req.cookies which is where we will be placing the JWT that is generated.
To get started, create a new folder in the root folder called auth and inside this file create a new file called auth.js. In auth.js, add the following code:
const passport = require('passport');
const localStrategy = require('passport-local').Strategy;
const JWTstrategy = require('passport-jwt').Strategy;
const UserModel = require('../models/userModel');
// handle user registration
passport.use('signup', new localStrategy({
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
}, async (req, email, password, done) => {
try {
const { name } = req.body;
const user = await UserModel.create({ email, password, name});
return done(null, user);
} catch (error) {
done(error);
}
}));In the code above, we did the following:
- First, we imported the
passport,passport-local, andpassport-jwt. Then, we imported theuserModel. - Next, we configured
passport to use a new local strategy when thesignup route is called. ThelocalStrategy takes two arguments: anoptions object and a callback function.- For the
options object, we set theusernameField andpasswordField fields. By default, if these fields are not providedpassport-local will expect theusername andpassword fields, and if we want to use a different combination we need to provide them. - Also, in the
options object, we set thepassReqToCallback field and we set it totrue. By setting this field totrue, the request object will be passed to the callback function.
- For the
- Then, in the callback function, we took the logic for creating the user that was in the
signup route and we placed it here. - Lastly, we called the
done function that was passed as an argument to the callback function.
Before we update the server and routes to use the new authentication, we will need to finish configuring passport. In auth.js, add the following code at the bottom of the file:
// handle user login
passport.use('login', new localStrategy({
usernameField: 'email',
passwordField: 'password'
}, async (email, password, done) => {
try {
const user = await UserModel.findOne({ email });
if (!user) {
return done(null, false, { message: 'User not found' });
}
const validate = await user.isValidPassword(password);
if (!validate) {
return done(null, false, { message: 'Wrong Password' });
}
return done(null, user, { message: 'Logged in Successfully' });
} catch (error) {
return done(error);
}
}));
// verify token is valid
passport.use(new JWTstrategy({
secretOrKey: 'top_secret',
jwtFromRequest: function (req) {
let token = null;
if (req && req.cookies) token = req.cookies['jwt'];
return token;
}
}, async (token, done) => {
try {
return done(null, token.user);
} catch (error) {
done(error);
}
}));Let’s review the code we just added:
- We set up another local strategy for the
login route. Then in the callback function, we took the logic for logging a user in from thelogin route and we placed it here. - Next, we configured passport to use a new JWT strategy. For the JWT strategy, we provided two arguments: an options object and a callback function.
- In the options object, we provided two fields:
secretOrKey andjwtFromRequest. secretOrKey is used for signing the JWT that is created. For this tutorial, we used a placeholder secret and normally you would want to pull this from your environment variables or use some other secure method, and you would want to use a much more secure secret.jwtFromRequest is a function that is used for getting thejwt from the request object. For this tutorial, we will be placing thejwt in a cookie, so in the function, we pull thejwt token from the request object cookie if it exists otherwise we returnnull.- Lastly, in the callback function, we call the
done function that was provided to the callback.
- In the options object, we provided two fields:
Now that we have finished configuring passport, we need to update app.js to use passport. To do this, open app.js and add the following code at the top of the file with the other require statements:
const cookieParser = require('cookie-parser');
const passport = require('passport');Then, add the following code below the `app.use(bodyParser.json());` line:
app.use(cookieParser());
// require passport auth
require('./auth/auth');Finally, replace this line: `app.use(‘/’, secureRoutes);` with the following code:
app.use('/', passport.authenticate('jwt', { session : false }), secureRoutes);In the code above, we did the following:
- First, we imported
cookie-parser andpassport. - Then, told our express app to use
cookie-parser. By usingcookie-parser, the request object will have the cookies included. - Lastly, we imported our auth file and then updated our secure routes to use the passport JWT strategy we set up.
The last thing we need to do before we can test our code changes is, update the routes in routes/main.js. To do this, open routes/main.js and replace all of the code in the file with the following code:
const passport = require('passport');
const express = require('express');
const jwt = require('jsonwebtoken');
const tokenList = {};
const router = express.Router();
router.get('/status', (req, res, next) => {
res.status(200).json({ status: 'ok' });
});
router.post('/signup', passport.authenticate('signup', { session: false }), async (req, res, next) => {
res.status(200).json({ message: 'signup successful' });
});
router.post('/login', async (req, res, next) => {
passport.authenticate('login', async (err, user, info) => {
try {
if (err || !user) {
const error = new Error('An Error occured');
return next(error);
}
req.login(user, { session: false }, async (error) => {
if (error) return next(error);
const body = {
_id: user._id,
email: user.email
};
const token = jwt.sign({ user: body }, 'top_secret', { expiresIn: 300 });
const refreshToken = jwt.sign({ user: body }, 'top_secret_refresh', { expiresIn: 86400 });
// store tokens in cookie
res.cookie('jwt', token);
res.cookie('refreshJwt', refreshToken);
// store tokens in memory
tokenList[refreshToken] = {
token,
refreshToken,
email: user.email,
_id: user._id
};
//Send back the token to the user
return res.status(200).json({ token, refreshToken });
});
} catch (error) {
return next(error);
}
})(req, res, next);
});
router.post('/token', (req, res) => {
const { email, refreshToken } = req.body;
if ((refreshToken in tokenList) && (tokenList[refreshToken].email === email)) {
const body = { email, _id: tokenList[refreshToken]._id };
const token = jwt.sign({ user: body }, 'top_secret', { expiresIn: 300 });
// update jwt
res.cookie('jwt', token);
tokenList[refreshToken].token = token;
res.status(200).json({ token });
} else {
res.status(401).json({ message: 'Unauthorized' });
}
});
router.post('/logout', (req, res) => {
if (req.cookies) {
const refreshToken = req.cookies['refreshJwt'];
if (refreshToken in tokenList) delete tokenList[refreshToken]
res.clearCookie('refreshJwt');
res.clearCookie('jwt');
}
res.status(200).json({ message: 'logged out' });
});
module.exports = router;
Let’s review the code we just added:
- First, we imported
jsonwebtoken andpassport, and then we removed `userModel` and `asyncMiddleware`. - Next, in the
signup route, we added thepassport.authenticate middleware and set it to use the passportsignup configuration we created. Since we moved all of the logic for creating the use to theauth.js file, the only thing we need to do in the callback function is to return a 200 response. - Then, in the
login route we added thepassport.authenticate middleware and set it to use the passportlogin configuration we created.- In the callback function, we first check to see if there was an error or if a user object was not returned from the passport middleware. If this check is true, then we create a new error and pass it to the next middleware.
- If that check is false, we then call the
login method that is exposed on the request object. This method is added by passport automatically. When we call this method, we pass theuser object, an options object, and a callback function as arguments. - In the callback function, we create two JSON web tokens by using the `jsonwebtoken` library. For the JWTs, we include the id and email of the user in the JWT payload, and we set the main
token to expire in five minutes and therefreshToken to expire in one day. - Then, we stored both of these tokens in the response object by calling the
cookie method, and we stored these tokens in memory so we can reference them later in the token refresh endpoint. Note: for this tutorial, we are storing these tokens in memory, but in practice, you would want to store this data in some type of persistent storage. - Lastly, we responded with a 200 response code and in the response, we send the
token andrefreshToken.
- Next, in the
token route, we pulled theemail andrefreshToken from the request body. We then checked to see if theÂrefreshToken is in thetokenList object we are using for tracking the user’s tokens, and we made sure the providedemail matches the one stored in memory.- If these do not match, or if the token is not in memory, then we respond with a 401 status code.
- If they do match, then we create a new token and store it in memory and update the response cookie with the new token.
- We then respond with a 200 response code and in the response, we send the new
token.
- Finally, in the
logout route, we check to request object has any cookies.- If the request object does have any cookies, then we pull the `refreshJwt` from the cookie and delete it from our in-memory token list if it exists.
- We then clear the
jwt andrefreshJwt cookies by calling theclearCookie method on the response object. - Lastly, we respond with a 200 response code.
Now that the routes have been updated, you can test the new authentication. To do this, save your code changes and restart your server. If you try sending a request to the `submit-score` and scores endpoints, you should a 401 unauthorized message.
If you send a login request, you should get a response that includes the token and refreshToken.
Conclusion
Now that we have finished setting up the user authentication, that brings Part 3 of this tutorial to an end. In Part 4, we will start working on our game by adding a login page, which will then take the player to the Phaser game after they log in.
I hope you enjoyed this tutorial and found it helpful. If you have any questions, or suggestions on what we should cover next, please let us know in the comments below.






