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 +157 -0
- package/lib/clientCertificateAuth.d.cts +2 -4
- package/lib/clientCertificateAuth.d.ts +2 -4
- package/lib/helpers.d.ts +6 -3
- package/lib/helpers.js +23 -8
- package/package.json +11 -5
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
|
-
*
|
|
48
|
+
* Response object for client certificate auth middleware.
|
|
49
49
|
*/
|
|
50
|
-
export
|
|
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
|
-
*
|
|
46
|
+
* Response object for client certificate auth middleware.
|
|
47
47
|
*/
|
|
48
|
-
export
|
|
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?:
|
|
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
|
|
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
|
|
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
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
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 (
|
|
46
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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": "^
|
|
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": "^
|
|
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",
|