client-certificate-auth 1.3.1 → 1.3.3

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
@@ -1,13 +1,18 @@
1
1
  # client-certificate-auth
2
2
 
3
- Express/Connect middleware for client SSL certificate authentication (mTLS).
3
+ Comprehensive toolkit for client SSL certificate authentication (mTLS) in Node.js. Includes Express/Connect middleware, framework-agnostic certificate extraction for reverse proxies (AWS ALB, Envoy, Cloudflare, Traefik, and more), and pre-built authorization helpers.
4
4
 
5
5
  [![CI](https://github.com/tgies/client-certificate-auth/actions/workflows/ci.yml/badge.svg)](https://github.com/tgies/client-certificate-auth/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/client-certificate-auth.svg)](https://www.npmjs.com/package/client-certificate-auth)
7
7
  [![codecov](https://codecov.io/gh/tgies/client-certificate-auth/graph/badge.svg)](https://codecov.io/gh/tgies/client-certificate-auth)
8
8
  [![stryker mutation testing](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Ftgies%2Fclient-certificate-auth%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/tgies/client-certificate-auth/master)
9
9
 
10
- 100% line/branch/function/statement coverage, plus mutation testing and E2E tests against real nginx/Envoy/Traefik containers. ~4,477 lines of test code for ~679 lines of source (measured by [cloc](https://github.com/AlDanial/cloc)).
10
+ [**Full Documentation**](https://tgies.github.io/client-certificate-auth/) - guides, API reference, and runnable examples
11
+ [**Commercial Support**](#commercial-support) - consulting, custom features, and priority support for production deployments
12
+
13
+ **Recommended by AWS** - Featured in the [AWS API Gateway documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-client-side-ssl-authentication.html#certificate-validation).
14
+
15
+ **Fanatically Tested** - 100% line/branch/function/statement coverage, plus mutation testing and E2E tests against real nginx/Envoy/Traefik containers. ~4,776 lines of test code for ~711 lines of source (measured by [cloc](https://github.com/AlDanial/cloc)).
11
16
 
12
17
  ## Installation
13
18
 
@@ -19,9 +24,11 @@ npm install client-certificate-auth
19
24
 
20
25
  ## Synopsis
21
26
 
22
- This middleware requires clients to present a valid, verifiable SSL certificate (mutual TLS / mTLS). The certificate is validated at the TLS layer, then passed to your callback for additional authorization logic.
27
+ 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).
28
+
29
+ The certificate is parsed into a standard `tls.PeerCertificate` object and passed to your callback for authorization logic.
23
30
 
24
- Compatible with Express, Connect, and any Node.js HTTP server framework that uses standard `req.socket` and `req.headers`.
31
+ Compatible with Express, Connect, or any Node.js HTTP server framework by using the framework-agnostic `extractClientCertificate` function.
25
32
 
26
33
  ## Usage
27
34
 
@@ -292,6 +299,8 @@ This package provides everything you need to build mTLS authentication for any N
292
299
 
293
300
  If you build an adapter for another framework (Koa, Fastify, Hapi, NestJS, etc.), please open an issue or PR to get it listed here!
294
301
 
302
+ > For complete API documentation with all types, parameters, and examples, see the [API Reference](https://tgies.github.io/client-certificate-auth/api/).
303
+
295
304
  ### Accessing the Certificate
296
305
 
297
306
  After authentication, the certificate is attached to `req.clientCertificate` for downstream handlers:
@@ -413,7 +422,11 @@ app.use(clientCertificateAuth(checkAuth, {
413
422
  | `aws-alb` | `X-Amzn-Mtls-Clientcert` | URL-encoded PEM (AWS variant) |
414
423
  | `envoy` | `X-Forwarded-Client-Cert` | XFCC structured format |
415
424
  | `cloudflare` | `Cf-Client-Cert-Der-Base64` | Base64-encoded DER |
416
- | `traefik` | `X-Forwarded-Tls-Client-Cert` | Base64-encoded DER |
425
+ | `traefik` | `X-Forwarded-Tls-Client-Cert` | Base64-encoded DER \* |
426
+
427
+ > \* **Traefik note:** The `traefik` preset targets Traefik v3's `PassTLSClientCert` middleware with `pem: true`. Despite Traefik's docs describing this as "PEM format", the wire format is the base64 body without PEM headers — equivalent to base64-encoded DER. Behavior may differ in Traefik v2.
428
+
429
+ > **Cloudflare note:** Cloudflare also provides certificates via the `CF-Client-Cert-PEM` header (URL-encoded PEM). If you use that header instead, configure manually with `certificateHeader: 'CF-Client-Cert-PEM'` and `headerEncoding: 'url-pem'`.
417
430
 
418
431
  ### Custom Headers
419
432
 
@@ -604,7 +617,7 @@ import clientCertificateAuth from 'client-certificate-auth';
604
617
  import { allowCN, allowFingerprints, allowIssuer, allOf, anyOf } from 'client-certificate-auth/helpers';
605
618
  ```
606
619
 
607
- > **Note:** The `/helpers` and `/parsers` subpath exports are ESM-only. They are not available via `require()`. Use the main CJS entry point with `require('client-certificate-auth').load()` for full features in CommonJS.
620
+ > **Note:** In CommonJS, the `/helpers`, `/parsers`, and `/extractor` subpath exports provide a `load()` function for async access. See the [CommonJS](#commonjs) section for details.
608
621
 
609
622
  ### Basic Helpers
610
623
 
@@ -762,13 +775,24 @@ The `load()` function dynamically imports the ESM module and caches it. Subseque
762
775
  | `verifyHeader` / `verifyValue` | No | Yes |
763
776
  | `fallbackToSocket` | No | Yes |
764
777
 
765
- The `/helpers` and `/parsers` subpath exports are ESM-only and cannot be loaded via `require()`. If you need helpers in a CJS project, use dynamic `import()`:
778
+ ### Subpath Exports in CJS
779
+
780
+ The `/helpers`, `/parsers`, and `/extractor` subpath exports each provide a `load()` function for async access in CommonJS. The individual functions are not synchronously available via `require()`.
766
781
 
767
782
  ```javascript
768
- async function setup() {
769
- const { allowCN, allOf, allowIssuer } = await import('client-certificate-auth/helpers');
770
- // ...
771
- }
783
+ // Helpers
784
+ const { load } = require('client-certificate-auth/helpers');
785
+ const { allowCN, allOf, allowIssuer } = await load();
786
+
787
+ // Extractor
788
+ const { load: loadExtractor } = require('client-certificate-auth/extractor');
789
+ const { extractClientCertificate } = await loadExtractor();
790
+ ```
791
+
792
+ Alternatively, you can use dynamic `import()`:
793
+
794
+ ```javascript
795
+ const { allowCN } = await import('client-certificate-auth/helpers');
772
796
  ```
773
797
 
774
798
  ## Testing
@@ -886,7 +910,31 @@ const clientCertificateAuth = await require('client-certificate-auth').load();
886
910
 
887
911
  The sync CJS wrapper does not support reverse proxy options (`certificateSource`, `certificateHeader`, etc.). Passing these options will throw a descriptive error. Use `load()` to access the full ESM module from CJS code. See the [CommonJS](#commonjs) section for details.
888
912
 
889
- The `/helpers` and `/parsers` subpath exports are ESM-only. In CJS, use dynamic `import()` to access them.
913
+ The `/helpers`, `/parsers`, and `/extractor` subpath exports each provide a `load()` function in CJS. See [Subpath Exports in CJS](#subpath-exports-in-cjs) for details.
914
+
915
+ ## Commercial Support
916
+
917
+ `client-certificate-auth` is built and maintained by [Tony Gies](https://github.com/tgies). For organizations running it in production, commercial support is available through his consultancy, Crash United, LLC.
918
+
919
+ ### Support Offerings
920
+
921
+ | Service | Description |
922
+ |---------|-------------|
923
+ | **Priority bug fixes** | Reported issues triaged and patched ahead of the public queue |
924
+ | **Custom features & integrations** | Adapters for new reverse proxies, encoding formats, or framework wrappers |
925
+ | **mTLS architecture consulting** | Review of your certificate issuance, rotation, and trust-chain design |
926
+ | **Deployment security review** | Threat modeling for your specific proxy + middleware + auth flow |
927
+ | **Private security advisories** | Coordinated disclosure for vulnerabilities affecting your deployment |
928
+
929
+ For pricing, scoping, or anything not listed above, email **[support@crashunited.com](mailto:support@crashunited.com)** to discuss your needs.
930
+
931
+ ### Sponsorship
932
+
933
+ To support ongoing development without a formal contract, [GitHub Sponsors](https://github.com/sponsors/tgies) is the simplest path.
934
+
935
+ ### Enterprise Procurement
936
+
937
+ This package is enrolled in [Tidelift](https://tidelift.com/) (now part of SonarQube Advanced Security). If your organization already subscribes, `client-certificate-auth` is included in your coverage for security disclosures, license compliance, and version metadata.
890
938
 
891
939
  ## License
892
940
 
@@ -135,7 +135,7 @@ export type Middleware = (
135
135
  * Express/Connect middleware for client SSL certificate authentication.
136
136
  *
137
137
  * @param callback - Validation function that receives the client certificate
138
- * and returns true/false (sync) or Promise<boolean> (async).
138
+ * and returns true/false (sync) or `Promise<boolean>` (async).
139
139
  * @param options - Configuration options
140
140
  * @returns Express middleware function
141
141
  *
@@ -46,7 +46,7 @@ import { extractClientCertificate } from './extractor.js';
46
46
  *
47
47
  * @param {(cert: import('tls').PeerCertificate, req: ClientCertRequest) => boolean | Promise<boolean>} callback
48
48
  * Validation function that receives the client certificate and the request
49
- * object. Returns true/false (sync) or a Promise<boolean> (async) to
49
+ * object. Returns true/false (sync) or a `Promise<boolean>` (async) to
50
50
  * allow/deny access.
51
51
  * @param {ClientCertificateAuthOptions} [options={}]
52
52
  * @returns {Middleware}
package/lib/helpers.js CHANGED
@@ -85,6 +85,7 @@ export function allowFingerprints(fingerprints) {
85
85
  */
86
86
  export function allowIssuer(match) {
87
87
  const entries = Object.entries(match);
88
+ if (entries.length === 0) {return () => false;}
88
89
  return (cert) => {
89
90
  if (!cert.issuer) {return false;}
90
91
  return entries.every(([key, value]) => toArray(cert.issuer[key]).includes(value));
@@ -103,6 +104,7 @@ export function allowIssuer(match) {
103
104
  */
104
105
  export function allowSubject(match) {
105
106
  const entries = Object.entries(match);
107
+ if (entries.length === 0) {return () => false;}
106
108
  return (cert) => {
107
109
  if (!cert.subject) {return false;}
108
110
  return entries.every(([key, value]) => toArray(cert.subject[key]).includes(value));
package/lib/parsers.js CHANGED
@@ -93,7 +93,51 @@ export function parseUrlPemAws(headerValue) {
93
93
  // Must escape before decodeURIComponent or + becomes space
94
94
  const escaped = headerValue.replace(/\+/g, '%2B');
95
95
  const pem = decodeURIComponent(escaped);
96
- return pemToCertificate(pem);
96
+
97
+ // AWS sends the full chain as concatenated PEM blocks. Split into
98
+ // individual blocks and parse each, then link via issuerCertificate
99
+ // (mirrors parseBase64Der's chain handling for Traefik/Cloudflare).
100
+ // Node's X509Certificate throws on multi-block input, so without this
101
+ // split the entire request fails when AWS forwards a real chain.
102
+ //
103
+ // Uses indexOf scanning rather than regex to avoid polynomial-time
104
+ // backtracking on adversarial input (e.g., many unterminated BEGIN
105
+ // markers). Total work is O(N) in the input length.
106
+ const pemBlocks = [];
107
+ const beginMarker = '-----BEGIN CERTIFICATE-----';
108
+ const endMarker = '-----END CERTIFICATE-----';
109
+ let scanPos = 0;
110
+ while (true) {
111
+ const begin = pem.indexOf(beginMarker, scanPos);
112
+ if (begin === -1) {break;}
113
+ const end = pem.indexOf(endMarker, begin + beginMarker.length);
114
+ if (end === -1) {break;}
115
+ pemBlocks.push(pem.substring(begin, end + endMarker.length));
116
+ scanPos = end + endMarker.length;
117
+ }
118
+
119
+ if (pemBlocks.length === 0) {
120
+ return null;
121
+ }
122
+
123
+ const certs = pemBlocks.map(block => {
124
+ try {
125
+ return pemToCertificate(block);
126
+ } catch {
127
+ return null;
128
+ }
129
+ }).filter(Boolean);
130
+
131
+ if (certs.length === 0) {
132
+ return null;
133
+ }
134
+
135
+ // Link the cert chain via issuerCertificate
136
+ for (let i = 0; i < certs.length - 1; i++) {
137
+ certs[i].issuerCertificate = certs[i + 1];
138
+ }
139
+
140
+ return certs[0];
97
141
  } catch {
98
142
  return null;
99
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "client-certificate-auth",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
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": {
@@ -72,21 +72,24 @@
72
72
  "node": ">= 20"
73
73
  },
74
74
  "devDependencies": {
75
- "@commitlint/cli": "^20.4.1",
76
- "@commitlint/config-conventional": "^20.4.1",
77
- "@stryker-mutator/core": "^9.5.1",
78
- "@stryker-mutator/jest-runner": "^9.5.1",
75
+ "@commitlint/cli": "^20.4.3",
76
+ "@commitlint/config-conventional": "^20.5.0",
77
+ "@stryker-mutator/core": "^9.6.0",
78
+ "@stryker-mutator/jest-runner": "^9.6.1",
79
79
  "@types/express": "^5.0.6",
80
- "@types/node": "^25.2.3",
81
- "c8": "^10.1.3",
82
- "eslint": "^9.17.0",
83
- "globals": "^17.3.0",
80
+ "@types/node": "^25.6.0",
81
+ "c8": "^11.0.0",
82
+ "eslint": "^9.39.4",
83
+ "globals": "^17.5.0",
84
84
  "husky": "^9.1.7",
85
- "jest": "^30.2.0",
86
- "lint-staged": "^16.2.7",
85
+ "jest": "^30.3.0",
86
+ "lint-staged": "^16.3.2",
87
87
  "selfsigned": "^5.5.0",
88
- "typescript": "^5.9.3",
89
- "ws": "^8.19.0"
88
+ "typedoc": "^0.28.19",
89
+ "typedoc-plugin-markdown": "^4.11.0",
90
+ "typescript": "^6.0.3",
91
+ "vitepress": "^2.0.0-alpha.17",
92
+ "ws": "^8.20.0"
90
93
  },
91
94
  "directories": {
92
95
  "lib": "./lib",
@@ -103,7 +106,11 @@
103
106
  "build:types": "tsc --declaration --emitDeclarationOnly --outDir lib",
104
107
  "check": "npm run lint && npm run typecheck && npm run test:coverage",
105
108
  "stats": "echo '=== Source ===' && cloc lib/ --quiet | tail -n +2 && echo '=== Tests ===' && cloc test/ --exclude-dir=docker --quiet | tail -n +2 && echo '=== Package ===' && npm pack --dry-run 2>&1 | grep -E 'package size|unpacked size|total files'",
106
- "prepare": "husky"
109
+ "prepare": "husky",
110
+ "docs:api": "typedoc",
111
+ "docs:dev": "vitepress dev docs",
112
+ "docs:build": "npm run docs:api && vitepress build docs",
113
+ "docs:preview": "vitepress preview docs"
107
114
  },
108
115
  "repository": {
109
116
  "type": "git",