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!
Table of contents
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": "t[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 therefreshToken
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.