client-certificate-auth 1.3.0 → 1.3.2

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
@@ -7,7 +7,7 @@ Express/Connect middleware for client SSL certificate authentication (mTLS).
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
+ 100% line/branch/function/statement coverage, plus mutation testing and E2E tests against real nginx/Envoy/Traefik containers. ~4,742 lines of test code for ~709 lines of source (measured by [cloc](https://github.com/AlDanial/cloc)).
11
11
 
12
12
  ## Installation
13
13
 
@@ -413,7 +413,11 @@ app.use(clientCertificateAuth(checkAuth, {
413
413
  | `aws-alb` | `X-Amzn-Mtls-Clientcert` | URL-encoded PEM (AWS variant) |
414
414
  | `envoy` | `X-Forwarded-Client-Cert` | XFCC structured format |
415
415
  | `cloudflare` | `Cf-Client-Cert-Der-Base64` | Base64-encoded DER |
416
- | `traefik` | `X-Forwarded-Tls-Client-Cert` | Base64-encoded DER |
416
+ | `traefik` | `X-Forwarded-Tls-Client-Cert` | Base64-encoded DER \* |
417
+
418
+ > \* **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.
419
+
420
+ > **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
421
 
418
422
  ### Custom Headers
419
423
 
@@ -604,7 +608,7 @@ import clientCertificateAuth from 'client-certificate-auth';
604
608
  import { allowCN, allowFingerprints, allowIssuer, allOf, anyOf } from 'client-certificate-auth/helpers';
605
609
  ```
606
610
 
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.
611
+ > **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
612
 
609
613
  ### Basic Helpers
610
614
 
@@ -762,13 +766,24 @@ The `load()` function dynamically imports the ESM module and caches it. Subseque
762
766
  | `verifyHeader` / `verifyValue` | No | Yes |
763
767
  | `fallbackToSocket` | No | Yes |
764
768
 
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()`:
769
+ ### Subpath Exports in CJS
770
+
771
+ 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
772
 
767
773
  ```javascript
768
- async function setup() {
769
- const { allowCN, allOf, allowIssuer } = await import('client-certificate-auth/helpers');
770
- // ...
771
- }
774
+ // Helpers
775
+ const { load } = require('client-certificate-auth/helpers');
776
+ const { allowCN, allOf, allowIssuer } = await load();
777
+
778
+ // Extractor
779
+ const { load: loadExtractor } = require('client-certificate-auth/extractor');
780
+ const { extractClientCertificate } = await loadExtractor();
781
+ ```
782
+
783
+ Alternatively, you can use dynamic `import()`:
784
+
785
+ ```javascript
786
+ const { allowCN } = await import('client-certificate-auth/helpers');
772
787
  ```
773
788
 
774
789
  ## Testing
@@ -886,7 +901,7 @@ const clientCertificateAuth = await require('client-certificate-auth').load();
886
901
 
887
902
  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
903
 
889
- The `/helpers` and `/parsers` subpath exports are ESM-only. In CJS, use dynamic `import()` to access them.
904
+ 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.
890
905
 
891
906
  ## License
892
907
 
package/lib/helpers.js CHANGED
@@ -9,6 +9,17 @@
9
9
  * @typedef {(cert: PeerCertificate, req?: import('http').IncomingMessage) => boolean | Promise<boolean>} ValidationCallback
10
10
  */
11
11
 
12
+ /**
13
+ * Normalize a certificate DN field value to an array.
14
+ * Node.js returns string for single-valued and string[] for multi-valued DN attributes.
15
+ * @param {string | string[] | undefined} value
16
+ * @returns {string[]}
17
+ */
18
+ function toArray(value) {
19
+ if (value === undefined || value === null) {return [];}
20
+ return Array.isArray(value) ? value : [value];
21
+ }
22
+
12
23
  /**
13
24
  * Create a validation callback that allows certificates with matching Common Names.
14
25
  *
@@ -20,7 +31,7 @@
20
31
  */
21
32
  export function allowCN(names) {
22
33
  const allowed = new Set(names);
23
- return (cert) => allowed.has(cert.subject?.CN);
34
+ return (cert) => toArray(cert.subject?.CN).some((cn) => allowed.has(cn));
24
35
  }
25
36
 
26
37
  /**
@@ -74,9 +85,10 @@ export function allowFingerprints(fingerprints) {
74
85
  */
75
86
  export function allowIssuer(match) {
76
87
  const entries = Object.entries(match);
88
+ if (entries.length === 0) {return () => false;}
77
89
  return (cert) => {
78
90
  if (!cert.issuer) {return false;}
79
- return entries.every(([key, value]) => cert.issuer[key] === value);
91
+ return entries.every(([key, value]) => toArray(cert.issuer[key]).includes(value));
80
92
  };
81
93
  }
82
94
 
@@ -92,9 +104,10 @@ export function allowIssuer(match) {
92
104
  */
93
105
  export function allowSubject(match) {
94
106
  const entries = Object.entries(match);
107
+ if (entries.length === 0) {return () => false;}
95
108
  return (cert) => {
96
109
  if (!cert.subject) {return false;}
97
- return entries.every(([key, value]) => cert.subject[key] === value);
110
+ return entries.every(([key, value]) => toArray(cert.subject[key]).includes(value));
98
111
  };
99
112
  }
100
113
 
@@ -109,7 +122,7 @@ export function allowSubject(match) {
109
122
  */
110
123
  export function allowOU(ous) {
111
124
  const allowed = new Set(ous);
112
- return (cert) => allowed.has(cert.subject?.OU);
125
+ return (cert) => toArray(cert.subject?.OU).some((ou) => allowed.has(ou));
113
126
  }
114
127
 
115
128
  /**
@@ -123,7 +136,7 @@ export function allowOU(ous) {
123
136
  */
124
137
  export function allowOrganization(orgs) {
125
138
  const allowed = new Set(orgs);
126
- return (cert) => allowed.has(cert.subject?.O);
139
+ return (cert) => toArray(cert.subject?.O).some((o) => allowed.has(o));
127
140
  }
128
141
 
129
142
  /**
@@ -196,9 +209,9 @@ export function allowEmail(emails) {
196
209
  const allowed = new Set(emails.map((e) => e.toLowerCase()));
197
210
 
198
211
  return (cert) => {
199
- // Check subject.emailAddress
212
+ // Check subject.emailAddress (may be string or string[] for multi-valued)
200
213
  if (cert.subject?.emailAddress) {
201
- if (allowed.has(cert.subject.emailAddress.toLowerCase())) {
214
+ if (toArray(cert.subject.emailAddress).some((e) => allowed.has(e.toLowerCase()))) {
202
215
  return true;
203
216
  }
204
217
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "client-certificate-auth",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
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": {