Skip to content

A "before" hook for FeathersJS to authorize requests accompanied by an Auth0-issued JWT.

License

Notifications You must be signed in to change notification settings

morphatic/feathers-auth0-authorize-hook

Repository files navigation

FeathersJS Auth0 Authorization Hook

Build Status Coverage Status npm version GitHub license

This plugin was designed to be used in scenarios where FeathersJS is being used solely as a backend API server, and Auth0 is being used for authentication from a frontend client written with, e.g. Vue, React, or Angular. For a fuller discussion of this scenario and why I chose to write this plugin, check out this blog post. If you are using Feathers for BOTH the backend AND the frontend, you're probably much better off using the @feathersjs/authentication that's already part of the framework.

Installation and Configuration

To install this plugin, from the root of your package:

npm install --save @morphatic/feathers-auth0-authorize-hook

In your feathers app config (usually found in config/default.json) you'll need to add the following properties. You'll want to update them with your actual Auth0 domain, i.e. swap out the example below with the domain for your app which you can find in your Auth0 application settings.

{
  "jwksUri": "https://example.auth0.com/.well-known/jwks.json",
  "jwtOptions": {
    "algorithms": [
      "RS256"
    ],
    "audience": [
      "https://example.auth0.com/api/v2/",
      "https://example.auth0.com/userinfo"
    ],
    "ignoreExpiration": false,
    "issuer": "https://example.auth0.com/"
  }
}

Also, you will need to create or update two services: users and keys. You'll also need to add a simple middleware.

The users service

It's likely that you already have a users service in your app. If you don't you'll need to create one, e.g. by using the feathers-plus generator. Assuming you already have a users service, you'll need to ensure that your model has two string properties: user_id and currentToken. user_id will hold the user_id generated by Auth0 when the user's account was created. currentToken will be used to store already-verified tokens, to prevent the app from having to re-verify tokens on every request. For reference, a JSON Schema representation of a minimal users service is included in this repo.

The keys service

You'll also need to generate a keys service in your app. This is used to store JSON web keys (JWKs) retrieved from Auth0's JWKS endpoint. This service does not need to be persistent (I use an in-memory service) as it is mainly used to cache already-retrieved JWKs and improve performance. For reference, please see the JSON Schema version of the keys service contained in this repo.

Although not strictly necessary, since the keys service does not contain any non-public information, I typicaly add a hook to prevent the keys service from being called by external clients like this:

// src/services/keys/keys.hooks.js
const { disallow } = require('feathers-hooks-common')
module.exports = {
  before: {
    all: [
      disallow('external')
    ]
  }
}

The middleware

This hook relies upon extracting a JWT from the Authorization HTTP header (and as such ONLY works for REST transports--not socket.io/primus). By default, however, HTTP headers are not available to hooks, so you need to create middleware to make them so. One way to do this is to generate new middleware with a CLI generator and add the following code to it:

// src/middleware/authorize.js
module.exports = function (options = {}) {
  return function authorize(req, res, next) {
    req.feathers = { ...req.feathers, headers: req.headers }
    next()
  }
}

Alternatively, you can modify src/app.js to modify the REST configuration section as follows:

// src/app.js
app.configure(express.rest(
  (req, res, next) => {
    req.feathers = { ...req.feathers, headers: req.headers }
    next()
  }
))

(Personally, I prefer the former method as I like to leave the generated app.js file basically untouched.)

Using the authorize hook

You'll need to make changes on both the server and the client to use this hook.

On the server

Once installed you'll need to add this hook into your feathers app. You can use it either for the entire app or for individual services. I find it easiest to set it up in conjunction with feathers-hooks-common. This hook ONLY works as a before hook. Here's an example of using it for your entire app (NOTE: I've abbreviated this to just the before.all hook.):

// src/app.hooks.js
// import feathers-hooks-common `isProvider` and `unless`
const { isProvider, unless } = require('feathers-hooks-common')
// import the authorize hook
const { authorize } = require('@morphatic/feathers-auth0-authorize-hook')() // <-- note the parentheses

module.exports = {
  before: {
    all: [
      unless(isProvider('server'), authorize)
    ]
  }
}

Setting it up this way will not require internal service calls to be authorized. If you don't do this, it could make your feathers app unusable since internal functions would have no way to be authorized.

On the client

From your frontend app, you'll need to set up any services that access your feathers API as follows. I've used this in a Vue app, but it should work equally well for any other frontend framework like React or Angular. In this example, I'm imagining that I'm developing the proverbial To Do List app, and that my To Do list items are stored in my feathers backend.

// src/services/feathers.js
import feathers from '@feathersjs/client'
import axios from 'axios' // NOTE: this only works for REST clients

const app = feathers()
const rc = feathers.rest('https://api.example.com') // the URL to your feathers API server
app.configure(rc.axios(axios))

export const api = token => {
  // throw an error if no token was passed
  if (!token) throw 'Token cannot be empty!'

  // add the token to the Authorization header
  const params = {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  }

  // now just pass these params with every call to the API
  return {
    getTodos: async query => {
      if (query) params.query = query
      return app.service('todos').find(params)
    }
  }
}

And then in the view or component that consumes the api:

// src/views/ToDoList.js
import { api } from '../services/feathers'

// assuming you've already authenticated with Auth0 and stored your
// token localy, e.g. in window.sessionStorage
const token = sessionStorage.getItem('auth0_access_token')
const { getTodos } = api(token)

// get all "to do" items due in the future
const query = {
  dueDate: {
    $gt: new Date().getTime()
  }
}
getTodos(query)
  .then(items => {
    // do something with the retrieved items
  })
  .catch(err => {
    // handle any errors, including failure to authorize
  })

Custom Configuration

Remember the "extra" parentheses we had to use when importing the hook into app.hooks.js above?

// from src/app.hooks.js in our feathers app
const { authorize } = require('@morphatic/feathers-auth0-authorize-hook')() // <-- NOTE: the "extra" parentheses

They're necessary because the function that returns the hook uses the factory pattern and allows for customization. While you can swap out any of the functions in this plugin for your own implementation, the main things you're likely to want to customize are the services used to hold the user_id and keys.

Using a different users service

By default, the hook queries the users service to find a user whose user_id matches the Auth0 user_id stored in the sub claim in the token. However, you might want to store this information on a different model in a different service. To do that, you'd provide an options object as a parameter to the require()() function and set the userService property to the name of the service you want to use. For example, pretend that instead of users, you want to look for the user_id property on the members service instead:

// example src/app.hooks.js with custom `users` service
// first, create an `options` object with the preferred name for your service
const options = {
  userService: 'members'
}
const { authorize } = require('@morphatic/feathers-auth0-authorize-hook')(options) // <-- second, pass it to the import statement

That's it!

Using a different keys service

Similarly, you can use a different service name for where you store and look for the JWKs. Here's an example showing customization of BOTH the users and keys services:

// example src/app.hooks.js with custom `users` service
// first, create an `options` object with the preferred names for your services
const options = {
  userService: 'members',
  keysService: 'jwks'
}
const { authorize } = require('@morphatic/feathers-auth0-authorize-hook')(options) // <-- second, pass it to the import statement

Comments, Questions, Issues, etc

I welcome feedback, bug reports, enhancement requests, or reports on your experiences with the plugin. Undoubtedly, there's a better way to do this than what I've come up with, and I'd love to hear about it. That being said, I hope some people will find this useful!!!

About

A "before" hook for FeathersJS to authorize requests accompanied by an Auth0-issued JWT.

Resources

License

Stars

Watchers

Forks

Packages

No packages published