client-certificate-auth 1.1.1 → 1.1.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
@@ -600,12 +600,71 @@ app.use(clientCertificateAuth(checkAuth, {
600
600
 
601
601
  ## CommonJS
602
602
 
603
+ The main entry point works with `require()` out of the box for socket-based mTLS:
604
+
603
605
  ```javascript
604
606
  const clientCertificateAuth = require('client-certificate-auth');
605
607
 
606
608
  app.use(clientCertificateAuth((cert) => cert.subject.CN === 'admin'));
607
609
  ```
608
610
 
611
+ The sync CJS wrapper supports `includeChain`, `onAuthenticated`, and `onRejected` options:
612
+
613
+ ```javascript
614
+ const clientCertificateAuth = require('client-certificate-auth');
615
+
616
+ app.use(clientCertificateAuth(
617
+ (cert) => cert.subject.CN === 'admin',
618
+ {
619
+ includeChain: true,
620
+ onAuthenticated: (cert, req) => {
621
+ console.log(`Authenticated: ${cert.subject.CN}`);
622
+ }
623
+ }
624
+ ));
625
+ ```
626
+
627
+ ### Full Features via `load()`
628
+
629
+ Reverse proxy support (header-based certificate extraction) requires async initialization. Use the `load()` function to get the full-featured ESM module:
630
+
631
+ ```javascript
632
+ const { load } = require('client-certificate-auth');
633
+
634
+ async function setup() {
635
+ const clientCertificateAuth = await load();
636
+
637
+ app.use(clientCertificateAuth(checkAuth, {
638
+ certificateSource: 'aws-alb' // Now supported
639
+ }));
640
+ }
641
+
642
+ setup();
643
+ ```
644
+
645
+ The `load()` function dynamically imports the ESM module and caches it. Subsequent calls return the cached module immediately.
646
+
647
+ ### CJS Limitations
648
+
649
+ | Feature | `require()` (sync) | `load()` (async) |
650
+ |---------|---------------------|-------------------|
651
+ | Socket-based mTLS | Yes | Yes |
652
+ | `includeChain` | Yes | Yes |
653
+ | `onAuthenticated` / `onRejected` | Yes | Yes |
654
+ | `certificateSource` presets | No | Yes |
655
+ | `certificateHeader` / `headerEncoding` | No | Yes |
656
+ | `verifyHeader` / `verifyValue` | No | Yes |
657
+ | `fallbackToSocket` | No | Yes |
658
+
659
+ 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()`:
660
+
661
+ ```javascript
662
+ async function setup() {
663
+ const { allowCN, allOf, allowIssuer } = await import('client-certificate-auth/helpers');
664
+ // ...
665
+ }
666
+ ```
667
+
609
668
  ## Testing
610
669
 
611
670
  This library has comprehensive test coverage across multiple layers:
@@ -625,6 +684,104 @@ The E2E tests spin up real reverse proxies, generate fresh certificates, and ver
625
684
  - **When using header-based auth**, ensure your proxy strips certificate headers from external requests
626
685
  - Use `verifyHeader`/`verifyValue` as defense-in-depth when using header-based authentication
627
686
 
687
+ ## Troubleshooting
688
+
689
+ ### `DEPTH_ZERO_SELF_SIGNED_CERT` error
690
+
691
+ This error occurs when the TLS layer rejects a self-signed client certificate. Set `rejectUnauthorized: false` in your HTTPS server options to let the middleware handle authorization instead of dropping the connection:
692
+
693
+ ```javascript
694
+ const opts = {
695
+ key: fs.readFileSync('server.key'),
696
+ cert: fs.readFileSync('server.pem'),
697
+ ca: fs.readFileSync('ca.pem'),
698
+ requestCert: true,
699
+ rejectUnauthorized: false // Required for self-signed certs
700
+ };
701
+
702
+ https.createServer(opts, app).listen(443);
703
+ ```
704
+
705
+ > **Warning:** In production, prefer certificates signed by your own CA rather than self-signed certificates. If you must use self-signed certs, ensure you set `ca` to the self-signed certificate so Node.js can verify the chain.
706
+
707
+ ### Certificate not reaching middleware
708
+
709
+ If the middleware always rejects with "socket not authorized", verify that your HTTPS server has `requestCert: true` set. Without this option, Node.js will not ask clients for a certificate during the TLS handshake:
710
+
711
+ ```javascript
712
+ const opts = {
713
+ // ...
714
+ requestCert: true, // Must be true
715
+ rejectUnauthorized: false
716
+ };
717
+ ```
718
+
719
+ Also confirm that the client is actually sending a certificate. Tools like `openssl s_client` can verify this:
720
+
721
+ ```bash
722
+ openssl s_client -connect localhost:443 -cert client.pem -key client.key
723
+ ```
724
+
725
+ ### Reverse proxy headers not working
726
+
727
+ When using header-based certificate extraction behind a reverse proxy:
728
+
729
+ 1. **Verify the proxy is setting the correct header.** Check your proxy logs or use a test endpoint to inspect incoming headers.
730
+
731
+ 2. **Ensure the `certificateSource` or `certificateHeader`/`headerEncoding` options match your proxy's configuration.** A mismatch will result in unparseable or missing certificate data.
732
+
733
+ 3. **Confirm the proxy strips certificate headers from external requests.** If external clients can set these headers directly, they can bypass authentication. See [Security Considerations](#security-considerations).
734
+
735
+ 4. **Consider using `verifyHeader`/`verifyValue`** for defense-in-depth, so the middleware validates that the proxy actually verified the certificate.
736
+
737
+ ### WebSocket authentication failing
738
+
739
+ For WebSocket connections using the `ws` library with `noServer: true`, you must handle the `upgrade` event yourself and run the middleware manually. The middleware needs a response-like object and a `next` callback:
740
+
741
+ ```javascript
742
+ server.on('upgrade', (req, socket, head) => {
743
+ const middleware = clientCertificateAuth(checkAuth);
744
+ const res = { writeHead: () => {}, end: () => {}, redirect: () => {} };
745
+
746
+ middleware(req, res, (err) => {
747
+ if (err) {
748
+ socket.write(`HTTP/1.1 ${err.status} ${err.message}\r\n\r\n`);
749
+ socket.destroy();
750
+ return;
751
+ }
752
+ wss.handleUpgrade(req, socket, head, (ws) => {
753
+ wss.emit('connection', ws, req);
754
+ });
755
+ });
756
+ });
757
+ ```
758
+
759
+ See the full [WebSocket Support](#websocket-support) section for complete examples with `ws` and Socket.IO.
760
+
761
+ ### ESM vs CJS import differences
762
+
763
+ This package is an ES module (`"type": "module"` in package.json) with a CJS compatibility wrapper.
764
+
765
+ **ESM** (recommended):
766
+ ```javascript
767
+ import clientCertificateAuth from 'client-certificate-auth';
768
+ import { allowCN } from 'client-certificate-auth/helpers';
769
+ ```
770
+
771
+ **CJS** (sync, socket-only):
772
+ ```javascript
773
+ const clientCertificateAuth = require('client-certificate-auth');
774
+ ```
775
+
776
+ **CJS** (async, full features):
777
+ ```javascript
778
+ const clientCertificateAuth = await require('client-certificate-auth').load();
779
+ ```
780
+
781
+ 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.
782
+
783
+ The `/helpers` and `/parsers` subpath exports are ESM-only. In CJS, use dynamic `import()` to access them.
784
+
628
785
  ## License
629
786
 
630
787
  MIT © Tony Gies
@@ -45,11 +45,9 @@ export interface ClientCertRequest extends IncomingMessage {
45
45
  }
46
46
 
47
47
  /**
48
- * Extended response object with redirect method.
48
+ * Response object for client certificate auth middleware.
49
49
  */
50
- export interface ClientCertResponse extends ServerResponse {
51
- redirect(statusOrUrl: number | string, url?: string): void;
52
- }
50
+ export type ClientCertResponse = ServerResponse;
53
51
 
54
52
  /**
55
53
  * Options for the synchronous CommonJS wrapper.
@@ -43,11 +43,9 @@ export interface ClientCertRequest extends IncomingMessage {
43
43
  }
44
44
 
45
45
  /**
46
- * Extended response object with redirect method.
46
+ * Response object for client certificate auth middleware.
47
47
  */
48
- export interface ClientCertResponse extends ServerResponse {
49
- redirect(statusOrUrl: number | string, url?: string): void;
50
- }
48
+ export type ClientCertResponse = ServerResponse;
51
49
 
52
50
  export interface ClientCertificateAuthOptions {
53
51
  /**
package/lib/helpers.d.ts CHANGED
@@ -4,12 +4,13 @@
4
4
  * @license MIT
5
5
  */
6
6
 
7
- import type { PeerCertificate } from 'tls';
7
+ import type { PeerCertificate, DetailedPeerCertificate } from 'tls';
8
+ import type { ClientCertRequest } from './clientCertificateAuth.js';
8
9
 
9
10
  /**
10
11
  * Validation callback for clientCertificateAuth middleware.
11
12
  */
12
- export type ValidationCallback = (cert: PeerCertificate, req?: import('http').IncomingMessage) => boolean | Promise<boolean>;
13
+ export type ValidationCallback = (cert: PeerCertificate | DetailedPeerCertificate, req?: ClientCertRequest) => boolean | Promise<boolean>;
13
14
 
14
15
  /**
15
16
  * Distinguished Name fields for matching.
@@ -37,7 +38,9 @@ export declare function allowCN(names: string[]): ValidationCallback;
37
38
 
38
39
  /**
39
40
  * Create a validation callback that allows certificates with matching fingerprints.
40
- * Supports both full format (e.g., "SHA256:AB:CD:...") and raw hex.
41
+ * Supports SHA-1 fingerprints (compared against cert.fingerprint) and SHA-256
42
+ * fingerprints with "SHA256:" prefix (compared against cert.fingerprint256).
43
+ * Fingerprints without a prefix are treated as SHA-1.
41
44
  * @param fingerprints - Allowed fingerprints
42
45
  */
43
46
  export declare function allowFingerprints(fingerprints: string[]): ValidationCallback;
package/lib/helpers.js CHANGED
@@ -25,25 +25,40 @@ export function allowCN(names) {
25
25
 
26
26
  /**
27
27
  * Create a validation callback that allows certificates with matching fingerprints.
28
- * Supports both full format (e.g., "SHA256:AB:CD:...") and raw hex.
28
+ * Supports SHA-1 fingerprints (compared against cert.fingerprint) and SHA-256
29
+ * fingerprints with "SHA256:" prefix (compared against cert.fingerprint256).
30
+ * Fingerprints without a prefix are treated as SHA-1.
29
31
  *
30
32
  * @param {string[]} fingerprints - Allowed fingerprints
31
33
  * @returns {ValidationCallback}
32
34
  *
33
35
  * @example
34
36
  * app.use(clientCertificateAuth(allowFingerprints([
35
- * 'SHA256:AB:CD:EF:...',
36
- * 'AB:CD:EF:...'
37
+ * 'SHA256:AB:CD:EF:...', // matched against cert.fingerprint256
38
+ * 'AB:CD:EF:...' // matched against cert.fingerprint (SHA-1)
37
39
  * ])));
38
40
  */
39
41
  export function allowFingerprints(fingerprints) {
40
- // Normalize fingerprints: uppercase, remove SHA256: prefix if present
41
- const normalize = (fp) => fp.toUpperCase().replace(/^SHA256:/i, '');
42
- const allowed = new Set(fingerprints.map(normalize));
42
+ const sha256Allowed = new Set();
43
+ const sha1Allowed = new Set();
44
+
45
+ for (const fp of fingerprints) {
46
+ const upper = fp.toUpperCase();
47
+ if (upper.startsWith('SHA256:')) {
48
+ sha256Allowed.add(upper.slice(7));
49
+ } else {
50
+ sha1Allowed.add(upper);
51
+ }
52
+ }
43
53
 
44
54
  return (cert) => {
45
- if (!cert.fingerprint) {return false;}
46
- return allowed.has(normalize(cert.fingerprint));
55
+ if (sha1Allowed.size > 0 && cert.fingerprint) {
56
+ if (sha1Allowed.has(cert.fingerprint.toUpperCase())) {return true;}
57
+ }
58
+ if (sha256Allowed.size > 0 && cert.fingerprint256) {
59
+ if (sha256Allowed.has(cert.fingerprint256.toUpperCase())) {return true;}
60
+ }
61
+ return false;
47
62
  };
48
63
  }
49
64
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "client-certificate-auth",
3
- "version": "1.1.1",
4
- "description": "Middleware for Node.js implementing client SSL certificate authentication/authorization",
3
+ "version": "1.1.2",
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": {
7
7
  "url": "https://github.com/tgies/client-certificate-auth/issues"
@@ -46,12 +46,12 @@
46
46
  "@stryker-mutator/core": "^9.4.0",
47
47
  "@stryker-mutator/jest-runner": "^9.4.0",
48
48
  "@types/express": "^5.0.0",
49
- "@types/node": "^22.10.2",
49
+ "@types/node": "^25.2.1",
50
50
  "c8": "^10.1.3",
51
51
  "eslint": "^9.17.0",
52
52
  "globals": "^17.0.0",
53
53
  "husky": "^9.1.7",
54
- "jest": "^29.7.0",
54
+ "jest": "^30.2.0",
55
55
  "lint-staged": "^16.2.7",
56
56
  "selfsigned": "^5.4.0",
57
57
  "typescript": "^5.7.2",
@@ -86,7 +86,13 @@
86
86
  "client-certificate",
87
87
  "express",
88
88
  "connect",
89
- "middleware"
89
+ "middleware",
90
+ "mutual-tls",
91
+ "zero-trust",
92
+ "pki",
93
+ "x509",
94
+ "reverse-proxy",
95
+ "certificate-authentication"
90
96
  ],
91
97
  "author": "Tony Gies <tony.gies@gruppe86.net> (https://github.com/tgies)",
92
98
  "license": "MIT",