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.
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.
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.
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')
]
}
}
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.)
You'll need to make changes on both the server and the client to use this hook.
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.
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
})
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
.
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!
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
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!!!