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!
BUILD GAMES
FINAL DAYS: Unlock 250+ coding courses, guided learning paths, help from expert mentors, and more.
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.