FeathersJS Auth Recipe: Custom Auth Strategy

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. We can leverage this to create completely custom authentication strategies using Passport Custom. Let’s take a look at two such examples in this guide.

Setting up the basic app

Let’s first start by creating a basic server setup.

  1. const feathers = require('@feathersjs/feathers');
  2. const express = require('@feathersjs/express');
  3. const auth = require('@feathersjs/authentication');
  4. const jwt = require('@feathersjs/authentication-jwt');
  5. const memory = require('feathers-memory');
  6. const app = express(feathers());
  7. app.configure(express.rest());
  8. app.use(express.json());
  9. app.use(express.urlencoded({ extended: true }));
  10. app.configure(auth({ secret: 'secret' }));
  11. app.configure(jwt());
  12. app.use('/users', memory());
  13. app.hooks({
  14. before: {
  15. all: [auth.hooks.authenticate('jwt')]
  16. }
  17. });
  18. app.listen(8080);

Creating a Custom API Key Auth Strategy

The first custom strategy example we can look at is an API Key Strategy. Within it, we’ll check if there is a specific header in the request containing a specific API key. If true, we’ll successfully authorize the request.

First let’s make the strategy using passport-custom npm package.

  1. const Strategy = require('passport-custom');
  2. module.exports = opts => {
  3. return function() {
  4. const verifier = (req, done) => {
  5. // get the key from the request header supplied in opts
  6. const key = req.params.headers[opts.header];
  7. // check if the key is in the allowed keys supplied in opts
  8. const match = opts.allowedKeys.includes(key);
  9. // user will default to false if no key is present
  10. // and the authorization will fail
  11. const user = match ? 'api' : match;
  12. return done(null, user);
  13. };
  14. // register the strategy in the app.passport instance
  15. this.passport.use('apiKey', new Strategy(verifier));
  16. // Add options for the strategy
  17. this.passport.options('apiKey', {});
  18. };
  19. };

Next let’s add this to our server setup

  1. const apiKey = require('./apiKey');
  2. app.configure(
  3. apiKey({
  4. // which header to look at
  5. header: 'x-api-key',
  6. // which keys are allowed
  7. allowedKeys: ['opensesame']
  8. })
  9. );

Next let’s create a custom authentication hook that conditionally applies auth for all external requests.

  1. const commonHooks = require('feathers-hooks-common');
  2. const authenticate = () =>
  3. commonHooks.iff(
  4. // if and only if the request is external
  5. commonHooks.every(commonHooks.isProvider('external')),
  6. commonHooks.iffElse(
  7. // if the specific header is included
  8. ctx => ctx.params.headers['x-api-key'],
  9. // authentication with this strategy
  10. auth.hooks.authenticate('apiKey'),
  11. // else fallback on the jwt strategy
  12. auth.hooks.authenticate(['jwt'])
  13. )
  14. );
  15. app.hooks({
  16. before: {
  17. all: [authenticate()]
  18. }
  19. });

Finally our server.js looks like this:

  1. const feathers = require('@feathersjs/feathers');
  2. const express = require('@feathersjs/express');
  3. const auth = require('@feathersjs/authentication');
  4. const jwt = require('@feathersjs/authentication-jwt');
  5. const memory = require('feathers-memory');
  6. const commonHooks = require('feathers-hooks-common');
  7. const apiKey = require('./apiKey');
  8. const app = express(feathers());
  9. app.configure(express.rest());
  10. app.use(express.json());
  11. app.use(express.urlencoded({ extended: true }));
  12. app.configure(auth({ secret: 'secret' }));
  13. app.configure(jwt());
  14. app.configure(
  15. apiKey({
  16. header: 'x-api-key',
  17. allowedKeys: ['opensesame']
  18. })
  19. );
  20. app.use('/users', memory());
  21. const authenticate = () =>
  22. commonHooks.iff(
  23. commonHooks.every(commonHooks.isProvider('external')),
  24. commonHooks.iffElse(
  25. ctx => ctx.params.headers['x-api-key'],
  26. auth.hooks.authenticate('apiKey'),
  27. auth.hooks.authenticate(['jwt'])
  28. )
  29. );
  30. app.hooks({
  31. before: {
  32. all: [authenticate()]
  33. }
  34. });
  35. app.listen(8080);

Now any request with a header x-api-key and the value opensesame will be authenticated by the server.

Creating an Anonymous User Strategy

The second strategy we’ll look at is for an anonymous user. For this specific flow we’ll expect the client to call the /authentication endpoint letting us know that it wants to authenticate anonymously. The server will then create a new user and return a new JWT token that the client will have to use from that point onwards.

First let’s create the strategy using passport-custom

  1. const Strategy = require('passport-custom');
  2. module.exports = opts => {
  3. return function() {
  4. const verifier = async (req, done) => {
  5. // create a new user in the user service
  6. // mark this user with a specific anonymous=true attribute
  7. const user = await this.service(opts.userService).create({
  8. anonymous: true
  9. });
  10. // authenticate the request with this user
  11. return done(null, user, {
  12. userId: user.id
  13. });
  14. };
  15. // register the strategy in the app.passport instance
  16. this.passport.use('anonymous', new Strategy(verifier));
  17. };
  18. };

Next let’s update our server.js to use this strategy.

  1. const anonymous = require('./anonymous');
  2. app.configure(
  3. anonymous({
  4. // the user service
  5. userService: 'users'
  6. })
  7. );
  8. const authenticate = () =>
  9. commonHooks.iff(
  10. commonHooks.every(commonHooks.isProvider('external')),
  11. commonHooks.iffElse(
  12. ctx => ctx.params.headers['x-api-key'],
  13. auth.hooks.authenticate('apiKey'),
  14. // add the additional anonymous strategy
  15. auth.hooks.authenticate(['jwt', 'anonymous'])
  16. )
  17. );

Finally our server.js looks like this:

  1. const feathers = require('@feathersjs/feathers');
  2. const express = require('@feathersjs/express');
  3. const auth = require('@feathersjs/authentication');
  4. const jwt = require('@feathersjs/authentication-jwt');
  5. const memory = require('feathers-memory');
  6. const commonHooks = require('feathers-hooks-common');
  7. const apiKey = require('./apiKey');
  8. const anonymous = require('./anonymous');
  9. const app = express(feathers());
  10. app.configure(express.rest());
  11. app.use(express.json());
  12. app.use(express.urlencoded({ extended: true }));
  13. app.configure(auth({ secret: 'secret' }));
  14. app.configure(jwt());
  15. app.configure(
  16. apiKey({
  17. header: 'x-api-key',
  18. allowedKeys: ['opensesame']
  19. })
  20. );
  21. app.configure(
  22. anonymous({
  23. userService: 'users'
  24. })
  25. );
  26. app.use('/users', memory());
  27. const authenticate = () =>
  28. commonHooks.iff(
  29. commonHooks.every(commonHooks.isProvider('external')),
  30. commonHooks.iffElse(
  31. ctx => ctx.params.headers['x-api-key'],
  32. auth.hooks.authenticate('apiKey'),
  33. auth.hooks.authenticate(['jwt', 'anonymous'])
  34. )
  35. );
  36. app.hooks({
  37. before: {
  38. all: [authenticate()]
  39. }
  40. });
  41. app.listen(8080);

Now any such request will return a valid JWT token:

  1. POST /authentication
  2. {
  3. strategy: 'anonymous'
  4. }

Note that this looks very similar to a request body for local strategy:

  1. POST /authentication
  2. {
  3. strategy: 'local',
  4. username: 'admin',
  5. password: 'password'
  6. }

So for any new strategy we register, we can call the /authentication endpoint with a specific body and expect a valid JWT in return, which we can use from thereon.


As we can see it’s very easy to create a completely custom auth strategy in a standard passport way using passport-custom.

Happy Hacking!!