Week 9
JWT Review

Quiz 7: JWT Authentication 15 mins

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

Assignment Reminder

Assignment 3 - Authentication - is due before next week's class.

# Agenda

  • AMA (10 mins)
  • Quiz (10 mins)
  • A little more JWT (30 mins)
  • Re-exporting with ES Modules (5 mins)
  • Review Assignment 3 (5 mins)
  • Lab Time

# JWT Review

  1. Is the payload data in a JWT securely encrypted?
    No. It is a simple hash that can be easily decoded and is not suitable for transmitting sensitive information. However, the token is cryptographically signed, so we can be sure that it has not been altered in any way.

  2. Can a JWT be revoked?
    No. Once issued, a JWT cannot be revoked. It can be set with a short expiry time to limit the risk of a token being compromised.

# Enhance JWT security

# Set an expiry limit for the token

Without setting an expiry time for the JWT, it will be valid indefinitely. This is a very bad idea! Since the JWT acts as an id_token (see the OpenID Connect (opens new window) specification), you should think of it as a permanently valid username/password combination. Therefore, it is highly recommended that you set a short TTL (time to live) for your tokens.

# Token Expiration (exp claim) (opens new window)

The standard for JWT defines an exp claim for expiration. The expiration is represented as a standard timestamp, meaning an integer value in seconds since 1970-01-01T00:00:00Z UTC.

What is a claim?

A 'claim' is a key name in the JWT payload object.

Suppose we want to set the token to expire in one hour. It can be set directly with a manual calculation as part of the payload.



 





jwt.sign(
  {
    exp: Math.floor(Date.now() / 1000) + 60 * 60,
    uid: this._id // 'this' is an instance of the User model class
  },
  'secret'
)

Remember

The Date.now() method returns the current time as a timestamp in milliseconds.

Or, better yet, the expiration property can be set with a helper function in the jsonwebtoken library by setting the relative future value in the options object – expressed as a string like '2h' or '1d'.

jwt.sign(
  { uid: this._id }, // payload
  'secret', // secret encryption key
  { expiresIn: '1h' } // options object
)

TIP

Read more about the JWT signing options in the official repo.
https://github.com/auth0/node-jsonwebtoken (opens new window)

# Set the expected algorithm

When the server is verifying a token received from the client, the server should explicitly declare the expected hashing algorithm and not rely on the alg property in the header. You can find more background on this practice in the article, Critical vulnerabilities in JSON Web Token libraries (opens new window).

# Set the algorithm when signing

The default algorithm used by the jsonwebtoken library to sign the token is HS256. This is an implementation of the Hash-Based Message Authentication Codes (HMAC) algorithm family using a SHA256 hash function.

Even though this is the default, let's set that choice explicitly in the signing options object.




 


jwt.sign(
  { uid: this._id }, // payload
  'secret', // secret encryption key
  { expiresIn: '1h', algorithm: 'HS256' } // options object
)

Then while verifying the token when it is sent back to the server on an API request, you should explicitly set the verification options object to expect that same algorithm.

jwt.verify(token, 'secret', {algorithms: ['HS256']})

Read more about the additional verification options (opens new window) in the official docs.

Notice

The verification options object uses a pluralized key name, algorithms, which accepts an array of acceptable algorithms.

# Silently refresh tokens

Short lived tokens provide better security against tokens intercepted in transit. If a token is exposed, it will expire quickly and limit the potential abuse. But forcing your users to provide their username/password repeatedly while using your application does not deliver a positive user experience.

One way to overcome this is to have the client application silently use the old token to request a new one. Typically this would be done via a POST request to the /auth/tokens resource path, sending the current/expired token in the header and an empty body. A short grace period can optionally be allowed for expired tokens.

Once the client supplied token is verified, then a new token is generated and returned to the client. The client would then update it's application state and proceed with the user's requested action. The re-authentication process would be invisible to the user.

If the client implementation proactively requests a new token before the old token has expired, the old token is added to the revoked token list so that it can no longer be used for either authentication or token refresh.

# Better logout

The simple implementation of client-side logout with a JWT is to clear it from active memory and the on-device cache, usually localStorage. A better implementation is to also send a RESTful API request to the server to register the user's explicit logout action.

The server would then record that token as revoked. The JWT verification logic would need to be augmented to check against this table of revoked tokens. Because JWT verification needs to happen on every API call, the revoked token list is typically implemented with an in-memory database like Redis. Redis also has a built-in feature to automatically remove keys from a datastore after a set expiry time. This ensures that expired keys do not clutter up the datastore and the look-up task remains fast.

# Re-export with ES Modules

Sometimes it is useful to separate out sub-sets of certain modules into separate module files, but still make them available from a common module to make consuming them simpler. This is done by setting up the common module, or aggregator, to import the smaller modules and then re-export them.

Take the model modules from our example application. It might be useful to be able to import several models at once, or all of the models in a single object.

# Create a new aggregator module

Start by creating a new index.js file in the /models folder. There are several syntax options available (opens new window), but the simplest is the export..from syntax.

// /models/index.js
export { default as Car } from './Car.js'
export { default as Person } from './Person.js'
export { default as User } from './User.js'

# Import from this new module

Elsewhere in your application when you need to import multiple model classes, you can do it as named imports.

import { Car, Person } from './models/index.js'
// ...
const cars = await Car.find()

Or import all of the models in a single object.

import * as models from './models/index.js'
// ...
const cars = await models.Car.find()

# 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: 3/5/2021, 11:12:03 AM