FeathersJS Service Hooks: Designing the params.provider Trust Boundary
Use FeathersJS before hooks to separate external transport requests from trusted internal service calls, with checks for hook ordering and failure modes.
12 Jul 2025, 11:36 UTC

The Problem: Authorization Logic Drifts Across Transports
In a FeathersJS application, the same service method can be reached through HTTP, a WebSocket, or directly from another service inside the same Node.js process. FeathersJS services implement a fixed CRUD interface (find, get, create, update, patch, remove), and each method can be wrapped in before hooks—functions that run before the service method executes. When authorization checks live inside route handlers or transport-specific middleware, internal calls can bypass them entirely, or external callers can reach logic meant for trusted code only. The result is inconsistent enforcement: a client creates a record through the REST API without the right role, while an internal script performs the same write without any audit hook.
Useful takeaway: place authorization and validation in a FeathersJS before hook and use context.params.provider to distinguish external requests from internal service calls. This keeps one control point for every path into a service.
Requirements
- FeathersJS v4 or v5 application with at least one transport, such as REST or Socket.IO.
- A service path to protect, for example messages.
- Ability to run hooks and call the service both through the transport and internally via app.service('path').
Smallest Suitable Design
Register a single before hook on the service. The hook inspects context.params.provider. If the value is a string such as rest or socketio, the request arrived through an external transport. If provider is undefined, the call originated inside the application, for example from another service or a scheduled job.
External requests run through authentication and authorization logic. Internal calls from trusted code can skip transport-level authentication, but sensitive operations still require an explicit permission argument passed through params, because provider alone does not prove the caller is authorized.
// Example hook skeleton. Run in the application directory with Node.js installed.
// Replace 'messages' with your actual service path.
module.exports = function authorizeExternal (options = {}) {
return async function (context) {
const { params, method, data } = context
if (params.provider) {
// External transport request: enforce authentication and roles here.
if (!params.user) {
throw new Error('Not authenticated')
}
if (method === 'create' && !params.user.canCreate) {
throw new Error('Forbidden')
}
}
// Internal calls: provider is undefined. Validate explicit trust tokens
// passed through params if the operation touches sensitive data.
if (method === 'remove' && params.provider === undefined && !params.trustedInternal) {
throw new Error('Internal remove requires explicit trust flag')
}
return context
}
}
Register the hook in the service configuration so it runs before the database adapter, the library that translates Feathers queries into database operations:
// app/services/messages/messages.hooks.js (structure varies by Feathers version)
const { authenticate } = require('@feathersjs/authentication')
const authorizeExternal = require('./authorize.external.hook')
module.exports = {
before: {
all: [ authenticate('jwt'), authorizeExternal() ],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
after: {},
error: {}
}
FeathersJS runs before hooks in registration order. Later hooks see changes made by earlier ones, so place authentication before authorization, and avoid mutating context.data in a way that later hooks depend on unless that dependency is intentional.
Trust and Data Boundary: params.provider
params.provider is set by the transport layer for requests arriving over HTTP or WebSocket. It is undefined when one service calls another through app.service('path').find(). This is a data boundary, not an authentication boundary. An internal caller running in the same process already has access to the application instance, so provider === undefined means internal but not necessarily safe.
Treat provider as a signal for transport-level concerns such as authentication middleware, rate limiting, or real-time event emission. Use explicit fields such as params.trustedInternal or params.callerId for authorization decisions inside the application.
Operational Checks
Run these checks from the application directory as a normal user. No elevated permissions are required unless the application binds to a port below 1024.
- Start the FeathersJS application.
- Call the service through the REST client, for example GET /messages, and confirm the before hook logs context.method === 'find' and context.params.provider === 'rest'.
- Call the same service internally from a Node script or another service:
// Internal call example. Run in the application directory.
// Replace 'messages' with your service path.
const messages = app.service('messages')
const result = await messages.find({
query: { $limit: 5 },
provider: undefined
})
console.log(result)
- Confirm the hook observes params.provider === undefined and that the internal call does not run transport-only middleware such as CSRF checks.
- Throw an error in a before hook and confirm the service method returns an error without executing the database adapter or running after hooks.
- Paginate a find call with $limit and $skip in params.query and verify the response contains the expected subset and a total when pagination is enabled.
Failure Modes
| Failure mode | Cause | Effect |
|---|---|---|
| Before hook throws | Rejected promise or thrown error in a before hook | Service method short-circuits; the adapter and after hooks do not run; error hooks run if registered |
| Silent data mutation | Earlier before hook modifies context.data or context.params.query | Later hooks and the adapter operate on changed values; difficult to trace because no error is raised |
| Missing real-time events | context.dispatcher used in pure HTTP mode without transport event configuration | Event emission is silently dropped; clients do not receive created, patched, or removed notifications |
| Internal bypass | Authorization logic keyed only on params.provider | Trusted internal code performs sensitive writes without permission validation |
Conditions That Would Change the Design
- If you move from FeathersJS v4 to v5, import paths and package structure change; hook registration may need updating.
- If you remove WebSocket support and run pure HTTP request/response, real-time event emission through context.dispatcher is not guaranteed; do not rely on it for business-critical notifications.
- If internal services need to act on behalf of external users, pass the user context explicitly through params instead of reconstructing it from the transport.
Limitations
Hooks do not replace database-level authorization such as row-level security or adapter-specific query filtering. A before hook can reject a request, but it cannot prevent a misconfigured adapter from returning data the caller should not see. Combine hook-level checks with adapter query constraints where the datastore supports them.
Practical Verification
Create a minimal FeathersJS app, register a service with a before hook that logs context.method, context.params.provider, and context.data, then call the service through the REST client and internally through app.service('path').find(). The provider value should differ between the two calls. This confirms the trust boundary is observable before you add authorization logic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.