FeathersJS Auth Recipe: Customizing the JWT Payload

The Auk release of FeathersJS includes a powerful new authentication suite built on top of PassportJS. The new plugins are very flexible, allowing you to customize nearly everything. One feature added in the latest release is the ability to customize the JWT payload using hooks. Let’s take a look at what this means, how to make it work, and learn about the potential pitfalls you may encounter by using it.

The JWT Payload

If you read the resources on how JWT works, you’ll know that a JWT is an encoded string that can contain a payload. For a quick example, check out the Debugger on jwt.io. The purple section on jwt.io is the payload. You’ll also notice that you can put arbitrary data in the payload. The payload data gets encoded as a section of the JWT string.

The default JWT payload contains the following claims:

  1. const decode = require('jwt-decode')
  2. // Retrieve the token from wherever you've stored it.
  3. const jwt = window.localStorage.getItem('feathers-jwt')
  4. const payload = decode(jwt)
  5. payload === {
  6. aud: 'https://yourdomain.com', // audience
  7. exp: 23852348347, // expires at time
  8. iat: 23852132232, // issued at time
  9. iss: 'feathers', // issuer
  10. sub: 'anonymous', // subject
  11. userId: 1 // the user's id
  12. }

Notice that the payload is encoded and IS NOT ENCRYPTED. It’s an important difference. It means that you want to be careful what you store in the JWT payload.

Customizing the Payload with Hooks

The authentication services uses the params.payload object in the hook context for the JWT payload. This means you can customize the JWT by adding a before hook after the authenticate hook.

  1. app.service('authentication').hooks({
  2. before: {
  3. create: [
  4. authentication.hooks.authenticate(config.strategies),
  5. // This hook adds the `test` attribute to the JWT payload by
  6. // modifying params.payload.
  7. context => {
  8. // make sure params.payload exists
  9. context.params.payload = context.params.payload || {}
  10. // merge in a `test` property
  11. Object.assign(context.params.payload, {test: 'test'})
  12. }
  13. ],
  14. remove: [
  15. authentication.hooks.authenticate('jwt')
  16. ]
  17. }
  18. })

Now the payload will contain the test attribute:

  1. const decode = require('jwt-decode')
  2. // Retrieve the token from wherever you've stored it.
  3. const jwt = window.localStorage.getItem('feathers-jwt')
  4. const payload = decode(jwt)
  5. payload === {
  6. aud: 'https://yourdomain.com',
  7. exp: 23852348347,
  8. iat: 23852132232,
  9. iss: 'feathers',
  10. sub: 'anonymous',
  11. userId: 1
  12. test: 'test' // Here's the new claim we just added
  13. }

Note: The payload is not automatically decoded and made available in the hooks, thus, requiring you to implement this functionality in your app. Using jwt-decode is a simple solution that could be dropped in a hook as needed.

Important Security Information

As you add data to the JWT payload the token size gets larger. Try it out on jwt.io to see for yourself. There is an important security issue to keep in mind when customizing the payload. This issue involves the default HS256 algorithm used to sign the token.

With HS256, there is a relationship between the length of the secret (which must be a minimum of 256-bits) and the length of the encoded token (which varies with the payload). A larger secret-to-payload ratio (so the secret is larger than the JWT) will result in a more secure JWT. This also means that keeping the secret size the same and increasing the payload size will actually make your JWT comparatively less secure.

The Feathers generator creates a 2048-bit secret, by default, so there is a small amount of allowable space for putting additional attributes in the JWT payload. It’s very important to keep the secret-to-payload length ratio as high as possible to avoid brute force attacks. In a brute force attack, the attacker attempts to retrieve the secret by guessing the secret over and over until getting it right. If your secret is compromised, they will be able to create signed JWT with whatever payload they wish. In short, be cautious about what you put in your JWT payload.

Finally, remember that the secret created by the generator is meant for development purposes, only. You never want to check your production secret into your version control system (Git, etc.). It is best to put your production secret in an environment variable and reference it in the app configuration.