Api Key Exposure in Feathersjs with Jwt Tokens
Api Key Exposure in Feathersjs with Jwt Tokens — how this specific combination creates or exposes the vulnerability
FeathersJS is a framework for real-time APIs that commonly uses JWT tokens for stateless authentication. When API keys are embedded in client-side code, HTTP requests, or JavaScript bundles and JWT tokens are used for authorization, the combination can unintentionally expose sensitive credentials. A typical misconfiguration occurs when an API key intended for server-to-server services is stored in environment variables that are accessible to the client-side build, or when the client application reads the key from a configuration file that is bundled and shipped to browsers.
In FeathersJS, services are often defined with hooks that rely on the authentication header containing a JWT. If an API key is passed in a custom header or query parameter alongside the JWT, and the server echoes or logs that header without sanitization, the API key may be exposed in logs, error messages, or through overly verbose debugging endpoints. For example, sending X-API-Key in clear text together with an Authorization bearer JWT over HTTP (not HTTPS) can lead to interception. Even when HTTPS is used, if CORS is permissive and preflight responses expose headers containing the API key, browsers or malicious sites can read those values via JavaScript.
Another exposure vector arises from service hooks and transports. FeathersJS supports multiple transports such as REST and Socket.io; if the API key is stored in a hook’s params object and that object is serialized into logs or error responses, the key can leak. JWT tokens themselves may carry claims that include identifiers used to look up permissions; if those identifiers are derived from or include API keys, the token becomes a secondary channel for key exposure. Misconfigured feathers-authentication hooks that accept both JWT and API key in the same request can treat the API key as a secret while inadvertently returning it in responses or in client-side runtime contexts.
Consider a service that validates a JWT and then uses a header value to route to a third-party provider. If the header value is the API key and the server does not strip it before sending an error to the client, the key can be reflected back in stack traces or validation messages. Additionally, if the server uses JWTs with long lifetimes and stores the associated API key in an insecure cache or session store that is shared across instances, an attacker who compromises one instance can retrieve the key through SSRF or log scraping.
These issues are not inherent to JWTs, but to how keys and tokens are handled together. JWTs provide integrity and identity, but they do not prevent accidental leakage of separate credentials. The risk is elevated when development practices treat configuration the same across environments, or when secrets are passed to the client under the assumption that JWTs will protect them. Continuous scanning focused on authentication and data exposure is valuable for detecting such patterns before they reach production.
Jwt Tokens-Specific Remediation in Feathersjs — concrete code fixes
To reduce exposure, isolate API keys from JWT handling and ensure keys never reach the client or appear in logs. Store API keys securely on the server, reference them via environment variables, and avoid placing them in hook params that may be serialized or logged. Below are concrete examples for a FeathersJS service that uses JWT authentication while safely using an API key for outbound calls.
Example 1: JWT-only authentication without exposing API keys
Configure authentication to rely solely on JWTs and keep API keys server-side.
// src/authentication.js
const authentication = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
module.exports = function (app) {
const config = app.get('authentication');
const auth = authentication(config);
app.configure(auth);
app.configure(jwt());
};
Ensure the JWT secret is strong and stored in an environment variable. Do not embed API keys in the JWT payload.
Example 2: Server-side API key usage in a service hook
Access the API key from server-only configuration and use it in a hook without attaching it to params or responses.
// src/hooks/api-key-hook.js
module.exports = function apiKeyHook(options = {}) {
return async context => {
const { apiKey } = context.params.provider ? context.params : {};
// API key should come from server-side config, not client params
const serverApiKey = process.env.EXTERNAL_API_KEY;
if (!serverApiKey) {
throw new Error('Missing server API key');
}
// Use serverApiKey for outbound request headers, not context.params
context.headers = context.headers || {};
context.headers['X-External-Key'] = serverApiKey;
// Ensure no sensitive headers are echoed back
delete context.headers['X-API-Key'];
return context;
};
};
Apply the hook in your service definition:
// src/services/items/index.js
const { authenticate } = require('@feathersjs/authentication').hooks;
const apiKeyHook = require('./hooks/api-key-hook');
module.exports = function (app) {
const options = {
name: 'items',
paginate: { default: 10, max: 25 }
};
app.use('/items', createService(options));
const service = app.service('items');
service.hooks({
before: {
all: [authenticate('jwt'), apiKeyHook()],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
after: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
error: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
});
};
Example 3: Secure HTTP client setup that avoids leaking headers
When calling external services, use a client that does not automatically forward all incoming headers.
// src/hooks/external-client.js
const axios = require('axios');
module.exports = function externalClient(context) {
const serverApiKey = process.env.EXTERNAL_API_KEY;
const instance = axios.create({
baseURL: 'https://api.external.example.com',
timeout: 5000,
headers: {
'Authorization': `Bearer ${context.params.accessToken || ''}`,
'X-External-Key': serverApiKey
}
});
// Ensure no response headers containing secrets are passed back
instance.interceptors.response.use(response => {
// Do not reflect sensitive response headers to the client
return response;
}, error => {
if (error.response) {
// Strip sensitive headers from error responses
const { headers, ...safeResponse } = error.response;
throw new Error('External service error');
}
return Promise.reject(error);
});
return instance(context.url || context.path, {
method: context.method || 'get',
data: context.data,
params: context.params.query
});
};
General hardening practices
- Never place API keys in client-side JavaScript, HTML, or JSON responses.
- Use strict CORS policies and avoid exposing custom headers like
X-API-KeyinAccess-Control-Expose-Headers. - Scrub logs and error messages to remove header values that may contain keys.
- Rotate API keys regularly and scope them to least privilege.
- Prefer short-lived JWTs and store refresh tokens securely; keep API keys independent of JWT claims.
These patterns help ensure that JWT tokens handle identity and authorization while API keys remain server-only secrets, reducing the chance of accidental exposure through logs, error responses, or misconfigured hooks.