client-certificate-auth 2.1.2 → 2.1.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 +38 -15
- package/lib/extractor.js +2 -1
- package/lib/parsers.js +6 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -191,8 +191,9 @@ Returns Express middleware.
|
|
|
191
191
|
| Name | Type | Description |
|
|
192
192
|
|------|------|-------------|
|
|
193
193
|
| `callback` | `(cert, req?) => boolean \| PromiseLike<boolean>` | Receives the client certificate and request, returns `true` to allow access |
|
|
194
|
-
| `options.certificateSource` | `string` | Use a preset for a known proxy: `'aws-alb'`, `'
|
|
194
|
+
| `options.certificateSource` | `string` | Use a preset for a known proxy: `'aws-alb'`, `'aws-alb-verify'`, `'azure-app-service'`, `'cloudflare'`, `'cloudflare-rfc9440'`, `'envoy'`, `'traefik'` |
|
|
195
195
|
| `options.certificateHeader` | `string` | Custom header name to read certificate from |
|
|
196
|
+
| `options.chainHeader` | `string` | Second header carrying the certificate chain alongside the leaf (RFC 9440 style) |
|
|
196
197
|
| `options.headerEncoding` | `string` | Encoding format: `'url-pem'`, `'url-pem-aws'`, `'xfcc'`, `'base64-der'`, `'rfc9440'` |
|
|
197
198
|
| `options.fallbackToSocket` | `boolean` | If header extraction fails, try `socket.getPeerCertificate()` (default: `false`) |
|
|
198
199
|
| `options.includeChain` | `boolean` | If `true`, include full certificate chain via `cert.issuerCertificate` (default: `false`) |
|
|
@@ -413,21 +414,36 @@ When your application runs behind a TLS-terminating reverse proxy, the client ce
|
|
|
413
414
|
For common proxies, use the `certificateSource` option:
|
|
414
415
|
|
|
415
416
|
```javascript
|
|
416
|
-
// AWS Application Load Balancer
|
|
417
|
+
// AWS Application Load Balancer (mTLS passthrough)
|
|
417
418
|
app.use(clientCertificateAuth(checkAuth, {
|
|
418
419
|
certificateSource: 'aws-alb'
|
|
419
420
|
}));
|
|
420
421
|
|
|
422
|
+
// AWS Application Load Balancer (mTLS verify mode)
|
|
423
|
+
app.use(clientCertificateAuth(checkAuth, {
|
|
424
|
+
certificateSource: 'aws-alb-verify'
|
|
425
|
+
}));
|
|
426
|
+
|
|
427
|
+
// Azure App Service
|
|
428
|
+
app.use(clientCertificateAuth(checkAuth, {
|
|
429
|
+
certificateSource: 'azure-app-service'
|
|
430
|
+
}));
|
|
431
|
+
|
|
421
432
|
// Envoy / Istio
|
|
422
433
|
app.use(clientCertificateAuth(checkAuth, {
|
|
423
434
|
certificateSource: 'envoy'
|
|
424
435
|
}));
|
|
425
436
|
|
|
426
|
-
// Cloudflare
|
|
437
|
+
// Cloudflare (legacy Cf-Client-Cert-Der-Base64 header)
|
|
427
438
|
app.use(clientCertificateAuth(checkAuth, {
|
|
428
439
|
certificateSource: 'cloudflare'
|
|
429
440
|
}));
|
|
430
441
|
|
|
442
|
+
// Cloudflare (RFC 9440 Client-Cert / Client-Cert-Chain headers, March 2026+)
|
|
443
|
+
app.use(clientCertificateAuth(checkAuth, {
|
|
444
|
+
certificateSource: 'cloudflare-rfc9440'
|
|
445
|
+
}));
|
|
446
|
+
|
|
431
447
|
// Traefik
|
|
432
448
|
app.use(clientCertificateAuth(checkAuth, {
|
|
433
449
|
certificateSource: 'traefik'
|
|
@@ -436,16 +452,21 @@ app.use(clientCertificateAuth(checkAuth, {
|
|
|
436
452
|
|
|
437
453
|
### Preset Details
|
|
438
454
|
|
|
439
|
-
| Preset | Header | Encoding |
|
|
440
|
-
|
|
455
|
+
| Preset | Header(s) | Encoding |
|
|
456
|
+
|--------|-----------|----------|
|
|
441
457
|
| `aws-alb` | `X-Amzn-Mtls-Clientcert` | URL-encoded PEM (AWS variant) |
|
|
442
|
-
| `
|
|
458
|
+
| `aws-alb-verify` | `X-Amzn-Mtls-Clientcert-Leaf` | URL-encoded PEM (AWS variant) |
|
|
459
|
+
| `azure-app-service` | `X-ARR-ClientCert` | Base64-encoded DER |
|
|
443
460
|
| `cloudflare` | `Cf-Client-Cert-Der-Base64` | Base64-encoded DER |
|
|
461
|
+
| `cloudflare-rfc9440` | `Client-Cert` + `Client-Cert-Chain` | RFC 9440 |
|
|
462
|
+
| `envoy` | `X-Forwarded-Client-Cert` | XFCC structured format |
|
|
444
463
|
| `traefik` | `X-Forwarded-Tls-Client-Cert` | Base64-encoded DER \* |
|
|
445
464
|
|
|
446
|
-
>
|
|
465
|
+
> ⚠️ **Passthrough presets do not validate the certificate.** ALB mTLS passthrough mode (`aws-alb`) and Azure App Service (`azure-app-service`) forward whatever certificate the client presented during the handshake, without checking it against a trust store. The middleware parses the certificate, but your callback is responsible for trust verification: matching the issuer against an expected CA, checking the validity window, and checking revocation if applicable. A callback that only checks `cert.subject.CN` (including the `allowCN` helper) will accept any well-formed certificate a client presents. To rely on proxy-side validation instead, use ALB verify mode (`aws-alb-verify`), which validates against a configured trust store, or pin the certificate with `allowFingerprints`.
|
|
447
466
|
|
|
448
|
-
> **
|
|
467
|
+
> \* **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. This applies to Traefik v2.9.4 and later and all of v3; Traefik 2.8 through 2.9.1 URL-escaped the value, which this preset does not decode.
|
|
468
|
+
|
|
469
|
+
> **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'`. For the RFC 9440 forwarding feature (March 2026 and later), use the `cloudflare-rfc9440` preset, which pairs the `Client-Cert` header with `Client-Cert-Chain` via the `chainHeader` option.
|
|
449
470
|
|
|
450
471
|
### Custom Headers
|
|
451
472
|
|
|
@@ -458,9 +479,9 @@ app.use(clientCertificateAuth(checkAuth, {
|
|
|
458
479
|
headerEncoding: 'url-pem'
|
|
459
480
|
}));
|
|
460
481
|
|
|
461
|
-
// Google Cloud Load Balancer (RFC 9440)
|
|
482
|
+
// Google Cloud Load Balancer (custom header populated from {client_cert_leaf}, RFC 9440)
|
|
462
483
|
app.use(clientCertificateAuth(checkAuth, {
|
|
463
|
-
certificateHeader: 'X-
|
|
484
|
+
certificateHeader: 'X-Client-Cert-Leaf',
|
|
464
485
|
headerEncoding: 'rfc9440'
|
|
465
486
|
}));
|
|
466
487
|
|
|
@@ -475,11 +496,13 @@ app.use(clientCertificateAuth(checkAuth, {
|
|
|
475
496
|
|
|
476
497
|
| Encoding | Description | Used By |
|
|
477
498
|
|----------|-------------|---------|
|
|
478
|
-
| `url-pem` | URL-encoded PEM certificate | nginx
|
|
499
|
+
| `url-pem` | URL-encoded PEM certificate | nginx (`$ssl_client_escaped_cert`) |
|
|
479
500
|
| `url-pem-aws` | URL-encoded PEM (AWS variant, `+` as safe char) | AWS ALB |
|
|
480
501
|
| `xfcc` | Envoy's structured `Key=Value;...` format | Envoy, Istio |
|
|
481
|
-
| `base64-der` | Base64-encoded DER certificate | Cloudflare, Traefik |
|
|
482
|
-
| `rfc9440` | RFC 9440 format: `:base64-der:` | Google Cloud LB |
|
|
502
|
+
| `base64-der` | Base64-encoded DER certificate | Cloudflare, Traefik, Azure App Service, HAProxy (`ssl_c_der,base64`) |
|
|
503
|
+
| `rfc9440` | RFC 9440 format: `:base64-der:` | Cloudflare (RFC 9440 forwarding), Google Cloud LB |
|
|
504
|
+
|
|
505
|
+
HAProxy's native certificate forwarding (`http-request set-header ... %[ssl_c_der,base64]`) is base64 DER, so pair it with `base64-der`. HAProxy has no built-in URL-encoded-PEM output; `url-pem` from HAProxy requires a custom Lua `url_enc` converter.
|
|
483
506
|
|
|
484
507
|
### Fallback Mode
|
|
485
508
|
|
|
@@ -646,8 +669,8 @@ app.use(clientCertificateAuth(allowCN(['service-a', 'service-b'])));
|
|
|
646
669
|
|
|
647
670
|
// Allowlist by fingerprint
|
|
648
671
|
app.use(clientCertificateAuth(allowFingerprints([
|
|
649
|
-
'SHA256:AB:CD:EF:...',
|
|
650
|
-
'AB:CD:EF:...'
|
|
672
|
+
'SHA256:AB:CD:EF:...', // matched against cert.fingerprint256
|
|
673
|
+
'AB:CD:EF:...' // SHA-1, matched against cert.fingerprint
|
|
651
674
|
])));
|
|
652
675
|
|
|
653
676
|
// Allowlist by Organization
|
package/lib/extractor.js
CHANGED
|
@@ -163,7 +163,8 @@ export function extractClientCertificate(req, options = {}) {
|
|
|
163
163
|
const chainEncoding = headerEncoding ?? preset?.encoding;
|
|
164
164
|
if (chainHeaderName && chainEncoding) {
|
|
165
165
|
const chainHeaderValue = req.headers[chainHeaderName];
|
|
166
|
-
|
|
166
|
+
// Only a non-empty string is parseable; anything else leaves the leaf unchained.
|
|
167
|
+
if (typeof chainHeaderValue === 'string' && chainHeaderValue) {
|
|
167
168
|
const chainCerts = chainHeaderValue
|
|
168
169
|
.split(',')
|
|
169
170
|
.map(item => item.trim())
|
package/lib/parsers.js
CHANGED
|
@@ -403,6 +403,11 @@ export function derToCertificate(der) {
|
|
|
403
403
|
* @returns {PeerCertificate | null} Parsed certificate or null on failure
|
|
404
404
|
*/
|
|
405
405
|
export function parseHeaderValue(headerValue, encoding) {
|
|
406
|
+
// Only strings are parseable; a header name like 'constructor' resolves
|
|
407
|
+
// to a function on plain-object header maps.
|
|
408
|
+
if (typeof headerValue !== 'string') {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
406
411
|
switch (encoding) {
|
|
407
412
|
// Stryker disable next-line ConditionalExpression: parseUrlPem/parseUrlPemAws are equivalent for inputs without + chars
|
|
408
413
|
case 'url-pem':
|
|
@@ -468,6 +473,7 @@ export function getCertificateFromHeaders(headers, config) {
|
|
|
468
473
|
|
|
469
474
|
// Node.js HTTP consolidates duplicate headers into string[].
|
|
470
475
|
// Parsers expect a single string; reject arrays to fail safely.
|
|
476
|
+
// Stryker disable next-line ConditionalExpression,BlockStatement: →false/{} equivalent (parseHeaderValue rejects non-strings); →true killed by valid header extraction tests
|
|
471
477
|
if (Array.isArray(headerValue)) {
|
|
472
478
|
return null;
|
|
473
479
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "client-certificate-auth",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.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": {
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"@stryker-mutator/core": "^9.6.0",
|
|
106
106
|
"@stryker-mutator/jest-runner": "^9.6.1",
|
|
107
107
|
"@types/express": "^5.0.6",
|
|
108
|
-
"@types/node": "^
|
|
108
|
+
"@types/node": "^26.1.1",
|
|
109
109
|
"c8": "^11.0.0",
|
|
110
110
|
"eslint": "^10.2.1",
|
|
111
111
|
"globals": "^17.5.0",
|