Broken Authentication in Feathersjs with Api Keys
Broken Authentication in Feathersjs with Api Keys — how this specific combination creates or exposes the vulnerability
FeathersJS is a framework for real-time web applications and REST APIs. When using API keys for authentication, broken authentication can arise from insecure key handling, missing validation, and weak integration with the service layer. An API key is a long-term credential; if it is transmitted over unencrypted channels, stored in client-side code, or accepted without robust validation and scope checks, the API surface becomes vulnerable to unauthorized access.
One common pattern in Feathers is to accept an API key in a header (for example, x-api-key) and use it to identify a service caller. If the service route does not enforce authentication for that key, or if the key is compared using a non-constant-time check, an attacker can enumerate valid keys or escalate privileges. A second vector is key leakage: keys included in URLs or logs may be captured in browser history, server logs, or error messages. A third vector is missing scope/role checks; a key intended for read-only access may be accepted by write routes if the service does not validate permissions on each operation. These issues map to the broken authentication category because the identity proof (the API key) is not verified with sufficient rigor and the authorization boundary around its use is weak.
Consider an unprotected Feathers service that accepts an API key but does not ensure it is tied to a specific set of allowed actions. An attacker who obtains a key (through social engineering, accidental exposure in client-side bundles, or log leakage) can call any method exposed by the service. If the service also exposes sensitive data endpoints without additional context checks (such as ensuring the caller is allowed to access only their own resources), the vulnerability can lead to mass data exposure. The OpenAPI contract may declare security schemes for API keys, but if runtime enforcement is inconsistent or skipped, the spec becomes misleading and the unauthenticated attack surface grows. In such cases, scanning with tools that test both spec-defined expectations and runtime behavior—like checking whether a key is required for sensitive operations—can reveal gaps between declared and actual security.
Real-world attack patterns include enumeration of valid API keys via timing differences or error messages, injection of keys into SSRF payloads if the key is forwarded to internal services, and privilege escalation when higher-privilege keys are accidentally issued to low-privilege users. Because API keys are long-lived secrets, compromise can have a broad impact across the system. Mitigations should focus on secure transmission, strict validation, key scoping, and robust logging without exposing keys in responses or error details.
Api Keys-Specific Remediation in Feathersjs — concrete code fixes
To fix broken authentication when using API keys in FeathersJS, validate the key on every request, enforce scope/role checks, and avoid leaking the key in logs or URLs. Use environment variables to store keys, prefer HTTPS, and ensure your service configuration does not skip authentication for any route.
Example: secure API key authentication in a Feathers service with explicit key lookup and scope validation.
// src/services/notes/notes.class.js or hooks file
const crypto = require('node:crypto');
// Compare keys in constant time to avoid timing attacks
function safeKeyCompare(actual, supplied) {
return crypto.timingSafeEqual(
Buffer.from(actual || ''),
Buffer.from(supplied || '')
);
}
// In your service hooks
module.exports = {
before: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
after: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
error: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
};
// Example hook to validate API key for all operations
function validateApiKey(keyConfig) {
return async context => {
const { app, params } = context;
const supplied = context.headers['x-api-key'] || context.query.apiKey;
if (!supplied) {
throw new Error('Unauthorized: missing API key');
}
// Retrieve allowed keys from configuration or a secure store
const allowedKeys = keyConfig.allowedKeys || [];
const hasValidKey = allowedKeys.some(k => safeKeyCompare(k, supplied));
if (!hasValidKey) {
throw new Error('Unauthorized: invalid API key');
}
// Optional: scope/role check
const keyScopes = keyConfig.scopes && keyConfig.scopes[supplied];
if (context.method === 'create' || context.method === 'patch' || context.method === 'update' || context.method === 'remove') {
if (!keyScopes || !keyScopes.write) {
throw new Error('Forbidden: API key lacks write scope');
}
} else if (context.method === 'find' || context.method === 'get') {
if (!keyScopes || !keyScopes.read) {
throw new Error('Forbidden: API key lacks read scope');
}
}
// Attach identity to context for downstream use (avoid logging the key)
context.params.apiKeyId = keyConfig.keyIds && keyConfig.keyIds[supplied];
return context;
};
}
// In src/services/notes/notes.js or plugin configuration
const keyConfig = {
allowedKeys: [
// Store hashed or plaintext keys from environment; compare safely
process.env.FEATHERS_API_KEY_1,
process.env.FEATHERS_API_KEY_2
].filter(Boolean),
scopes: {
[process.env.FEATHERS_API_KEY_1]: { read: true, write: true },
[process.env.FEATHERS_API_KEY_2]: { read: true, write: false }
},
keyIds: {
[process.env.FEATHERS_API_KEY_1]: 'key-001',
[process.env.FEATHER_API_KEY_2]: 'key-002'
}
};
const notesService = app => new (class NotesService {
async find(params) {
// Use params.apiKeyId or validated identity from hooks
return app.service('notes')._getItems(params);
}
async get(id, params) {
return app.service('notes').get(id, params);
}
async create(data, params) {
if (!params.apiKeyId) throw new Error('Forbidden');
return app.service('notes').create(data, params);
}
async update(id, data, params) {
if (!params.apiKeyId) throw new Error('Forbidden');
return app.service('notes').update(id, data, params);
}
async patch(id, data, params) {
if (!params.apiKeyId) throw new Error('Forbidden');
return app.service('notes').patch(id, data, params);
}
async remove(id, params) {
if (!params.apiKeyId) throw new Error('Forbidden');
return app.service('notes').remove(id, params);
}
});
// Register the service with hooks
const notes = notesService(app);
notes.before.hooks.push(validateApiKey(keyConfig));
app.use('/notes', notes);
Additional recommendations: store keys in environment variables or a secrets manager, never in client-side code or logs; enforce HTTPS to prevent interception; rotate keys periodically; and prefer short-lived tokens for high-risk operations where feasible. These steps reduce the risk of broken authentication when relying on API keys in FeathersJS.
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |
Frequently Asked Questions
Can an API key in a URL or referer header cause broken authentication in FeathersJS?
x-api-key and avoid embedding keys in URLs.