Section 12: API Authentication and Security (Task App)

102. Section Introduction

103. Securely Storing Passwords: Part I

Every application should securely store user passwords.  We currently store the password in plaintext in the database.  This is problematic.  If our database was hacked, the hackers would have access to the passwords which might be used by users for other applications.

We’re going to use the npm module named bcryptjs to hash the passwords.  First, on the command line, install the bcryptjs module.

$ npm in bcryptjs

Then, in index.js, we need to load the bcryptjs module.

const bcrypt = require('bcryptjs')

To see how it is used, let’s create a little example.  The first argument to bcrypt.hash() is the password, and the second argument is the number of rounds that the hashing algorithm is executed.

const myFunction = async () => {
  const password = 'red12345!'
  const hashedPassword = await bcrypt.hash(password, 8)

  console.log(password)
  console.log(hashedPassword)
}

myFunction()

When we start our server we see the hashed password is a string of, what appears to be, random characters.  This hashed password is what we’ll save in the database.

The hashing algorithm is a one-way function, that is, we can’t get the original password from from the hash.  The way we’ll use it as follows.  When the user logs in, we’ll ask the user for their password.  We’ll then hash what they enter.  If it matches what is in the database then we know that they’ve entered in the correct password.  bcrypt has a method named compare() to get this done.  The first argument is the password that the user enters and the second argument is the hashed password that is saved in the database.

const attempt = 'red12345!'

const isMatch = await bcrypt.compare(attempt, hashedPassword)
console.log(isMatch)

104. Securely Storing Passwords: Part II

There are two places where plain-text passwords are provided to our application; both are in the /user router.  The first is in the route used for adding new users and the second is in the route used to update a user document.

To support hashed passwords, we’re going to modify the user model in models/user.js.  Mongoose supports middleware.  With middleware we can register functions that run before or after certain mongoose events occur. For example, we can run code just before or after data is validated, saved, or removed.

In our case, we want to run some code before the password is saved to a MongoDB document.  If there is a plain text password we’ll hash it.

Mongoose requires a schema in order to use middleware.    Each Schema object has a pre method that is used to register a function for a specific event (e.g. validate, save, or remove).

Below we call the pre() method on userSchema, passing to pre the name of the event, ‘save’, and a function.  We must pass a function, not a lambda expression, because lambda  do not bind to this, and we need to use this.  The function must be async and has a parameter named next.

const userSchema = new mongoose.Schema({
    // user properties
})

userSchema.pre('save', async function(next) {
    // actions to perform before save
})

const User = mongoose.model('User', userSchema);
...

Inside our middleware function, we’ll create a constant to hold the reference to this.

userSchema.pre('save', async function(next) {

    const user = this

    if (user.isModified('password')) {
      user.password = await bcrypt.hash(user.password, 8)
    }

    next()  // run the save() method
})

User.findbyId() and User.update(), bypass mongoose middlewhere, so currently this middlewhere is executed when we want to update the user’s password in the document.

In router.patch(‘/users/:id’, …) we need to change the try-block

try {
  const user = await User.findById(req.params.id)

  updates.forEach((field) => {
      user[field] = req.body[field]
  })

  await user.save()

  if (!user) {
    return res.status(404).send()
  }

}

//Exercise to change task patch endpoint to get task by id, make changes, and save like we did above.

105. Logging in Users

We’re going to create a new endpoint that users can use to log in.   The client will send an email address and a password to the endpoint and if the email and password belong to the same user, the server will return a status code 200.  If not, the server will return a status code 400.

Since the logic of determining if a user is in a collection belongs in the model code, let’s add a method onto userSchema that either returns a user if authenticated, or throws an error if not.

userSchema.statics.findByCredentials = asynch (email, password) => {       
  const user = await User.findOne({email}) 
  if (!user) { 
    throw new Error('Unable to login') 
  } 
  const isMatch = await bycrypt.compare(password, user.password) 
  if (!isMatch) { 
    throw new Error('Unable to login') 
  } 
  return user 
}

Now our new router will look like this.

router.post('/users/login', async (req, res) => {

  try {
    const user = await User.findByCredentials(req.body.email, req.body.password)
    res.status(200).send(user)

  } catch (e) {    
    res.status(400).send()
  }
}

106. JSON Web Tokens

Two routes will be public (POST /users and POST /users/login), all others will require users to have ben authenticated.

We will use JWTs (JSON web tokens) to verify authentication.   Our application will have a private key, a secret, stored on the server.  Only our server will know it. We will use that secret to generate and sign a token when a user logs in and we’ll pass the token back to the user.  All subsequent requests from the user will have to include the token.

To use JWTs we first need to install the npm module named jsonwebtoken and import module in our code.

const jwt = require('jsonwebtoken')

To create a new JWT we call jwt.sign().  The first argument is an object that that will be stored in the token’s payload.  The second argument is a secret that will be used to sign the token.

const token = jwt.sign( {_id: 'abc123' }, 'thisismysecret')

The token returned by jwt.sign() is a string containing 3 substrings (header, payload, signature) separated by periods.  If we print the token to the console, copy the middle section to the clipboard, and paste it in the website base64decode.org, we see that the middle substring contains the object we passed to sign.

The goal of the token is not to hide the data in the payload, it is to verify that the token is a valid token

const payload = jwt.verify(token, 'thisismysecret')

jwt.verify() returns the object that is in the payload.

We can set an expiration date for the token

const token = jwt.sign( 
  {_id: 'abc123' }, 
  'thisismysecret', 
  { expiresIn: '7 days'}
)

107. Generating Authentication Tokens

When a user logs in, we’ll generate an authentication token, store the token in the user’s MongoDB document, and send it back to the user.  When a user creates a new account, we’ll do the same thing.

Let’s first add an array property to the user Schema that we can store authentication tokens in.

In userSchema,  add the following property named tokens that is an array of objects.  Each object will have a token field which is a string and is required.

tokens: [{
  token: {
    type: String,
    required: true
  }
}]

Since each token is owned by a single user, we are going to create a method to create a token, that can be called on instances of the User model.  To set this up, we can create another method on the userSchema object, but this time on the userSchema.methods property.

Inside models/user.js lets add the following method definition.

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

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

  return token
}

Now in the /users/login router, we’ll generate the authentication token and send it to the user.

router.post('/users/login', ...
  try {
    const user = await User.findByCredentials(...)
    const token = await user.generateAuthToken()
    res.send({ user, token })
  }
  ...

// As an exercise we will so something similar for the sign up request.

In the POST /users router, we generate an auth token and send it to the user

router.post('/users', ...
  const user = new User(req.body)

  try {
    await user.save()
    const token = await user.generateAuthTOken()
    res.status(201).send({user, token})
}
...

108. Express Middleware

Ever single request will need to provide the authentication token except to create an account, and log in.

We’ll be working in src/app.js.

With middleware when a new request comes in, we can do something  (like check for an auth token), before the request is sent to the router handler.

We register middleware by calling app.use().  Now we want to register the auth middleware before our other existing calls to app.use().  The function we pass to app.use() has 3 parameters: the request from the client, a response object, and a method named next.

The next method will be called when we want the request to continue on being passed to the router.

app.use((req, res, next) => {
  if () {
    // do something
    next()
  } 
})

app.use(express.json())

109. Accepting Authentication Tokens

Lets create a new directory named src/middlewhere.  Each middleware will be in a separate file.

Create a file named src/middlware/auth.js

const auth = async (req, res, next) => {


  next()
}

module.exports = auth

We only want the auth middleware to run for certain requests.  Let’s go into src/routers/users.js.

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

We can require middleware to run for a particular route by passing the middleware function as the second argument to the route handler.  For example, to require the auth middleware to run when a request come in for the GET /users/:id route, we would change the parameter list to the following:

router.get('/users/:id', auth, async (req, res) => {
  ...
}

The auth function.

We’re going to require that the client put the authentication token in the request header using the name ‘Authentication‘.

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

const auth = async (req, res, next) => {
  try {
    const token = req.header('Authorization')
    //console.log(token)
    token = token.replace('Bearer ', '')
    const decoded = jwt.verify(token, 'thisismysecret')
    const user = await User.findOne({_id: decoded._id, 'tokens.token': 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

<< Setting up headers in Postman 6:15>>

Header
Key              Value
Authorization.   Bearer <<auth-token>>

Add a new route GET /users/me.

router.get('/users/me', auth, async (req, res) => {
  res.send(req.user)
})

110. Advanced Postman

As we are developing our api we will be testing our API on both localhost and on heroku.  In Postman, we can create two environments, one for development and one for production, and switch between them, depending on what we are doing.

In Postman, an environment is a set of variables that allow you to switch the context of your requests.  So for example, we can create a variable named host and set it to localhost in our development environment and set it to the heroku URL for our production environment.

To create an environment click the eye icon in the upper right hand corner (next to the box that reads No Environment) to open a drop down menu.  In the Environment section, click add to create our first environment.  Let’s name this environment Web App API (dev).

Now in the Variable column add url, and set the initial value to “127.0.0.1:3000”.

Create a second environment named Web App API (prod) and in it, create a variable named url and set its initial value to your heroku URL.

Set up Postman to Use Auth Tokens

When we create a new user, we get a new auth token.  So to use the other routes for that new user, we have to cut and past the token from the response into the collection’s Auth Token field.  We can automate this process.

In the Tests tab of a request,  we can provide JavaScript code that will be run after a response has been received.

For the Create user and Login user routes, we’ll extract the token from the body and set an environmental variable named authToken equal to the value.  We’ll then use the environmental variable inside the collection.

In the Tests tab for the Login request enter the following.

if (pm.response.code === 200) {
  pm.environment.set('authToken', pm.response.json().token)
}

In the Tests tab for the Create user request enter the following.

if (pm.response.code === 200) {
  pm.environment.set('authToken', pm.response.json().token)
}

*****

For any request, click the Authorization tab. Notice the TYPE is set to Inherit auth from parent.

Now click on the elipsis next to the Web App API collection, and choose Edit.  Click on the Authorization tab, and choose Bearer Token for TYPE, and set the token field to {{authToken}}.

Press the Update button.

Now all requests that have their Authorization TYPE set to Inherit auth from parent will the auth header set.

*****

For requests that don’t require authorization (POST /users, POST /users/login), set Athorization TYPE to No Auth.  Press Save.

111. Logging Out

We’ll create a new route to allow a user to log out.

router.post('/users/logout', auth, async (req, res) => {
  try {
    req.user.tokens = req.user.tokens.filter((token) => {
      return token.token !== req.token
    })
    await req.user.save()
    
    res.send()
  }
  catch (e) {
    res.status(500).send()
  }
})

Video shows how to create /usrs/logoutAll router.

112. Hiding Private Data

When send a logged in user their profile, we don’t need to send them their hashed password or auth tokens.

In src/models/user.js, lets add an instance method.

userSchema.methods.toJSON = async function() {
  const user = this

  const publicProfile = user.toObject()
  delete publicProfile.password
  delete publicProfile.tokens

  return publicProfile
}

When express passes an object to res.send(), express calls JSON.stringify on the object.  When a schema has an instance method named toJSON, express calls toJSON before the object gets stringified.

So now when a user object is sent to a user, the object will not contain the password or tokens properties.

113. Authenticating User Endpoints

The endpoint for a user to delete her own account is below.

router.delete('/users/me', auth, async (req, res) => {
  try {
    await req.user.remove()
    res.send(req.user)
  } 
  catch (e) {
    res.status(500).send()
  }
})

The endpoint for a user to update her own profile is below.

router.patch('/users/me', auth, async (req, res) => {
  const updates = Object.keys(req.body)
  const allowedUpdates = ['email', 'password']
  const isValidOperation = updates.every((update) => allowedUpdates.includes(update))

  if (!isValidOperation) {
    return res.status(400).send({error: 'Invalid updates.'})
  }

  try {
    updates.forEach((update) => req.user[update] = req.body[update])
    await req.user.save()
    res.send(req.user)    
  }
  catch (e) {
    res.status(400).send(e)
  }
})

114. The User/Task Relationship

We need to create a relationship between users and tasks.

The user can store the ids of all of the tasks it has created. OR. we can store the user id of user who created a task in the task model.

In models/task.js add the following property to the task schema.

owner: {
    type: mongoose.Schema.Types.ObjectId,
    required: true,
    ref: 'User'
}

In the task router.

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

...
router.post('/tasks', auth, async (req, res) => {
  const task = new Task({
    ...req.body,
    owner: req.user._id
  })
  try {
    await task.save()
    res.status(201).send(task)
  }
  catch(e) {
    res.status(400).send(e)
  }
})

To get a user object from a task id we can do the following.

const task = await Task.findById('.....')
await task.populate('owner').execPopulate()
console.log(task.owner) //prints user object, not user id

To get a task object from a user id we need to set up a virtual entity in the userSchema.

userSchema.virtual('tasks', {
  ref: 'Task',
  localField; '_id',
  foreignField: 'owner'
})

Then if we have a user id we can get the tasks in the Task collection that have their owner set to that particular user id.

const user = await User.findById('.....')
await user.populate('tasks').execPopulate()
console.log(user.tasks)

115. Authenticating Task Endpoints

Let set up authentication for /tasks/:id

router.get('/tasks/:id', auth, async (req, res) => {
  const _id = req.params.id

  try {
    const task = await Task.findOne({ _id , owner: req.user._id})
     if (!task) {
      return res.status(404).send()
    }

    res.send(task)
  }
  catch (e) {
    res.status(500).send()
  }
})

And for Get /tasks

router.get('/tasks', auth, async (req, res) => {

  try {
    await req.user.populate('tasks').execPopulate()

    res.send(req.user.tasks)
  }  catch(e) {
    res.status(500).send()
  }
})

And for PATCH /tasks/:id

router.patch('/tasks/:id', auth, async (req, res) => {
  const updates = Object.keys(req.body)
  const allowedUpdates = ['description', 'completed']
  const isValidOperation = updates.every((update) => allowedUpdates.includes(update))

  if (!isValidOperation) {
    return res.status(400).send({error: 'Invalid updates.'})
  }

  try {
    const task = await Task.findOne({ _id: req.params.id, owner: req.user._id})

    if (!task) {
      return res.status(404).send()
    }
    
    updates.forEach((update) => task[update] = req.body[update])
    await task.save()
    res.send(task)
  } 
  catch (e) {
    res.status(400).send(e)
  }
})

And for DELETE /tasks/:id

router.delete('/tasks/:id', auth, async (req, res) {
  try {
    const task = await Task.findOneAndDelete({ _id: req.params.id, owner: req.user._id})

    if (!task) {
      return res.status(404).send()
    }

    req.send(task)
  }
  catch (e) {
    res.status(500).send()
  }
})

116. Cascade Delete Tasks

In this section we delete tasks belonging to a user when the user deletes her account.

We will create middlewhere when user is removed.

const Task = require('./task')
...

userSchema.pre('remove', async function(next) {
  const user = this  await Task.deleteMany({owner: user._id })
  next
})

// Wow what a course!

© 2021 – 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.