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 +60 -12
- package/lib/clientCertificateAuth.d.ts +1 -1
- package/lib/clientCertificateAuth.js +1 -1
- package/lib/helpers.js +2 -0
- package/lib/parsers.js +45 -1
- package/package.json +21 -14
package/README.md
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
# client-certificate-auth
|
|
2
2
|
|
|
3
|
-
|
|
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
|
[](https://github.com/tgies/client-certificate-auth/actions/workflows/ci.yml)
|
|
6
6
|
[](https://www.npmjs.com/package/client-certificate-auth)
|
|
7
7
|
[](https://codecov.io/gh/tgies/client-certificate-auth)
|
|
8
8
|
[](https://dashboard.stryker-mutator.io/reports/github.com/tgies/client-certificate-auth/master)
|
|
9
9
|
|
|
10
|
-
|
|
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
|
|
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,
|
|
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:**
|
|
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
|
-
|
|
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
|
-
|
|
769
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
76
|
-
"@commitlint/config-conventional": "^20.
|
|
77
|
-
"@stryker-mutator/core": "^9.
|
|
78
|
-
"@stryker-mutator/jest-runner": "^9.
|
|
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.
|
|
81
|
-
"c8": "^
|
|
82
|
-
"eslint": "^9.
|
|
83
|
-
"globals": "^17.
|
|
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.
|
|
86
|
-
"lint-staged": "^16.2
|
|
85
|
+
"jest": "^30.3.0",
|
|
86
|
+
"lint-staged": "^16.3.2",
|
|
87
87
|
"selfsigned": "^5.5.0",
|
|
88
|
-
"
|
|
89
|
-
"
|
|
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",
|