Api Key Exposure in Feathersjs with Oauth2
Api Key Exposure in Feathersjs with Oauth2 — how this specific combination creates or exposes the vulnerability
FeathersJS is a framework for creating JavaScript APIs with services and hooks. When OAuth 2.0 is used for authorization, developers often configure authentication hooks and pass tokens via headers. Misconfigured hooks or unprotected service routes can inadvertently expose API keys or bearer tokens used for downstream calls.
In FeathersJS, an OAuth 2.0 setup typically includes an authentication hook, custom headers for bearer tokens, and service logic that may forward credentials to external APIs. If the service does not properly strip or validate incoming authorization headers before making outbound calls, an API key intended for a backend service can be reflected in logs, error responses, or client payloads. For example, a hook that forwards authorization headers without validation might send an API key to a third-party endpoint that echoes it in debug output, creating a leakage path.
Consider a FeathersJS service that calls an external API using a static API key stored in environment variables. If the hook merges the request headers into the external call without filtering, the key may be exposed to the client-side response or to any interceptor that can read outbound traffic. Additionally, misconfigured CORS or improperly guarded service methods can allow unauthenticated callers to trigger routes that perform authenticated calls, inadvertently surfacing key material through verbose error messages or inconsistent state handling.
OAuth 2.0 access tokens themselves are not API keys, but the pattern of using bearer tokens in headers alongside static keys can create confusion. If a developer mistakenly treats an OAuth access token as an API key and logs it, or if the token is passed to an endpoint that does not require authentication, the token can be leaked to unauthorized parties. FeathersJS hooks that do not explicitly remove or mask sensitive headers before invoking services increase the risk of exposure.
Another vector involves service-to-service communication where an internal FeathersJS endpoint uses an API key to authenticate with another service. If this endpoint is exposed publicly without proper authentication, an attacker can invoke it and observe reflected key data in responses, logs, or error traces. The combination of OAuth 2.0 flows for user identity and separate API keys for service identity can lead to accidental mixing of credentials in middleware, increasing the surface area for exposure.
Oauth2-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on strict header management, proper OAuth 2.0 flows, and isolating service credentials. Always validate and sanitize incoming headers, avoid forwarding sensitive headers to external services, and ensure tokens are scoped appropriately.
Example 1: OAuth 2.0 authentication hook with header filtering
// src/hooks/authentication.js
const { AuthenticationError } = require('@feathersjs/errors');
module.exports = function (options = {}) {
return async context => {
const { headers } = context;
const authHeader = headers.authorization || '';
if (!authHeader.startsWith('Bearer ')) {
throw new AuthenticationError('Unauthorized');
}
const token = authHeader.slice(7);
// Validate token via OAuth2 introspection or public key verification
const isValid = await validateOAuthToken(token);
if (!isValid) {
throw new AuthenticationError('Invalid token');
}
// Attach user info to context and remove raw authorization header
context.params.user = { token };
delete context.headers.authorization;
return context;
};
};
async function validateOAuthToken(token) {
// Example introspection call (do not forward raw token to client)
const response = await fetch('https://auth.example.com/introspect', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.INTROSPECTION_KEY}` },
body: new URLSearchParams({ token, client_id: process.env.CLIENT_ID })
});
const data = await response.json();
return data.active === true;
}
Example 2: Service call with filtered headers and no API key leakage
// src/hooks/forward-without-secrets.js
module.exports = function (options = {}) {
return async context => {
const { headers, method, url, body } = context;
// Remove sensitive headers before forwarding
const filteredHeaders = { ...headers };
delete filteredHeaders.authorization;
delete filteredHeaders['x-api-key'];
// Perform outbound call using service-specific credentials, not request headers
const response = await fetch('https://external.example.com/data', {
method,
headers: {
'Content-Type': 'application/json',
'X-Service-Key': process.env.SERVICE_API_KEY
},
body: JSON.stringify(body)
});
context.result = await response.json();
return context;
};
};
Example 3: OAuth 2.0 protected endpoint with scopes
// src/hooks/require-scopes.js
module.exports = function (options = { requiredScopes: ['read:data'] }) {
return async context => {
const { params: { user } } = context;
if (!user || !user.scopes) {
throw new Error('Unauthorized');
}
const hasScope = options.requiredScopes.some(scope => user.scopes.includes(scope));
if (!hasScope) {
throw new Error('Insufficient scope');
}
return context;
};
};
Ensure that the OAuth 2.0 configuration in app.js explicitly sets token handling and does not expose secrets:
// src/app.js
const feathers = require('@feathersjs/feathers');
const authentication = require('@feathersjs/authentication');
const jwt = require('@feathersjs/authentication-jwt');
const { express } = require('@feathersjs/express');
const app = express(feathers());
app.configure(authentication({
secret: process.env.AUTH_SECRET,
strategies: ['jwt'],
path: '/authentication'
}));
app.use('/api', {
async find(params) {
// Ensure no API key is included in response
const results = await externalService.getData({ token: params.headers?.authorization || '' });
return results.map(({ sensitiveField, ...safe }) => safe);
}
});
These examples emphasize explicit header removal, token validation, and scoped access to reduce the chance of API key exposure when OAuth 2.0 is in use.