internal-dw-api-auth
v0.1.0
Published
Reusable server-to-server API-key authentication and explicit department context
Readme
internal-dw-api-auth
Reusable server-to-server API-key authentication for EIS, LMS, Policy Management and a common GraphQL API. This library supplies Express-compatible middleware, credential helpers and explicit department context. Applications own their server, database lookup, configuration and GraphQL context.
Compatibility and installation
Requires Node.js 18.0.0 or newer, based on the oldest consuming application's
runtime. CommonJS JavaScript and TypeScript declarations are included. CommonJS
require() and Node ESM named imports both use the same compiled entry point.
Express is supplied by the consuming application; no Express runtime is bundled.
The library does not use global fetch or global request state.
npm install internal-dw-api-authimport { generateApiKey } from 'internal-dw-api-auth';
// CommonJS: const { generateApiKey } = require('internal-dw-api-auth');Generate and store credentials
const generated = generateApiKey({ prefix: 'example' });
const { keyId, secretHash } = generated;
// Persist keyId and secretHash using your application's storage layer.
// Deliver generated.apiKey once through your secure provisioning workflow.The format is keyId.secret. The key ID contains the prefix and 16 random bytes
encoded as hex; it is a public lookup identifier. Prefixes must contain 1–64 ASCII
letters, digits, underscores or hyphens. Enforce uniqueness in your storage layer.
The secret contains 32 cryptographically secure random bytes encoded as hex.
secretHash is SHA-256 of only the secret. Store the hash on the receiving API;
the calling application must securely retain the raw key to authenticate.
Never log the generated object, raw key, secret, hash or authorization headers.
parseApiKey(apiKey) splits at the first dot and rejects missing or empty parts.
hashApiKeySecret(secret) returns a hex SHA-256 digest.
verifyApiKeySecret(secret, storedSecretHash) uses a constant-time buffer comparison
and returns false for malformed hashes.
Authentication and IP filtering
The following configures an existing application and router; the package does not
create or start a server. keyRepository is an application-owned adapter, and
allowedNetworks is parsed by the application from configuration.
import {
createApiKeyAuthenticationMiddleware,
createIpWhitelistMiddleware,
type FindApiKey,
} from 'internal-dw-api-auth';
const findApiKey: FindApiKey = async keyId => {
const row = await keyRepository.findByKeyId(keyId);
if (!row) return null;
return {
keyId: row.keyId,
secretHash: row.secretHash,
active: row.active,
expiresAt: row.expiresAt, // Date or null; convert serialized dates in your adapter.
revokedAt: row.revokedAt, // Date or null.
client: { id: row.client.id, code: row.client.code, active: row.client.active },
};
};
internalRouter.use(createIpWhitelistMiddleware({ allowedNetworks }));
internalRouter.use(createApiKeyAuthenticationMiddleware({ findApiKey }));Requests send Authorization: ApiKey keyId.secret. Missing/malformed credentials,
unknown keys, wrong secrets, inactive keys or clients, revoked keys and expired
keys all receive 401 { "error": "Unauthorized" }. Expiration at the current
instant is rejected. Authentication attaches req.internalRequestContext.client.
A failed database lookup forwards a sanitized error to Express for the application's
500 handler; it does not expose the underlying exception. No last-used timestamp
is updated. Redact credentials in application logging and error middleware too.
An empty whitelist denies every request. Exact IPv4, IPv6 and CIDR entries are
supported using proxy-addr; invalid configuration throws a generic error when
creating middleware. A disallowed source receives 403 { "error": "Forbidden" }.
The middleware uses Express req.ip, which respects the application's trusted
proxy configuration. It does not read forwarding headers independently.
Do not blindly use app.set('trust proxy', true). Configure only the actual
trusted proxy addresses/hops for your topology, ensure proxies replace untrusted
forwarding headers, and restrict direct access appropriately. Otherwise an attacker
may influence the source IP used by the whitelist.
To customize responses, provide onUnauthorized(error, req, res) or
onForbidden(error, req, res). The status is set to 401 or 403 before the callback;
the callback must finish the response. Errors contain only fixed generic messages.
Explicit department context
import {
extractDepartmentId,
requireDepartmentId,
} from 'internal-dw-api-auth';
// Install after authentication on the existing internal router.
internalRouter.use((req, res, next) => {
try {
const departmentId = extractDepartmentId(req.headers, {
validate: value => /^[A-Za-z0-9_-]+$/.test(value),
});
if (req.internalRequestContext) {
req.internalRequestContext.departmentId = departmentId;
}
next();
} catch {
res.status(400).json({ error: 'Invalid department context' });
}
});
// Copy these fields into your existing GraphQL server configuration:
const context = async ({ req }) => ({
client: req.internalRequestContext?.client,
departmentId: req.internalRequestContext?.departmentId,
});
// Inside a department-scoped resolver, pass the value explicitly to services:
const departmentId = requireDepartmentId(graphqlContext.departmentId);
await departmentService.list({ departmentId });Header names are case-insensitive. String and string-array values are trimmed;
identical repeated values are accepted, conflicting values are rejected. Missing
values return undefined. Empty values, invalid types, commas (possibly combined
headers), NUL and line breaks are rejected. Custom validators return a boolean;
validator exceptions become a generic DepartmentContextError.
requireDepartmentId rejects a missing/invalid value and returns the trimmed ID.
Apply your custom validation during extraction. The library neither queries
departments nor grants department access, and it needs no global context library.
Authentication initializes fresh context, so attach department context after it.
Outbound headers
import { createInternalRequestHeaders } from 'internal-dw-api-auth';
const headers = createInternalRequestHeaders({
apiKey: configuredApiKey,
departmentId, // Explicit for each scoped request; omit for unscoped operations.
});
// Pass headers to your application's existing HTTP transport.This returns Content-Type: application/json, Authorization: ApiKey ... and,
when provided, X-Department-Id. Never log the returned headers. This first version
provides the header helper only; callers own HTTP transport, timeouts, GraphQL
response parsing and error handling. Avoid automatically retrying mutations.
Placeholder configuration, read by the application only:
INTERNAL_API_KEY=example_key_id.example_secret
INTERNAL_API_ALLOWED_NETWORKS=127.0.0.1,::1Split the network configuration on commas and trim entries before passing it to the middleware. Decide explicitly whether missing configuration denies all access or prevents your application from starting.
Rotation and future authentication
Generate a new key, store its ID and hash, securely provision the raw key to the caller, then revoke the previous key after the rollout. Applications choose any overlap period and expiration policy. Multiple keys may map to one client.
AuthenticatedClient contains clientId, clientCode and authenticationType.
The latter is 'api-key' | 'access-token' for future bearer-token compatibility;
this version produces only 'api-key' and rejects bearer authentication.
Public API
Functions: generateApiKey, parseApiKey, hashApiKeySecret,
verifyApiKeySecret, createApiKeyAuthenticationMiddleware,
createIpWhitelistMiddleware, extractDepartmentId, requireDepartmentId,
createInternalRequestHeaders.
Errors: ApiKeyFormatError, InternalAuthenticationError,
DepartmentContextError, IpNotAllowedError.
Types: GeneratedApiKey, ApiKeyRecord, FindApiKey, AuthenticatedClient,
InternalRequestContext, ApiKeyAuthenticationOptions, IpWhitelistOptions,
DepartmentValidationOptions, InternalRequestHeadersOptions.
Only the package root is a supported entry point. Express request augmentation is included automatically when importing the package.
Development and publishing
npm ci
npm run build
npm run typecheck
npm run lint
npm pack --dry-runBuild output is CommonJS .js and .d.ts declarations without source maps.
The publish allowlist includes dist, README.md and LICENSE; npm also includes
package.json. Public access is configured. An authorized scope owner must publish
the package using their own npm credentials. No automated tests are included.
MIT License. Copyright (c) 2026 Anja Tomovska.
