Search code examples
feathersjsfeathers-hookfeathers-authentication

Hook to check if user is authenticate in Feathers (without send Unauthorized error if not, just check)


I want to implement a simple "published" field for the documents in database.

For example, if an user is not authenticated, I want to always insert { published: true } in the query for all find/get request, so it will return only published documents (useful in front end)

But if the user is authenticated and have required roles, I want to return all documents, not only published (useful in admin).

So I'm trying to create a before hook:

// assignQuery.hook.js

const { some } = require('lodash');

const defaultOptions = {
  rolesField: 'roles',
  query: {},
  allowRoles: []
};
module.exports = function (options) {
  return async context => {

    options = Object.assign({}, defaultOptions, options);

    // If it was an internal call then skip this hook
    if (!context.params.provider) {
      return context;
    }

    const { user } = context.params;

    // Return unchanged context if user have some "allowRoles"
    if (user && options.allowRoles && options.allowRoles.length && some(options.allowRoles, (role) => {
      return (user[options.rolesField] || '').includes(role);
    })) return context;

    // else assign query filter
    context.params.query = Object.assign({}, context.params.query, options.query);

    return context;
  };
};

// service.hooks.js

module.exports = {
  before: {
    all: [],
    find: [
      assignQuery({ query: {published:true}, allowRoles: ['admin', 'edit'] }),
    ],
    get: [
      assignQuery({ query: {published:true}, allowRoles: ['admin', 'edit'] }),
    ],
    create: [ 
      authenticate('jwt'), 
      restrictToRoles({ roles: ['admin', 'edit']}),
    ],
    update: [ 
      authenticate('jwt'), 
      restrictToRoles({ roles: ['admin', 'edit']}),})
    ],
    patch: [ 
      authenticate('jwt'), 
      restrictToRoles({ roles: ['admin', 'edit']}),
    ],
    remove: [ 
      authenticate('jwt'),
      restrictToRoles({ roles: ['admin', 'edit']})
    ]
  },

};

The problem is I have to authenticate user to get his roles, so if user is not authenticated, the service will send an Unauthorized error, in place of allow the response with the filtered query.

So I have to call authenticate before assignQuery. In this case it will work fine for admin, but unauthorized users in front-end cannot retrieve published articles.

And if I don't authenticate before assignQuery, it's working fine in front-end, unauthorized users can see only published articles, but it's not working anymore in the admin, administrators will always receive only published articles too, because in the assignQuery hook, context.params.user is undefined (because we don't authenticate before...)

So I need a way to check if user is authenticated, but not send him an Unauthorized error if he is unauthorized, I just want to send the response of the modified query.

I think I need something like this:

before: {
    all: [],
    find: [
      unless(isAuthenticateAndHaveRoles({roles:['admin', 'edit']}), 
        assignQuery({ query: {published:true} })
      )
    ],
    ...

But I don't know how to make a "no-blocking" authenticate or an isAuthenticate hook. Is it possible?

I hope my explanation is not so confuse, the goal is just to implement a published/draft (visible/hidden) (online/offline) field for my documents.

Any help is welcome. Thank you so much!


Solution

  • For Feathers v4 and later, you can use the anonymous authentication strategy.

    For v3 and earlier, the authenticate hook has a currently undocumented allowUnauthenticated option which, if set to true, just ads a params.authenticated flag. So

    find: [
      authenticate('jwt', {
        allowUnauthenticated: true
      }),
      assignQuery({ query: {published:true}, allowRoles: ['admin', 'edit'] }),
    ]
    

    Should do it.