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 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'`, `'envoy'`, `'cloudflare'`, `'traefik'` |
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
- | `envoy` | `X-Forwarded-Client-Cert` | XFCC structured format |
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
- > \* **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.
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
- > **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'`.
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-SSL-Whatever-You-Use',
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, HAProxy |
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:...' // SHA256: prefix optional
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
- if (chainHeaderValue && !Array.isArray(chainHeaderValue)) {
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.2",
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": "^25.6.0",
108
+ "@types/node": "^26.1.1",
109
109
  "c8": "^11.0.0",
110
110
  "eslint": "^10.2.1",
111
111
  "globals": "^17.5.0",