User Authorization

We don’t want arbitrary users to be able to log a user out, or worse, modify or delete a user’s profile. Therefore we need to create a process to authenticate a user and a way for a user to prove that they are authorized to perform certain actions like modify and delete their own profile.  In our app, users authenticate themselves by submitting proper credentials (email and password).  When they do, we will create an authorization token and return it to the client.  The client will then pass the authorization token to the API server whenever the client wants the API server to perform some action requiring user authorization.

In this tutorial, we’ll learn how to generate JSON Web Tokens (JWT) for our authorization tokens. A JWT is either signed or encrypted.  If it is signed it is a JSON Web Signature (JWS).  If it is encrypted, it is a JSON Web Encryption (JWE).  We will be using signed tokens.

A JWS is a string that has 3 parts: a header, a payload, and a signature; each separated by a period.  The header specifies how the token is signed.  The payload is a JSON object that holds some claim.  For example, your API will require the client to present a token to prove the user’s identity.  Thus the claim inside the payload of the token will have something that establishes the user’s identity (e.g. the user’s document id).  The signature is a representation of the header + payload which is encrypted using a secret key.  Only those who possess the secret key (e.g. your API server) can validate the signature of tokens produced with that same secret key.  Therefore malicious actors cannot create forged tokens, because they won’t have a signature that will pass the validation process.  Malicious actors can however masquerade as another user if they possess a valid token for the user, but we can avoid this by using HTTPS which encrypts all communications between the client and server.  If you want to learn more about JWTs, I’ve found this website to be helpful.

When the server receives a request for an endpoint that requires an authorization token (such as /user/logout), the server will call a middleware function (before the endpoint router code is executed) that checks to see if the authorization token is in the database.  If the token is found, then the user’s document containing the token will then be passed to the router thus giving the router the identity of the user.

In this tutorial, we’ll update the POST /user (create account) endpoint to create an authorization token and save the token in the user’s document.

Install the jsonwebtoken npm Module

We are going to create JSON Web Tokens using the jsonwebtoken npm module.  Let’s begin by installing the module.

$ npm install jsonwebtoken

Set JSON_WEB_TOKEN_SECRET

We will be creating signed JWSs using the jsonwebtoken module’s sign() method.  We’ll be passing to sign() an object containing the user’s id (the document id) along with a secret phrase that will be used to create the token’s signature.

Add the following line to your .env file, replacing <your-secret-phrase> with a secret phrase that you want to use to create your token signatures.  Make the phrase at least 32 characters long.

JSON_WEB_TOKEN_SECRET=<your-secret-phrase>

Now add your JSON_WEB_TOKEN_SECRET to Azure’s app configuration set in the Azure portal.

Generate and Send a Token

Since we need to generate a token when a user creates an account and when they log in, let’s create an instance method to hold the code that generates a JSON Web Token so it can be called in multiple endpoint handlers.  After we’ve created the instance method to generate tokens, we’ll update the POST /user endpoint handler to call the method.

Create an Instance Method to Generate Tokens

We saw earlier that we can create instance methods by adding functions to a schema’s methods property.   Let’s add a new instance method, that when invoked on a User document, creates a signed token, adds the token to the array of tokens in the user’s document, saves the document in the database, and returns the token to the caller.

Since we’ll be using the jsonwebtoken module to create the token, import the module at the top of src/models/user.js.

const jwt = require('jsonwebtoken')

Add the following instance method in …/src/models/user.js and read the explanation below.

userSchema.methods.generateAuthToken = async function () {
  const user = this
 
  const token = jwt.sign({ _id: user._id.toString() }, process.env.JSON_WEB_TOKEN_SECRET)

  user.tokens = user.tokens.concat(token)
  await user.save()

  return token
}
Explanation of the Code

We use a function on line 1, rather than a lambda expressions because we need access to the instance (this, on line 2) on which generateAuthToken is called.  We also require the async keyword because we have to wait for user.save() to return before returning from the method.

On line 4 we call the jsonwebtoken module’s sign() method, passing to it, an object containing value of the document’s id property (as a string), and the secret string.  The object that we passed becomes the payload of the token.

On line 6 we add a new object containing the new token to the document’s tokens array using the array’s concat() method.  The concat() method returns a new array so we set users.tokens equal to the new array created by concat() and then save the  modified user document to the database on line 7.

On line 9 we return the token to the caller.

Update the POST /user Endpoint

Now in the handler for the POST /user endpoint, after we create a new User document and save it to the database, we’ll call generateAuthToken() on the new document instance.

Update your POST /user endpoint handler so that it looks like the code below.

router.post('/user', async (req, res) => {
  
  ...
  
  const user = new User(req.body)

  try {
    await user.save()
    const token = await user.generateAuthToken()

    sendVerificationEmail(user.email, user.username, token)
    res.status(201).send(user)
  } 
  catch(error) {
    res.status(400).send(error)
  }
})

Note that we must use the await keyword when calling generateAuthToken(), since we have to wait for the token to be generated before we can continue.  Note also that we are now also passing the token to sendVerificationEmail().  We will update sendVerificationEmail() in the next tutorial.

Create Middleware to Validate Tokens

Express middleware is code that is executed by express after a request has been received, but before the endpoint code in the router is executed.

A middleware function is async and has 3 parameters (req, res, next). The req parameter is the Request object that is received from the client and res is the Response object that will be sent to the client.   next is a method that is called within the middleware if and when it has been decided that the next middleware routine, or perhaps the router’s endpoint handler, should be invoked.

We are going to create a middleware function named auth() that will be called before any endpoint that requires authorization is invoked.  auth() will validate the JSON Web Token sent from the client, and if valid, will call next() so that the endpoint code is executed.  If the token is not present or is invalid, auth() will throw an error rather than calling next().

Create the following directory and file to store the middleware function named auth().

.../src/middleware
.../src/middleware/auth.js

Add the following auth() function definition to …/src/middleware/auth.js, then read the explanation of the code below.

const jwt = require('jsonwebtoken')
const User = require('../models/user')

const auth = async (req, res, next) => {
  try {
    let token = req.header('Authorization')
    //console.log(token)
    token = token.replace('Bearer ', '')
    const decoded = jwt.verify(token, process.env.JSON_WEB_TOKEN_SECRET)
    const user = await User.findOne({_id: decoded._id, 'tokens': token})

    if (!user) {
      throw new Error()
    }

    req.token = token
    req.user = user
    next()
 
  } catch (e) {
    res.status(401).send({error: 'Please authenticate.'})
  }
}

module.exports = auth
Explanation of the Code

Lines 4-23 create an asynchronous function that the router will call when directed to.  We will see in the next section how to direct a router to execute middleware.

The arguments to the function, specified on line 4, contain the request, the response, and a function named next that is called when we want the router to continue processing the request.

Line 6 creates a variable named token which we will modify later (hence the use of let instead of const).  We initialize the variable with the Authorization string (JWT) retrieved from the request sent from the client. And on line 8, we strip the word ‘Bearer ‘ from the beginning of the Authorization string.

We call jwt.verify() on line 9, passing to it, the JWT (token) and the JWT secret.  verify() returns the decoded payload (a JSON object containing the user’s document id) if the signature in the token is valid.

We then, on line 10, pass an object containing the user’s id and the token to User.findOne().  This is a mongoose method that searches the User collection for a document containing both the specified id and token.  If found, it returns the user document.

On lines 12-14 we throw an error if the user was not found.

Otherwise, on lines 16 and 17, we add the token and user properties to the request.  Recall that router has still not called the endpoint handler.  When it does, the token and user objects will be included in the request available for the handler to use.

On line 18, we call next() to notify the router to continue processing the request.

If any of the above code throws an error, on line 21, we send the client a 401 status and an error message.

How to Invoke auth()

For endpoints that require authorization we can call the auth() middleware function by simply importing the function and placing the name of the function between the endpoint and the endpoint handler.  For example,

const auth = require('../middleware/auth') 
... 

router.post('my-endpoint', auth, async (req, res) => { 
    ... 
})

Test POST /user

Though we can’t yet test the auth() middleware, you can test generateAuthToken() to ensure that tokens are created and appended to the tokens array in the user’s document.

Create a user in Postman on localhost and verify using Compass that a token has been added to the user document in the database.  If all is well on localhost, push your code to GitHub and test your API server running on Azure.

 

 

© 2024, Eric McGregor. All rights reserved. Unauthorized use, distribution and/or duplication of this material without express and written permission from the author is strictly prohibited.