Week 8
Access Control with JWT

Assignment Due

Assignment 2 - Mongo CRUD - is due before 1:00 pm on Friday, March 6th. It is worth 10% of your final grade.

Quiz 6: Data Sanitization 15 mins

There will be a quiz today. It will be worth 2% of your final grade.

Agenda

  • AMA (10 mins)
  • Quiz (10 mins)
  • Register new users (30 mins)
  • Break (10 mins)
  • Login a user (30 mins)
  • Break (10 mins)
  • OOP Information Expert Principle (30 mins)
  • Break (10 mins)
  • Authentication guard middleware (30 mins)

EX8 Authentication & Authorization

Objective

Create the modules necessary to support user registration and authentication.

We will pickup where we left off last week. Clone this GitHub Classroom starter repo for a working version of the code from that exercise.

Auth Router

It is a common practice to separate the authentication related routes from the main API routes. This has several advantages. Chief among them is making it easier to separate user management into a separate micro service or outsource it to a third party like AWS Congnito or Auth0.

Start by creating a new folder in the /routes folder called auth. In that new folder, create a new module called index.js with this placeholder content. We will use it to prove that everything is wired-up correctly and then come back to add the real code.

const express = require('express')
const router = express.Router()

// Register a new user
router.post('/users', async (req, res) => {
  res.status(201).send({ data: 'new user created.' })
})

module.exports = router

Tell Express to use that router in app.js.

app.use('/auth', require('./routes/auth'))

Test it with Postman

Register a new user

Mongoose Model

We need a Mongoose Model so that we can actually talk to the database. Let's keep it simple for now with just four properties: firstName, lastName, email, and password.

Create a module called User.js in the /models folder.

const mongoose = require('mongoose')

const schema = new mongoose.Schema({
  firstName: { type: String, trim: true, maxlength: 64, required: true },
  lastName: { type: String, trim: true, maxlength: 64 },
  email: { type: String, trim: true, maxlength: 512, required: true },
  password: { type: String, trim: true, maxlength: 70, required: true }
})

const Model = mongoose.model('User', schema) // factory function returns a class

module.exports = Model

POST /auth/users

In the /routers/auth/index.js module, we can now update the route handler for creating new users. To save time, let's borrow the POST method from the cars router as a starting point and then modify it for our User model.

router.post('/users', sanitizeBody, async (req, res) => {
  try {
    let newUser = new User(req.sanitizedBody)
    await newUser.save()
    res.status(201).send({ data: newUser })
  } catch (err) {
    res.status(500).send({
      errors: [
        {
          status: 'Internal Server Error',
          code: '500',
          title: 'Problem saving document to the database.'
        }
      ]
    })
  }
})

Don't forget to require the User model at the top of the router module.

OK. Test it with Postman.

screenshot of Postman

That works, but ...

trump meme - yuuuuge problem

The password is saved in plain text. That is an enormous security risk!

Encrypt password

Obviously passwords should never be stored in plain text. The current best practice for storing passwords is to use a strong cryptographic algorythm to generate a one-way hash. That means that once the password is encrypted, there is no way to decrypt it.

Wait a minute ... how do we process a login request?

To validate a user supplied password, we encrypt it and then compare that against the encrypted password stored in the database. Given the same inputs, the encrypted hash values should also be the same.

bcrypt

bcrypt is the most widely used encryption library. It has been ported to most popular programming languages including, JavaScript, PHP, Python, C#, and Java.

Let's add bcrypt as a project dependancy.

npm install bcrypt

At the top of the Auth Router module, import the bcrypt library. Then create a new variable called saltRounds and set it to 14.

const bcrypt = require('bcrypt')
const saltRounds = 14

What is a salt?

A salt is random data that is used in cryptography as additional input along with the user supplied data. This ensures that even if two users have the same plain text password, the hashed values stored in the database will look different.

A new salt is randomly generated for each password. Salts defend against dictionary attacks or against their hashed equivalent, a pre-computed rainbow table attack.

Cryptographic Cost

When hashing the input data, bcrypt will go through a series of iterations or rounds to finally generate a secure hash. The saltRounds value is applied not as an integer, but as an exponent. Each hash generated will go through 2^saltRounds iterations.

Given a typical 2Ghz core processor, you could expect hashing throughput similar to this ...

rounds=8 : ~40 hashes/sec
rounds=9 : ~20 hashes/sec
rounds=10: ~10 hashes/sec
rounds=11: ~5  hashes/sec
rounds=12: 2-3 hashes/sec
rounds=13: ~1 sec/hash
rounds=14: ~1.5 sec/hash
rounds=15: ~3 sec/hash
rounds=25: ~1 hour/hash
rounds=31: 2-3 days/hash

We want to make the hashing process as slow as is tollerable by legitimate users. The slower it is, the greater deterent against brute-force hacking attemtps. The default value is 10. It is recommended to use a higher value of between 13 and 15.

Regrdless of the number salt rounds, the resulting hashed value will always be 60 characters long.

hash()

Now, in the 'create user' route handler, use the hash method to encrypt the password. We will replace the user supplied password with an encrypted version before saving to the database.



 



// ...
let newUser = new User(req.sanitizedBody)
newUser.password = await bcrypt.hash(newUser.password, saltRounds)
await newUser.save()
// ...

OK. Test it with Postman.

screenshot of Postman

Unique email

We will use the user's email property as the login name. So, we need these to be unique in our users collection, and we need an index on that property to speed up the querries.

In the User model, modify the schema to add unique: true to the email proptery. This will tell MongoDB to create a 'unique index' on the email property.

WARNING

The unique Option is Not a Validator. It is a helper method for creating an index with a unique value constraint.

If we attempt to save a new User with the same email address as another User, MongoDB will throw an error ...

week8:auth Error saving new user:
E11000 duplicate key error collection:
mad9124.users index: email_1 dup key: { : "mckennr@algonquincollege.com" }

We must query the database to validate that the new email address is not already registered before saving the new User. To make this query as fast as possible, we don't need to read the actual collection data, we can just see if it is in the index.

The countDocuments() method on the User model takes a MongoDB query expression and returns the number of matching occurrances in the collection. The JavaScript !! operator coerces the result into a Boolean value (true or false).

//...
let newUser = new User(req.sanitizedBody)
const itExists = !!(await User.countDocuments({ email: newUser.email }))
if (itExists) {
  // return error
}
newUser.password = await bcrypt.hash(newUser.password, saltRounds)
//...

That is the core logic for the unique check. Now, let's expand the handling of the valiation error.

if (itExists) {
  return res.status(400).send({
    errors: [
      {
        status: 'Bad Request',
        code: '400',
        title: 'Validation Error',
        detail: `Email address '${newUser.email}' is already registered.`,
        source: { pointer: '/data/attributes/email' }
      }
    ]
  })
}

TIP

Calling the res.send() method does not halt execution. So we need the return keyword to exit the route handler function.

Login a user

OK. You can now successfully register new users. It is time to think about how to handle authenticating them. What should the RESTful URI be? You might think to try POST /auth/user/:id/login, but that breaks some of the "rules" by overloading the primary resource path and adding an action verb.

It might help if I told you that we would send the client back a "security token" upon successful authentication. The client will then send that back as a header variable to each API call. So, in RESTful terms, what we really want to do is create a new authentication token. This makes more sematic sense.

POST /auth/tokens

This resource path will support only the POST action and will expect the payload to contain the username/loginName and password. Near the bottom of the Auth Router module, let's pseudo code this new route handler.

// Login a user and return an authentication token.
router.post('/tokens', sanitizeBody, async (req, res) => {
  // check if the payload.username is valid
  // retrieve the stored password hash
  // compare the payload.password with the hashed password
  // if all is good, return a token
  // if any condition failed, return an error message
})

OK. How do we do that? Let's take them one at a time. Use the mongoose findOne() method on the User model to search for a matching email. If we find it, then we also have the hashed password. If not, send an error response with a 401 status code.

TIP

The Mongoose findOne() method returns null if there is no matching record in the database.

// Login a user and return an authentication token.
router.post('/tokens', sanitizeBody, async (req, res) => {
  const { email, password } = req.sanitizedBody
  const user = await User.findOne({ email: email })
  if (!user) {
    return res.status(401).send({ errors: ['we will build this later'] })
  }
  // compare the payload.password with the hashed password
  // if all is good, return a token
  // if any condition failed, return an error message
})

Next, use the bcrypt.compare() method to validate the client supplied password.

// Login a user and return an authentication token.
router.post('/tokens', sanitizeBody, async (req, res) => {
  const { email, password } = req.sanitizedBody
  const user = await User.findOne({ email: email })
  if (!user) {
    return res.status(401).send({ errors: ['we will build this later'] })
  }

  const passwordDidMatch = await bcrypt.compare(password, user.password)
  if (!passwordDidMatch) {
    return res.status(401).send({ errors: ['we will build this later'] })
  }
  // if all is good, return a token
  // if any condition failed, return an error message
})

If we got this far, everything is good and we can return a token. Add these two lines as a placeholder for the moment. We will look at how to generate an industry standard token in just a minute.

const token = 'iamatoken'
res.status(201).send({ data: { token } })

This should be enough to test our logic. Let's go to our old friend Postman.

screenshot of successful login with Postman

It works! But, there is a marked difference in the response time when we give it an invalid email. Hackers can use this timing difference to their advantage. Let's refactor this to mask that difference.

To make sure the timing is consistent, we need the bycrypt.compare() function to run everytime. So, change it to compare to a new variable called hashedPassword rather than user.password. Then conditionally set hashedPassword.

If there was no User with a matching email (username) then we can set a fake value for the hashedPassword that will never get a valid match.

TIP

The first three characters signify the bcrypt version. The next three signify the number of salt rounds. The next 22 characters are the salt and the remainder is the encrypted password.

const user = await User.findOne({ email: email })

const hashedPassword = user
  ? user.password
  : `$2b$${saltRounds}$invalidusernameaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`

const passwordDidMatch = await bcrypt.compare(password, hashedPassword)

if (!user || !passwordDidMatch) {
  return res.status(401).send({ errors: ['we will build this later'] })
}

const token = 'iamatoken'
res.status(201).send({ data: { token } })

Test it again. Great!

Create a JWT

It is time to generate a real token. We will use the defacto industry standard JSON Web Tokens (JWT). There are libraries for just about every popular programming language. Let's install the Node.js module.

npm install jsonwebtoken

You should read the full documentation on JWTs but here are two critical things to understand.

  1. The payload is hashed, not encrypted. Anyone with some JavaScript can read it. So, never put sensitive data in the token payload.

  2. The token is cryptographically signed using a secret key on your sever. So we can validate that no one has altered the contents of the payload. Which means we can trust it.

Let's check the usage instructions ...

We need to require the library at the top.

const jwt = require('jsonwebtoken')

Then in the route handler we will update the token assignment line. For the payload, we will just supply the authenticated User's the unique _id property.

We will hard code the secret key for now -- never do this in production!

const token = jwt.sign({ _id: user._id }, 'superSecureSecret')

Test it in Postman ...

screenshot of Postman with a real JWT response

If you copy the token returned in Postman and paste it into the JWT Debugger at jwt.io you can verify the contents of the payload.

screenshot of the jwt.io website

Yay! This works.

However, we are starting to get more and more logic about the User model leaking into our router module. In a small application, this is not really a big deal. But as an application grows in complexity this can lead to unwanted tight coupling between modules - making it harder to know where to look for implementation code, and harder to swap out the implementation to a third party web service or our own independent microservice.

The solution is to follow the OOP Information expert principle and move some of this logic to custom methods defined in our User model class.

Custom instance methods

The mongoose.Schema class defines a methods object to which we can attatch our own custom instance methods. These methods will be available to all Mongoose document objects created with this schema.

In this case, go to the /models/User.js file and before calling the mongoose.model() function, add a new property to the userSchema.methods called generateAuthToken and set it to hold a regular function. Then paste in the jwt.sign() method from our router module.

TIP

Since we are creating an "instance method", we need to update the payload. There is no user variable. Instead we can use the special this keyword to refer to the instantiated object.

 


 
 
 




const jwt = require('jsonwebtoken')
const mongoose = require('mongoose')
// ...
schema.methods.generateAuthToken = function () {
  return jwt.sign({ _id: this._id }, 'superSecureSecret')
}

const Model = mongoose.model('User', schema)
module.exports = Model

WARNING

Do not use an arrow function here. We need this inside the function to reference the instatiated model object.

Now update the route handler to call this new method ...
















 


router.post('/tokens', sanitizeBody, async (req, res) => {
  const {email, password} = req.sanitizedBody

  const user = await User.findOne({email: email})

  const hashedPassword = user
    ? user.password
    : `$2b$${saltRounds}$invalidusernameaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`

  const passwordDidMatch = await bcrypt.compare(password, hashedPassword)

  if (!user || !passwordDidMatch) {
    return res.status(401).send({errors: ['we will build this later']})
  }

  res.status(201).send({data: {token: user.getAuthToken()}})
}

Custom class methods

Similarly, the route handler should not need to know about the password encryption implementation. We can relocate the authentication logic to a static class method and call it like User.authenticate(email, password) and expect either a User instance or null in return.

This time add a new authenticate property to the schema.statics object in the /models/User.js module. Copy over the logic from the router module and remember to change User to this.

const bcrypt = require('bcrypt')
const saltRounds = 14
// ...
schema.statics.authenticate = async function (email, password) {
  const user = await this.findOne({ email: email })
  const hashedPassword = user
    ? user.password
    : `$2b$${saltRounds}$invalidusernameaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`
  const passwordDidMatch = await bcrypt.compare(password, hashedPassword)

  return passwordDidMatch ? user : null
  // remember if the email did not match, user === null
}

And update the auth router module ...

router.post('/tokens', sanitizeBody, async (req, res) => {
  const { email, password } = req.sanitizedBody
  const user = await User.authenticate(email, password)

  if (!user) {
    return res.status(401).send({ errors: ['we will build this later'] })
  }

  res.status(201).send({ data: { token: user.generateAuthToken() } })
}

Test it with Postman to make sure that everything still works ... yay!

One last change to the POST /auth/tokens route handler ... let's put in a propper error message.

if (!user) {
  return res.status(401).send({
    errors: [
      {
        status: 'Unauthorized',
        code: '401',
        title: 'Incorrect username or password.'
      }
    ]
  })
}

Schema lifecylce hooks

There is one more implementation detail that is in the router module that should be moved to the User model. Currently the registration route is taking care of encrypting the password before saving the new User. It shouldn't need to know about this.

Mongoose Schema to the resscue again!

Mongoose provides some special middleware functions that are triggered by lifecycle events. We will use one that is triggered just before a model is saved to do the password encryption.

Add this code block to the /models/User.js module just below the custom methods that we added eariler.

schema.pre('save', async function (next) {
  // Only encrypt if the password property is being changed.
  if (!this.isModified('password')) return next()

  this.password = await bcrypt.hash(this.password, saltRounds)
  next()
})

We can now delete these lines from the router module ...

const bcrypt = require('bcrypt')
const saltRounds = 14
//...
newUser.password = await bcrypt.hash(newUser.password, saltRounds)

OK. This is a much cleaner implementation. The router does routing and the implementation details of how to manage password encryption and authenticate a user are in the User model.

How does our code look now?

Auth Router

const debug = require('debug')('week8:auth')
const User = require('../../models/User')
const sanitizeBody = require('../../middleware/sanitizeBody')
const express = require('express')
const router = express.Router()

// Register a new user
router.post('/users', sanitizeBody, async (req, res) => {
  try {
    let newUser = new User(req.sanitizedBody)
    const itExists = !!(await User.countDocuments({ email: newUser.email }))
    if (itExists) {
      return res.status(400).send({
        errors: [
          {
            status: 'Bad Request',
            code: '400',
            title: 'Validation Error',
            detail: `Email address '${newUser.email}' is already registered.`,
            source: { pointer: '/data/attributes/email' }
          }
        ]
      })
    }
    await newUser.save()
    res.status(201).send({ data: newUser })
  } catch (err) {
    debug('Error saving new user: ', err.message)
    res.status(500).send({
      errors: [
        {
          status: 'Server error',
          code: '500',
          title: 'Problem saving document to the database.'
        }
      ]
    })
  }
})

// Login a user and return an authentication token.
router.post('/tokens', sanitizeBody, async (req, res) => {
  const { email, password } = req.sanitizedBody
  try {
    const user = await User.authenticate(email, password)
    if (!user) {
      return res.status(401).send({
        errors: [
          {
            status: 'Unauthorized',
            code: '401',
            title: 'Authentication failed',
            description: 'Incorrect username or password.'
          }
        ]
      })
    }
    res.status(201).send({ data: { token: user.generateAuthToken() } })
  } catch (err) {
    debug(`Error authenticating user ... `, err.message)
    res.status(500).send({
      errors: [
        {
          status: 'Server error',
          code: '500',
          title: 'Problem saving document to the database.'
        }
      ]
    })
  }
})

module.exports = router

User Model

const bcrypt = require('bcrypt')
const saltRounds = 14
const jwt = require('jsonwebtoken')
const mongoose = require('mongoose')

const schema = new mongoose.Schema({
  firstName: { type: String, trim: true, maxlength: 64, required: true },
  lastName: { type: String, trim: true, maxlength: 64 },
  email: {
    type: String,
    trim: true,
    maxlength: 512,
    required: true,
    unique: true
  },
  password: { type: String, trim: true, maxlength: 60, required: true }
})

schema.methods.generateAuthToken = function () {
  return jwt.sign({ _id: this._id }, 'superSecureSecret')
}

schema.statics.authenticate = async function (email, password) {
  const user = await this.findOne({ email: email })
  const hashedPassword = user
    ? user.password
    : `$2b$${saltRounds}$invalidusernameaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`
  const passwordDidMatch = await bcrypt.compare(password, hashedPassword)

  return passwordDidMatch ? user : null
}

schema.pre('save', async function (next) {
  if (!this.isModified('password')) {
    return next()
  }
  this.password = await bcrypt.hash(this.password, saltRounds)
  next()
})

const Model = mongoose.model('User', schema)

module.exports = Model

Protected routes

The whole point of authenticating users, is to restrict access to certain resources. This can be as simple as "are you logged in?" or a more complex Role Based Access Control (RBAC) system that applies a more fine-grained user authorization check.

Let's start with the simple case. Create a new resource route to retrieve the currently logged-in user model. We could create a route formatted as GET /auth/users/:id, but it is a very common practice to have a dedicated route for the active user. It is usually formatted similar to this, GET /auth/users/me. Schetch out the logic ... add this to the Auth router module.

// Get the currently logged-in user
router.get('/users/me', async (req, res) => {
  // Get the JWT from the request header
  // Validate the JWT
  // Load the User document from the database using the `_id` in the JWT
  // Remember to redact sensitive data like the user's password
  // Send the data back to the client.
})

Access to the currently logged-in user is somthing that we will want to do in many route handlers, so let's build that logic in a reusable middleware function. It will tackle the first two steps above and make the token's payload available on the request object as req.user.

Create a new module called auth.js in the middleware folder of your project. Require the jsonwebtoken module and set the jwtPrivateKey to the same value that we used in the User model module (we are still hard coding this for now, but we'll look at a better way to handle this in a future module). Then add the basic middleware function signature.

const jwt = require('jsonwebtoken')
const jwtPrivateKey = 'superSecureSecret'

module.exports = (req, res, next) => {
  // Get the JWT from the request header
  // Validate the JWT
}

The client application will pass the token in a request header called Authorization. The corresponding value will be in the form of Bearer {{token}}, where {{token}} is a placeholder for the actual JWT. Note the word Bearer is capitalized and followed be a space.

We can use the req.header('Authorization') function to get the token from the request headers and, if the token is missing, send an authentication error.

const token = req.header('Authorization')
if (!token) {
  return res.status(401).send({
    errors: [
      {
        status: 'Unauthorized',
        code: '401',
        title: 'Authentication failed',
        description: 'Missing bearer token'
      }
    ]
  })
}

However, the value of the token is a string with two parts. The first part is the token type Bearer and the second part – after the space – is the actual token. We can use JavaScript's String.prototype.split() to issolate these two values as elements of an array. Then we can apply destructuring assignment to to extract the first two elements of the array into local variables, type and token.

const [type, token] = headerValue.split(' ')

To keep things clean and organized, let's move the logic to parse the Authorization header into a separate function.












 



const parseToken = function (headerValue) {
  if (headerValue) {
    const [type, token] = headerValue.split(' ')
    if (type === 'Bearer' && typeof token !== 'undefined') {
      return token
    }
    return undefined
  }
}

module.exports = async (req, res, next) => {
  const token = parseToken(req.header('Authorization'))
  if (!token)
  // ... rest of the module

If we have a token, we can proceed to validate it with the jwt.verify() method. It takes two arguments: the token, and the secret key. If the token is verified as not having been tampered with, then the payload is decoded and returned. We can then set the decoded payload as the user key on the request object and call next().

const payload = jwt.verify(token, jwtPrivateKey)
req.user = payload
next()

But we need to send a validation error if the token is not good. So, let's wrap that in a try catch block.

try {
  const payload = jwt.verify(token, jwtPrivateKey)
  req.user = payload
  next()
} catch (err) {
  res.status(400).send({
    errors: [
      {
        status: 'Bad request',
        code: '400',
        title: 'Validation Error',
        description: 'Invalid bearer token'
      }
    ]
  })
}

The final auth middleware module should look like this.

const jwt = require('jsonwebtoken')
const jwtPrivateKey = 'superSecureSecret'

const parseToken = function (headerValue) {
  if (headerValue) {
    const [type, token] = headerValue.split(' ')
    if (type === 'Bearer' && typeof token !== 'undefined') {
      return token
    }
    return undefined
  }
}

module.exports = (req, res, next) => {
  const token = parseToken(req.header('Authorization'))
  if (!token) {
    return res.status(401).send({
      errors: [
        {
          status: 'Unauthorized',
          code: '401',
          title: 'Authentication failed',
          description: 'Missing bearer token'
        }
      ]
    })
  }

  try {
    const payload = jwt.verify(token, jwtPrivateKey)
    req.user = payload
    next()
  } catch (err) {
    res.status(400).send({
      errors: [
        {
          status: 'Bad request',
          code: '400',
          title: 'Validation Error',
          description: 'Invalid bearer token'
        }
      ]
    })
  }
}

Now we can go back to our auth router module and use this new middleware. Require it at the top.

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

In the GET /auth/users/me route handler, leverage the authorize middleware. If the application flow made it this far, we know that the token was valid and the currently logged-in user's _id is available on the request object.

Remember

When we encoded the token, we included only the user's unique id property.

We can now load the user document from the database and return it to the client.

router.get('/users/me', authorize, async (req, res) => {
  const user = await User.findById(req.user._id)
  res.send({ data: user })
})

Redact Sensitive Data

One more thing... we have to guard against revealing sensitve data like user.password. One solution is to chain the Mongoose select() method onto the findById() request and limit the fields that are returned.

Prefixing field names with - explicitly tells Mongo to exlude this properties.


 



router.get('/users/me', authorize, async (req, res) => {
  const user = await User.findById(req.user._id).select('-password -__v')
  res.send({ data: user })
})

But a better solution is to make this the default behaviour in the User model schema. By overriding the toJSON() method in the model schema, we can be assured that we never forget to remove unwanted fields in a rout handler.

Add this code snippet to the /models/User.js module.

schema.methods.toJSON = function () {
  const obj = this.toObject()
  delete obj.password
  delete obj.__v
  return obj
}

And then we don't need the select() method in the route handler.

OK. Let's test it with Postman.

Set the Authorization header property to be the encoded token string that was returned from the login test – prefixed by Bearer.

screenshot of Postman setting bearer token

Submit your work

Use git to commit your changes and then push your commits up to the remote GitHub repo. Submit the GitHub repo's URL on Brightspace.

For next week

Before next week's class, please read these additional online resources.

Quiz

There will be a short quiz next class. The questions could come from any of the material referenced above.

Last Updated: 2/24/2020, 7:43:10 AM