client-certificate-auth 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,26 @@ Comprehensive toolkit for client SSL certificate authentication (mTLS) in Node.j
15
15
 
16
16
  **Fanatically Tested** - 100% line/branch/function/statement coverage, plus mutation testing and E2E tests against real nginx/Envoy/Traefik containers. ~6,207 lines of test code for ~937 lines of source (measured by [cloc](https://github.com/AlDanial/cloc)).
17
17
 
18
+ ## What Is This?
19
+
20
+ This library authenticates HTTP clients by their TLS client certificates, a scheme usually called mutual TLS (mTLS). Instead of presenting a password, API key, or bearer token, the client presents an X.509 certificate during the TLS handshake and proves possession of its private key; the server checks that the certificate was issued by a CA it trusts. The certificate is the credential.
21
+
22
+ Typical uses:
23
+
24
+ - **Service-to-service APIs**: internal microservices or partner integrations where each caller holds its own certificate rather than a shared secret.
25
+ - **Machine and device authentication**: CI runners, IoT devices, daemons. Certificates are issued per machine and revoked per machine.
26
+ - **Restricting sensitive endpoints**: admin interfaces, metrics, internal tooling that only known clients should reach.
27
+ - **Certificate-based user login**: enterprise PKI and smart-card environments, where certificates map to user accounts.
28
+
29
+ The TLS handshake proves who the client is; your application decides whether that client is allowed in. This library covers the part in between: it extracts the verified certificate from the request wherever your TLS terminates, parses it into a standard [`tls.PeerCertificate`](https://nodejs.org/api/tls.html#certificate-object) object, and passes it to your authorization callback. Supported certificate sources:
30
+
31
+ - **TLS terminated directly in Node.js**: the certificate is read from the TLS socket.
32
+ - **A TLS-terminating reverse proxy or load balancer** that forwards the certificate in an HTTP header: AWS ALB, Envoy/Istio, Cloudflare, Traefik, Azure App Service, nginx, HAProxy, and any proxy that implements RFC 9440 (e.g. Google Cloud Load Balancer).
33
+ - **AWS Lambda** behind API Gateway mTLS, via `client-certificate-auth/lambda`.
34
+ - **Web-standard `Request` runtimes** (Hono, Next.js, SvelteKit, Cloudflare Workers, Bun, Deno), via `client-certificate-auth/fetch`.
35
+
36
+ Express and Connect get drop-in middleware. Other frameworks use the same extraction logic through `extractClientCertificate()`. Pre-built authorization helpers cover the common policies (allowlist by CN, fingerprint, issuer, OU, SAN, and more).
37
+
18
38
  ## Installation
19
39
 
20
40
  ```bash
@@ -23,14 +43,6 @@ npm install client-certificate-auth
23
43
 
24
44
  **Requirements:** Node.js >= 20
25
45
 
26
- ## Synopsis
27
-
28
- This library provides everything you need to implement mutual TLS (mTLS) authentication in Node.js. It extracts client certificates from direct TLS connections (`req.socket`) or from HTTP headers forwarded by reverse proxies (AWS ALB, Envoy, Cloudflare, Traefik, nginx, HAProxy).
29
-
30
- The certificate is parsed into a standard `tls.PeerCertificate` object and passed to your callback for authorization logic.
31
-
32
- Compatible with Express, Connect, or any Node.js HTTP server framework by using the framework-agnostic `extractClientCertificate` function.
33
-
34
46
  ## Usage
35
47
 
36
48
  ### Basic Setup
@@ -32,6 +32,19 @@ function isThenable(value) {
32
32
  && typeof value.then === 'function';
33
33
  }
34
34
 
35
+ /**
36
+ * Wrap primitive values thrown or rejected by the validation callback in an
37
+ * Error so `.message` and `.status` access is safe; objects pass through.
38
+ * @param {unknown} err
39
+ * @returns {any}
40
+ */
41
+ function normalizeCallbackError(err) {
42
+ if (err !== null && (typeof err === 'object' || typeof err === 'function')) {
43
+ return err;
44
+ }
45
+ return new Error(err === null || err === undefined ? '' : String(err));
46
+ }
47
+
35
48
  /**
36
49
  * Options not supported by the sync CJS wrapper.
37
50
  * These require the ESM module's async header parsing.
@@ -145,7 +158,8 @@ function clientCertificateAuth(callback, options = {}) {
145
158
  try {
146
159
  const result = callback(cert, req);
147
160
  if (isThenable(result)) {
148
- Promise.resolve(result).then(doneAuthorizing).catch((err) => {
161
+ Promise.resolve(result).then(doneAuthorizing).catch((rejection) => {
162
+ const err = normalizeCallbackError(rejection);
149
163
  safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
150
164
  if (err.status === undefined) {
151
165
  err.status = 401;
@@ -155,7 +169,8 @@ function clientCertificateAuth(callback, options = {}) {
155
169
  } else {
156
170
  doneAuthorizing(result);
157
171
  }
158
- } catch (err) {
172
+ } catch (thrown) {
173
+ const err = normalizeCallbackError(thrown);
159
174
  safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
160
175
  if (err.status === undefined) {
161
176
  err.status = 401;
@@ -20,6 +20,19 @@ function isThenable(value) {
20
20
  && typeof /** @type {{then?: unknown}} */ (value).then === 'function';
21
21
  }
22
22
 
23
+ /**
24
+ * Wrap primitive values thrown or rejected by the validation callback in an
25
+ * Error so `.message` and `.status` access is safe; objects pass through.
26
+ * @param {unknown} err
27
+ * @returns {any}
28
+ */
29
+ function normalizeCallbackError(err) {
30
+ if (err !== null && (typeof err === 'object' || typeof err === 'function')) {
31
+ return err;
32
+ }
33
+ return new Error(err === null || err === undefined ? '' : String(err));
34
+ }
35
+
23
36
  /**
24
37
  * @typedef {import('http').IncomingMessage & { secure?: boolean; socket: import('net').Socket & { authorized?: boolean; authorizationError?: Error | string; getPeerCertificate?: (detailed?: boolean) => import('tls').PeerCertificate | import('tls').DetailedPeerCertificate }; clientCertificate?: import('tls').PeerCertificate }} ClientCertRequest
25
38
  * @typedef {import('http').ServerResponse & { redirect: (statusOrUrl: number | string, url?: string) => void }} ClientCertResponse
@@ -195,7 +208,8 @@ export default function clientCertificateAuth(callback, options = {}) {
195
208
  try {
196
209
  const callbackResult = callback(cert, req);
197
210
  if (isThenable(callbackResult)) {
198
- Promise.resolve(callbackResult).then(doneAuthorizing).catch((err) => {
211
+ Promise.resolve(callbackResult).then(doneAuthorizing).catch((rejection) => {
212
+ const err = normalizeCallbackError(rejection);
199
213
  safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
200
214
  if (err.status === undefined) {
201
215
  err.status = 401;
@@ -205,7 +219,8 @@ export default function clientCertificateAuth(callback, options = {}) {
205
219
  } else {
206
220
  doneAuthorizing(callbackResult);
207
221
  }
208
- } catch (err) {
222
+ } catch (thrown) {
223
+ const err = normalizeCallbackError(thrown);
209
224
  safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
210
225
  if (err.status === undefined) {
211
226
  err.status = 401;
package/lib/fetch.js CHANGED
@@ -53,7 +53,6 @@ export function extractClientCertificateFromRequest(request, options = {}) {
53
53
  headers[name.toLowerCase()] = value;
54
54
  }
55
55
 
56
- // Stryker disable next-line ObjectLiteral: destructure-discard strips fallbackToSocket
57
56
  const { fallbackToSocket: _ignored, ...rest } = options;
58
57
 
59
58
  return extractClientCertificate({ headers }, rest);
package/lib/parsers.js CHANGED
@@ -161,7 +161,7 @@ function chainFromMultiBlockPem(pem) {
161
161
  try {
162
162
  return pemToCertificate(block);
163
163
  } catch {
164
- // Stryker disable next-line BlockStatement: empty catch returns undefined which .filter(Boolean) drops alongside null same chain output
164
+ // Equivalent mutant (undefined also filtered by .filter(Boolean)) catch-body BlockStatement unsuppressible via Stryker comments
165
165
  return null;
166
166
  }
167
167
  }).filter(Boolean);
@@ -357,7 +357,7 @@ export function parseRfc9440(headerValue) {
357
357
  let base64 = headerValue;
358
358
  // Stryker disable next-line BlockStatement,ConditionalExpression,StringLiteral,LogicalOperator,MethodExpression: colon-stripping is defensive; all valid RFC 9440 inputs use :base64: format, so base64 decoder ignores colons either way
359
359
  if (base64.startsWith(':') && base64.endsWith(':')) {
360
- // Stryker disable next-line BlockStatement,MethodExpression: removing or no-oping the assignment is equivalent — base64 decoder ignores colons
360
+ // Stryker disable next-line MethodExpression: no-oping the assignment is equivalent — base64 decoder ignores colons
361
361
  base64 = base64.slice(1, -1);
362
362
  }
363
363
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "client-certificate-auth",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Express/Connect middleware for mTLS client certificate authentication with reverse proxy support (AWS ALB, Envoy, Cloudflare, Traefik)",
5
5
  "homepage": "https://github.com/tgies/client-certificate-auth",
6
6
  "bugs": {
@@ -99,8 +99,8 @@
99
99
  },
100
100
  "devDependencies": {
101
101
  "@arethetypeswrong/cli": "^0.18.2",
102
- "@commitlint/cli": "^20.4.3",
103
- "@commitlint/config-conventional": "^20.5.0",
102
+ "@commitlint/cli": "^21.0.0",
103
+ "@commitlint/config-conventional": "^21.0.0",
104
104
  "@eslint/js": "^10.0.1",
105
105
  "@stryker-mutator/core": "^9.6.0",
106
106
  "@stryker-mutator/jest-runner": "^9.6.1",
@@ -111,7 +111,7 @@
111
111
  "globals": "^17.5.0",
112
112
  "husky": "^9.1.7",
113
113
  "jest": "^30.3.0",
114
- "lint-staged": "^16.3.2",
114
+ "lint-staged": "^17.0.4",
115
115
  "selfsigned": "^5.5.0",
116
116
  "typedoc": "^0.28.19",
117
117
  "typedoc-plugin-markdown": "^4.11.0",
@@ -172,6 +172,7 @@
172
172
  "lib/**/*.d.cts"
173
173
  ],
174
174
  "overrides": {
175
- "fflate": "0.8.2"
175
+ "fflate": "0.8.2",
176
+ "qs": "^6.15.2"
176
177
  }
177
178
  }