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:

  1. npm install -g feathers-cli@pre
    or
    yarn global feathers-cli@pre
  2. mkdir feathers-demo-local; cd feathers-demo-local
    or a folder name you prefer.
  3. feathers generate app
    use the default prompts.
  4. feathers generate authentication
    • Select Username + Password (Local) when prompted for a provider.
    • Select the defaults for the remaining prompts.

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": {
        "type": "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('feathers-authentication');
const jwt = require('feathers-authentication-jwt');
const local = require('feathers-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 hook object at hook.params.token.

src/services/users/users.hooks.js
(This example only shows the find method's before hooks.)

'use strict';

const { authenticate } = require('feathers-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(
        hook => hook.params.token,
        authenticate('jwt')
      // No token was found, so limit the query to include `public: true`
      ).else( hook => Object.assign(hook.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(
  hook => hook.params.token,
  authenticate(‘jwt’)
)

For this application, the above snippet is equivalent to the snippet, below.

hook => {
  if (hook.params.token) {
    return authenticate(‘jwt’)
  } else {
    return Promise.resolve(hook)
  }
}

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( hook => Object.assign(hook.params.query, { public: true }) )

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 hook.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.

results matching ""

    No results matching ""