FeathersJS Auth Recipe: Create Endpoints with Mixed Auth
The Auk release of FeathersJS includes a powerful new authentication suite built on top of PassportJS. It can be customized to handle almost any app’s authentication requirements. In this guide, we’ll look at how to handle a fairly common auth scenario: Sometimes an endpoint needs to serve different information depending on whether the user is authenticated. An unauthenticated user might only see public records. An authenticated user might be able to see additional records.
Setup the Authentication Endpoint
To get started, we need a working authentication setup. Below is a default configuration and authentication.js
. They contain the same code generated by the feathers-cli. You can create the below setup using the following terminal commands:
npm install -g @feathersjs/cli
mkdir feathers-demo-local; cd feathers-demo-local
or a folder name you prefer.feathers generate app
use the default prompts.feathers generate authentication
- Select
Username + Password (Local)
when prompted for a provider. - Select the defaults for the remaining prompts.
- Select
config/default.json:
{
"host": "localhost",
"port": 3030,
"public": "../public/",
"paginate": {
"default": 10,
"max": 50
},
"authentication": {
"secret": "99294186737032fedad37dc2e847912e1b9393f44a28101c986f6ba8b8bc0eaab48b5b4c5178f55164973c76f8f98f2523b860674f01c16a23239a2e7d7790ae9fa00b6de5cc0565e335c6f05f2e17fbee2e8ea0e82402959f1d58b2b2dc5272d09e0c1edf1d364e9911ecad8172bdc2d41381c9ab319de4979c243925c49165a9914471be0aa647896e981da5aec6801a6dccd1511da11b696d4f6cce3a4534dab9368661458a466661b1e12170ad21a4045ce1358138caf099fbc19e05532336b5626aa376bc158cf84c6a7e0e3dbbb3af666267c08de12217c9b55aea501e5c36011779ee9dd2e061d0523ddf71cb1d68f83ea5bb1299ca06003b77f0fc69",
"strategies": [
"jwt",
"local"
],
"path": "/authentication",
"service": "users",
"jwt": {
"header": {
"typ": "access"
},
"audience": "https://yourdomain.com",
"subject": "anonymous",
"issuer": "feathers",
"algorithm": "HS256",
"expiresIn": "1d"
},
"local": {
"entity": "user",
"service": "users",
"usernameField": "email",
"passwordField": "password"
}
},
"nedb": "../data"
}
src/authentication.js:
'use strict';
const authentication = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
const local = require('@feathersjs/authentication-local');
module.exports = function () {
const app = this;
const config = app.get('authentication');
app.configure(authentication(config));
app.configure(jwt());
app.configure(local(config.local));
app.service('authentication').hooks({
before: {
create: [
authentication.hooks.authenticate(config.strategies)
],
remove: [
authentication.hooks.authenticate('jwt')
]
}
});
};
Set up a “Mixed Auth” Endpoint
Now we need to setup an endpoint to handle both unauthenticated and authenticated users. For this example, we’ll use the /users
service that was already created by the authentication generator. Let’s suppose that our application requires that each user
record will contain a public
boolean property. Each record will look something like this:
{
id: 1,
email: 'my@email.com'
password: "password",
public: true
}
If a user
record contains public: true
, then unauthenticated users should be able to access it. Let’s see how to use the iff
and else
conditional hooks from feathers-hooks-common
to make this happen. Be sure to read the iff hook API docs
and else hook API docs
if you haven’t, yet.
We’re going to use the iff
hook to authenticate users only if a token is in the request. The feathers-authentication-jwt
plugin, which we used in src/authentication.js
, includes a token extractor. If a request includes a token, it will automatically be available inside the context
object at context.params.token
.
src/services/users/users.hooks.js
(This example only shows the find
method’s before
hooks.)
'use strict';
const { authenticate } = require('@feathersjs/authentication').hooks;
const commonHooks = require('feathers-hooks-common');
module.exports = {
before: {
find: [
// If a token was included, authenticate it with the `jwt` strategy.
commonHooks.iff(
context => context.params.token,
authenticate('jwt')
// No token was found, so limit the query to include `public: true`
).else( context => Object.assign(context.params.query, { public: true }) )
]
}
};
Let’s break down the above example. We setup the find
method of the /users
service with an iff
conditional before hook:
iff(
context => context.params.token,
authenticate(‘jwt’)
)
For this application, the above snippet is equivalent to the snippet, below.
context => {
if (context.params.token) {
return authenticate(‘jwt’)
} else {
return Promise.resolve(context)
}
}
The iff
hook is actually more capable than the simple demonstration, above. It can handle an async predicate expression. This would be equivalent to being able to pass a promise
inside the if
statement’s parentheses. It also allows us to chain an .else()
statement, which will run if the predicate evaluates to false.
.else(
context => {
Object.assign(context.params.query, { public: true })
return context
)
The above statement simply adds public: true
to the query parameters. This limits the query to only find user
records that have the public
property set to true
.
Wrapping Up
With the above code, we’ve successfully setup a /users
service that responds differently to unauthenticated and authenticated users. We used the context.params.token
attribute to either authenticate a user or to limit the search query to only public users. If you become familiar with the Common Hooks API, you’ll be able to solve almost any authentication puzzle.