@morphatic/feathers-auth0-strategy
v1.0.0
Published
An RS256 authentication strategy for FeathersJS to support authentication with access tokens generated client-side with Auth0
Downloads
166
Maintainers
Readme
FeathersJS Auth0 Authorization Strategy
:warning: This package is designed to work with the FeatherJS v4.0 (Crow). :warning:
What this package does
This package does two things:
- It implements a custom
auth0
authentication strategy for FeathersJS. TheAuth0Strategy
verifies JWTs (JSON Web Tokens) using the asymmetricRS256
algorithm (the recommended practice), rather than theHS256
algorithm that is built into the FeathersJSJwtStrategy
. - It implements a custom FeathersJS authentication service. The
Auth0Service
extends, and is designed to be used in place of the defaultAuthenticationService
. The primary benefit of theAuth0Service
are that it removes the necessity for developers to specify theauthentication.secret
configuration property in their apps. The client secret is not used byRS256
token verification scenarios, but the defaultAuthenticationService
will throw an error if thesecret
is not specified.
This package has been designed and tested for use with the latest version of FeathersJS (v4.0, aka "Crow") and its new authentication style. Note, the Auth0Strategy
may be used on its own without also using the Auth0Service
but this will require developers to set a "secret" in the configuration, even though it may not be used.
Who should use this package
This package is designed specifically for the following scenario:
- FeathersJS is being used solely as a backend API server
- Auth0 is being used for authentication from a frontend client written with, e.g. Vue, React, or Angular
- Auth0 is configured to sign access tokens using the
RS256
algorithm (the recommended practice)
For a fuller discussion of this scenario and why I chose to write this package, check out this blog post.
Who should NOT use this package
If any of the following scenarios apply to your app, there are likely better options than this package:
- If you are using FeathersJS as BOTH frontend and backend, you're probably much better off simply using the
@feathersjs/authentication
that's already part of the framework. - If your access tokens are signed with the
HS256
algorithm (see: How can I check the signature algorithm?), there are easier ways to configure Feathers to verify them, namely to use the built-inJwtStrategy
.
Here is the official tutorial on using Auth0 with Feathers.
Installation and Configuration
To install this package, from the root of your FeathersJS app:
npm install --save @morphatic/feathers-auth0-strategy
After installation, you need to make sure that you have a users
service. Customization of the users
service will be discussed below.
Update the app configuration
In your server config file (usually config/default.json
), at a minimum, you'll need to add the following:
{
"authentication": {
"auth0": {
"domain": "example.auth0.com"
},
"authStrategies": ["auth0"],
"entity": "users",
"entityId": "user_id",
"service": "users"
}
}
The package assumes that your full Auth0 domain name is something like https://example.auth0.com/
. Note that you do NOT need to specify a secret
since the RS256
algorithm doesn't use them. You also do not need to specify jwtOptions
as these are handled automatically by this package.
Set up 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. Assuming you already have a users
service, you'll need to ensure that your model has a user_id
property. The user_id
field will hold the user_id
generated by Auth0 when the user's account was created. For reference, a JSON Schema representation of a minimal users
service is included in this repo.
If you'd like to use a different service for storing user information or the user_id
key, this is configurable using the options
described below.
Decide how to handle non-existent users
By default, this package will NOT add users to your API database. This is primarily an issue that affects authentication when users first create an account with your app. Since the signup and authentication process is handled by Auth0, you have to have some way to get the Auth0 user_id
stored in a new user
record in your API's database.
Option #1: Direct Auth0 <==> API user
creation
One way to get your Auth0 users into your API is to have Auth0 make an API request directly. From the Auth0 management dashboard, you can go to the "Rules" menu and create a new rule that will attempt to make an API request whenever someone logs in. I have something like this (NOTE: I'm using feathers-mongoose
in my API and I have a hook that enables upserts):
/**
* A RULE created from the Auth0 Dashboard
* Add user to the API users table
*/
function (user, context, callback) {
// check to see if they've already been added
if (user.app_metadata && !user.app_metadata.api_id) {
// no, they haven't, so...
// load the library we'll need to make an API request
const axios = require('axios');
// construct our URL and POST body
// NOTE: You MUST send an additional `api_url` param with the
// authentication request so Auth0 knows how to find your API!!!
const url = `${context.request.query.api_url}/users`;
const body = { ...user };
delete body._id;
// try to create/update a new user via the API
// NOTE: I'm using an "upsert" query here. Setting this up
// requires additional configuration on the API server side
axios.patch(url, body, {
headers: { 'content-type': 'application/json' },
params: { user_id: user.user_id }
}).then(
api_user => {
// it worked! set the API _id for the created user in app_metadata
if (Array.isArray(api_user.data) && api_user.data[0] && api_user.data[0]._id) {
user.app_metadata.api_id = api_user.data[0]._id;
} else if (Array.isArray(api_user.data.upserted) && api_user.data.upserted[0]._id) {
user.app_metadata.api_id = api_user.data.upserted[0]._id;
} else {
user.app_metadata.api_id = '';
}
return auth0.users.updateAppMetadata(user.user_id, user.app_metadata);
})
.then(() => {
callback(null, user, context);
})
.catch((err) => {
console.log('error',err);
// it didn't work.
callback(null, user, context);
});
} else {
// yes, they've already been added to the API
// carry on...
callback(null, user, context);
}
}
As will be explained below, this package includes an IP address whitelist hook to allow requests from Auth0 to be accepted by your API without the normal authentication. (You'll still need some mechanism to make sure that the requests actually came from Auth0 and not another agent spoofing an Auth0 IP address.)
Option #2: Allow Auth0Strategy
to create new users
A second method for creating users in your API database is to configure Auth0Strategy
to allow a new user
to be created if one doesn't already exist with the given Auth0 user_id
. To do this, the default configuration needs to be updated to:
{
"authentication": {
"auth0": {
"create": true, // <-- SET THIS TO `TRUE`
"domain": "example.auth0.com"
},
"authStrategies": ["auth0"],
"entity": "users",
"entityId": "user_id",
"service": "users"
}
}
In this case, the very first time a new user attempts to authenticate, a new user
record will be created that contains ONLY the Auth0 user_id
. This happens AFTER the access_token
has been verified to be valid.
This is easier to set up than the previous option, but it also means you'll have to figure out another way to transfer the user's profile from Auth0 to the API.
:warning: This package does NOT check permissions
This package is designed to verify that access tokens sent from your client are:
- Valid
- Current (i.e. not expired)
- Associated with an existing user account (although it is possible to create a minimal user record automatically)
That said, this package does NOT check to see if the users making API requests actually have permission to access the requested resources. You'll need to address permissions in some other way. Also, since Auth0 allows you to store user-related information in user_metadata
, and since app developers will make different decisions about where to store user info (API vs Auth0), how much (if any) info to duplicate across these stores, I chose not to address this in this package. Likewise, permissions scenarios are also likely to vary widely across apps, and are hence unaddressed here.
Basic usage
You'll need to do some configuration on both the server and in your clients.
Server setup when using Auth0Service
If you choose to use BOTH Auth0Strategy
and Auth0Service
, create a file called authentication.js
in your src/
directory:
// src/authentication.js
// import the service and strategy classes
const { Auth0Service, Auth0Strategy } = require('@morphatic/feathers-auth0-strategy')
module.exports = app => {
// instantiate the service
const auth0Service = new Auth0Service(app)
// register the strategy
auth0Service.register('auth0', new Auth0Strategy())
// register the `/authentication` service with feathers
app.use('/authentication', auth0Service)
}
Then in your main server configuration (usually src/app.js
):
// src/app.js
// import the addIP middleware
const { addIP } = require('@morphatic/feathers-auth0-strategy')
// import the auth configuration (created earlier)
const auth = require('./authentication')
// configure middleware for adding the IP address to the context for incoming requests
// this MUST be added to your `app.js` file BEFORE any services have been registered
app.configure(addIP)
/* ... then register all your services ... */
app.configure(services)
// finally register your authentication service
app.configure(auth)
Protect your services
As with other Feathers authentication strategies, you need to add hooks to your services to protect them, so in your src/services/*/*.hooks.js
files you'll likely want to do something like:
// src/services/users/users.hooks.js (e.g.)
const { fromAuth0 } = require('@morphatic/feathers-auth0-strategy')
const { isProvider, some, unless } = require('feathers-hooks-common')
module.exports = {
before: {
all: [
unless(some(isProvider('server'), fromAuth0()), authenticate('auth0'))
]
}
}
Auto-register for ALL or selected services
Instead of registering the authentication hook on each service independently, you can specify in the configuration (i.e. config/default.json
) that ALL services (except the authentication service itself) should have the authentication hook registered automatically:
{
"authentication": {
"auth0": {
"autoregister": true, // <-- SET THIS TO `TRUE`
"domain": "example.auth0.com"
},
"authStrategies": ["auth0"],
"entity": "users",
"entityId": "user_id",
"service": "users"
}
}
This will register the hook before every service call as described in the previous section. If you'd like to only autoregister authentication for some services but not all, you can specify the services that should be auto-registered like this:
{
"authentication": {
"auth0": {
"autoregister": true,`
"domain": "example.auth0.com",
"services": ["users", "products"] // <-- authentication auto-registered for these services
},
"authStrategies": ["auth0"],
"entity": "users",
"entityId": "user_id",
"service": "users"
}
}
Server setup when using AuthenticationService
If you choose to use ONLY Auth0Strategy
, your app config (config/default.json
) will need to look like this (note the addition of a secret
prop that must be set, but is not used):
{
"authentication": {
"auth0": {
"domain": "example.auth0.com"
},
"authStrategies": ["auth0"],
"secret": "i_am_not_used_but_must_be_set"
}
}
Otherwise, setup is the same. Additional configuration options are explained below.
Client setup
In your frontend app, you'll need to configure the standard @feathersjs/authentication-client
as follows:
// NOTE: this is almost exactly the same as standard client setup described in the official docs
const feathers = require('@feathersjs/feathers')
const socketio = require('@feathersjs/socketio-client')
const io = require('socket.io-client')
const auth = require('@feathersjs/authentication-client')
const socket = io('http://api.feathersjs.com')
const api = feathers()
// Setup the transport (Rest, Socket, etc.) here
api.configure(socketio(socket))
// Make sure the authentication client is configured to use the `auth0` strategy
api.configure(auth({
jwtStrategy: 'auth0' // <-- IMPORTANT!!!
}))
Client usage example
The key thing to remember here, is that FeathersJS is NOT being used to authenticate users in your client app. The authentication process is being handled by Auth0, and will result in an access token being stored in the client app, usually in some place like local storage, a cookie, or using a package like LocalForage
. FeathersJS does NOT create or issue a JWT (access token) for you like it does with other authentication strategies, but rather just verifies that the access token being sent from your app (i.e. the one acquired from Auth0) is valid and unexpired.
As such, setting up authentication in your client is beyond the scope of this documentation. Refer to the Auth0 docs for more information on this topic. You just need to make sure that the access tokens that you receive from Auth0 are stored in a place that @feathersjs/authentication-client
expects to find them (see the config example above).
So, assuming your user has successfully logged in, and a valid access token is stored in the browser somewhere, the following should "just work"™:
// using async/await
const getTodos = async () => {
let todos = []
try {
// if configured correctly, your feathers client should get
// the access token from where it is stored and automatically
// send it in the `Authorization` header for the request
const result = await app.service('todos').find({})
todos = result.data
} catch (err) {
// if the access token was missing or invalid (e.g. expired)
// an error will be thrown and you need to handle (re)authenticating
// your client app
}
return todos
}
// using promises
app.service('todos').find({})
.then(
result => {
// if configured correctly, your feathers client should get
// the access token from where it is stored and automatically
// send it in the `Authorization` header for the request
const todos = result.data
}
)
.catch(
err => {
// if the access token was missing or invalid (e.g. expired)
// an error will be thrown and you need to handle (re)authenticating
// your client app
}
)
Custom configuration
Auth0Strategy
and Auth0Service
allow for a number of additional configuration settings that will be described here. Here is an example of configuration with all options specified:
{
"authentication": {
"auth0": {
"create": false,
"domain": "example.auth0.com",
"entity": "user",
"entityId": "user_id",
"header": "Authorization",
"jwtOptions": {}, // <= these apply ONLY to auth0 and NOT other auth strategies
"schemes": ["Bearer", "JWT"],
"service": "users",
"whitelist": 'us'
},
"authStrategies": ["auth0", "jwt"],
"entity": "user",
"entityId": "_id",
"header": "Authorization",
"jwtOptions": {}, // <= these apply to all other strategies EXCEPT auth0
"schemes": ["Bearer", "JWT"],
"service": "users"
}
}
Note that if a configuration key is specified in BOTH the auth0
property and in the main authentication
block, the one inside the auth0
block will take precedence. That way you can have multiple strategies that use, e.g. different services, entities, or check against different entity IDs. Also note, that if you use ONLY Auth0, the entity
, entityId
and service
values must be specified in the main authentication
block because the service cannot be initialized without them.
The default jwtOptions
currently resolve to:
const jwtOptions = {
algorithms: ['RS256'],
audience: [
`https://${domain}/api/v2/`,
`https://${domain}/userinfo`
],
ignoreExpiration: false,
issuer: `https://${domain}/`
}
Where domain
is the authentication.auth0.domain
property in the config. domain
is a REQUIRED option and an error will be thrown if it is not set. Likewise the JWKS URL (where the package will go to retrieve the signing key) is set to: https://${domain}/.well-known/jwks.json
. If you use a custom domain with your Auth0 account, it should still work just fine assuming the URL structures are the same (I've never used a custom domain with Auth0 so I don't know exactly how it works).
The users
service
Auth0Strategy
gets the Auth0 user_id
from the sub
claim of the decoded JWT access token. By default, when looking up the associated user or entity in your FeathersJS API, it tries to retrieve a user
from the users
service using app.service('users').find({ query: { user_id } })
and returning the first matching result (since the user_id
should be unique, there should be only a single result).
If you'd like to store your user information in a different service, and/or use a different field for user_id
, this can be accomplished by updating the authentication
config. For example, say you call your users "members" and store them in a service with a matching name, you could have a config like:
{
"authentication": {
"auth0": {
"domain": "example.auth0.com"
},
"authStrategies": ["auth0"],
"entity": "member",
"entityId": "member_id",
"service": "members"
}
}
The fromAuth0()
IP address whitelist
This package includes a fromAuth0()
hook which is designed to allow requests to your API that come from one of Auth0's published IP addresses. By default, the list of whitelisted IP addresses is set to the US region. There are several ways to customize the whitelist. From the app config:
{
"authentication": {
"auth0": {
"domain": "example",
"whitelist": [
"111.111.111.111",
"222.222.222.222"
]
},
"authStrategies": ["auth0"],
"entity": "member",
"entityId": "member_id",
"service": "members"
}
}
You can also update the app config on the fly, e.g.:
// get the current authentication config
const config = app.get('authentication')
// update the auth0.whitelist property
config.auth0.whitelist = ['123.45.67.89']
// update the config with the new values
app.set('authentication', config)
And, if you are using the fromAuth0()
hook directly, you can pass an object with a single whitelist
property, e.g.:
app.service('todos').hooks({
before: {
all: [
unless(fromAuth0({ whitelist: ['123.45.67.89']}), authenticate('auth0'))
]
}
})
If your Auth0 server is in Europe or Australia, you can specify that in the configuration:
{
"authentication": {
"auth0": {
"domain": "example",
"whitelist": "eu" // or set to "au" for Australia
},
"authStrategies": ["auth0"],
"entity": "user",
"entityId": "user_id",
"service": "users"
}
}
If the whitelist is set to an empty array, i.e. []
, it will disallow non-authenticated requests from any external sources.
FAQ
I don't know if these are technically "frequently-asked", but they are questions...
Do you provide TypeScript typings
No, not yet. The package is currently written in plain JavaScript. I do plan at some point to update it to use TypeScript, but I can't say for sure when that will be. I know this is not a ton of work. I used to use TypeScript with some frequency, but it's been a while and frankly, the cognitive overhead of brushing up on TypeScript to get this out the door was more than I had bandwidth for at the time I wrote it. :sweat_smile:
Does Auth0Strategy
work in conjunction with other strategies
Is it possible for the Auth0Strategy
to be one of several, e.g. setting "authStrategies": ["auth0", "jwt", "local"]
in the authentication
config? To be honest, I haven't tried it, so I don't know. I'd love to get reports from others as to whether it works or not. I think it should work fine, though.
Why not just store the signing certificate on the server
The signing certificate used with the RS256
algorithm is a "public key" and as such, is not terribly sensitive. As an alternative to using this package, one could theoretically (i.e. I haven't actually tried this) download the signing key from Auth0 (it's in Auth0 Website > Dashboard > Applications > Your App > Advanced Settings > Certificates
) and store it on the API (FeathersJS) server, and then use the built-in JwtStrategy
with the following configuration (or something similar? like I said, I haven't tested this method):
{
"authentication": {
"secret": "your signing key",
"authStrategies": ["jwt"],
"jwtOptions": {
"audience": [
"https://example.auth0.com/api/v2/",
"https://example.auth0.com/userinfo"
],
"header": {
"typ": "JWT"
},
"issuer": "https://example.auth0.com/",
"algorithm": "RS256"
}
}
}
This is probably a perfectly satisfactory way to use RS256
authentication with your FeathersJS app. However, there are a couple of potential "gotchas" to consider:
- As discussed in this Auth0 blog post, one of the key goals of using an asymmetric signing algorithm like
RS256
is that you can avoid storing a secret key on multiple servers - Signing keys can change over time. Although this is probably a rare event, a not-so-unlikely scenario involves an app maintainer deciding to rotate the signing key. This would, in effect, force all of the users of your app to be logged out, and require them to re-authenticate. In this scenario, the app maintainer would have to remember to download and replace the signing key on the API server whenever it was rotated. This additional step introduces additional points of potential failure, e.g. forgetting to update the server, the signing key is corrupted in transit, etc.
- The JWKS architecture allows there to be multiple valid signing keys. The
JwtStrategy
only allows you to have a single secret associated with your authentication strategy.
The Auth0Strategy
addresses and handles each of these points, and is designed to operate according to the standard.
Why does Auth0Service
override the setup()
function
The FeathersJS docs say explicitly:
When extending
setup
,super.setup(path, app)
should always be called, otherwise events and real-time connection authentication will no longer work.
Because the default Authentication
service throws an error if secret
is not set in the configuration, it is necessary to set a dummy secret in the event that auth0
is the ONLY authentication strategy being used. Also, the setup()
function is the place where the authenticate
hook is auto-registered on services.
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!!!