client-certificate-auth 2.1.1 → 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 +46 -17
- package/lib/extractor.js +2 -1
- package/lib/fetch.js +1 -1
- package/lib/parsers.js +6 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# client-certificate-auth
|
|
2
2
|
|
|
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.
|
|
3
|
+
Comprehensive zero-dependency 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
|
+
|
|
5
|
+
This library lets you make additional auth decisions based on specific information within a client certificate after the client certificate is accepted at the socket level (i.e. by Node's `https` or a reverse proxy). This allows for fine-grained access control on top of the all-or-nothing allow/deny access control afforded by basic mTLS.
|
|
4
6
|
|
|
5
7
|
[](https://github.com/tgies/client-certificate-auth/actions/workflows/ci.yml)
|
|
6
8
|
[](https://www.npmjs.com/package/client-certificate-auth)
|
|
@@ -15,9 +17,13 @@ Comprehensive toolkit for client SSL certificate authentication (mTLS) in Node.j
|
|
|
15
17
|
|
|
16
18
|
**Fanatically Tested** - 100% line/branch/function/statement coverage, plus mutation testing and E2E tests against real nginx/Envoy/Traefik containers. ~6,207 lines of test code for ~937 lines of source (measured by [cloc](https://github.com/AlDanial/cloc)).
|
|
17
19
|
|
|
20
|
+
**Maintained since 2013** - Originally released for Node 0.10!
|
|
21
|
+
|
|
22
|
+
**Zero runtime dependencies** - Entirely self-contained, with straightforward, readable logic, no behavior hidden inside dependencies, no bloat, and no transitive-dependency security headaches.
|
|
23
|
+
|
|
18
24
|
## What Is This?
|
|
19
25
|
|
|
20
|
-
This library authenticates HTTP clients by their TLS client certificates, a scheme usually called mutual TLS (mTLS). Instead of presenting a password, API key, or bearer token, the client presents an X.509 certificate during the TLS handshake and proves possession of its private key; the server checks that the certificate was issued by a CA it trusts. The certificate is the credential.
|
|
26
|
+
This library authenticates HTTP clients by their TLS client certificates, a scheme usually called mutual TLS (mTLS). Instead of presenting a password, API key, or bearer token, the client presents an X.509 certificate during the TLS handshake and proves possession of its private key; the server checks that the certificate was issued by a CA it trusts. The certificate is the credential. The certificate signature verification is typically accomplished at the socket level by Node's `https` or a reverse proxy; this library's purpose is to allow you to easily inspect the properties of a verified certificate and make fine-grained access control decisions based on them.
|
|
21
27
|
|
|
22
28
|
Typical uses:
|
|
23
29
|
|
|
@@ -185,8 +191,9 @@ Returns Express middleware.
|
|
|
185
191
|
| Name | Type | Description |
|
|
186
192
|
|------|------|-------------|
|
|
187
193
|
| `callback` | `(cert, req?) => boolean \| PromiseLike<boolean>` | Receives the client certificate and request, returns `true` to allow access |
|
|
188
|
-
| `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'` |
|
|
189
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) |
|
|
190
197
|
| `options.headerEncoding` | `string` | Encoding format: `'url-pem'`, `'url-pem-aws'`, `'xfcc'`, `'base64-der'`, `'rfc9440'` |
|
|
191
198
|
| `options.fallbackToSocket` | `boolean` | If header extraction fails, try `socket.getPeerCertificate()` (default: `false`) |
|
|
192
199
|
| `options.includeChain` | `boolean` | If `true`, include full certificate chain via `cert.issuerCertificate` (default: `false`) |
|
|
@@ -407,21 +414,36 @@ When your application runs behind a TLS-terminating reverse proxy, the client ce
|
|
|
407
414
|
For common proxies, use the `certificateSource` option:
|
|
408
415
|
|
|
409
416
|
```javascript
|
|
410
|
-
// AWS Application Load Balancer
|
|
417
|
+
// AWS Application Load Balancer (mTLS passthrough)
|
|
411
418
|
app.use(clientCertificateAuth(checkAuth, {
|
|
412
419
|
certificateSource: 'aws-alb'
|
|
413
420
|
}));
|
|
414
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
|
+
|
|
415
432
|
// Envoy / Istio
|
|
416
433
|
app.use(clientCertificateAuth(checkAuth, {
|
|
417
434
|
certificateSource: 'envoy'
|
|
418
435
|
}));
|
|
419
436
|
|
|
420
|
-
// Cloudflare
|
|
437
|
+
// Cloudflare (legacy Cf-Client-Cert-Der-Base64 header)
|
|
421
438
|
app.use(clientCertificateAuth(checkAuth, {
|
|
422
439
|
certificateSource: 'cloudflare'
|
|
423
440
|
}));
|
|
424
441
|
|
|
442
|
+
// Cloudflare (RFC 9440 Client-Cert / Client-Cert-Chain headers, March 2026+)
|
|
443
|
+
app.use(clientCertificateAuth(checkAuth, {
|
|
444
|
+
certificateSource: 'cloudflare-rfc9440'
|
|
445
|
+
}));
|
|
446
|
+
|
|
425
447
|
// Traefik
|
|
426
448
|
app.use(clientCertificateAuth(checkAuth, {
|
|
427
449
|
certificateSource: 'traefik'
|
|
@@ -430,16 +452,21 @@ app.use(clientCertificateAuth(checkAuth, {
|
|
|
430
452
|
|
|
431
453
|
### Preset Details
|
|
432
454
|
|
|
433
|
-
| Preset | Header | Encoding |
|
|
434
|
-
|
|
455
|
+
| Preset | Header(s) | Encoding |
|
|
456
|
+
|--------|-----------|----------|
|
|
435
457
|
| `aws-alb` | `X-Amzn-Mtls-Clientcert` | URL-encoded PEM (AWS variant) |
|
|
436
|
-
| `
|
|
458
|
+
| `aws-alb-verify` | `X-Amzn-Mtls-Clientcert-Leaf` | URL-encoded PEM (AWS variant) |
|
|
459
|
+
| `azure-app-service` | `X-ARR-ClientCert` | Base64-encoded DER |
|
|
437
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 |
|
|
438
463
|
| `traefik` | `X-Forwarded-Tls-Client-Cert` | Base64-encoded DER \* |
|
|
439
464
|
|
|
440
|
-
>
|
|
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`.
|
|
466
|
+
|
|
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.
|
|
441
468
|
|
|
442
|
-
> **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'`.
|
|
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.
|
|
443
470
|
|
|
444
471
|
### Custom Headers
|
|
445
472
|
|
|
@@ -452,9 +479,9 @@ app.use(clientCertificateAuth(checkAuth, {
|
|
|
452
479
|
headerEncoding: 'url-pem'
|
|
453
480
|
}));
|
|
454
481
|
|
|
455
|
-
// Google Cloud Load Balancer (RFC 9440)
|
|
482
|
+
// Google Cloud Load Balancer (custom header populated from {client_cert_leaf}, RFC 9440)
|
|
456
483
|
app.use(clientCertificateAuth(checkAuth, {
|
|
457
|
-
certificateHeader: 'X-
|
|
484
|
+
certificateHeader: 'X-Client-Cert-Leaf',
|
|
458
485
|
headerEncoding: 'rfc9440'
|
|
459
486
|
}));
|
|
460
487
|
|
|
@@ -469,11 +496,13 @@ app.use(clientCertificateAuth(checkAuth, {
|
|
|
469
496
|
|
|
470
497
|
| Encoding | Description | Used By |
|
|
471
498
|
|----------|-------------|---------|
|
|
472
|
-
| `url-pem` | URL-encoded PEM certificate | nginx
|
|
499
|
+
| `url-pem` | URL-encoded PEM certificate | nginx (`$ssl_client_escaped_cert`) |
|
|
473
500
|
| `url-pem-aws` | URL-encoded PEM (AWS variant, `+` as safe char) | AWS ALB |
|
|
474
501
|
| `xfcc` | Envoy's structured `Key=Value;...` format | Envoy, Istio |
|
|
475
|
-
| `base64-der` | Base64-encoded DER certificate | Cloudflare, Traefik |
|
|
476
|
-
| `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.
|
|
477
506
|
|
|
478
507
|
### Fallback Mode
|
|
479
508
|
|
|
@@ -640,8 +669,8 @@ app.use(clientCertificateAuth(allowCN(['service-a', 'service-b'])));
|
|
|
640
669
|
|
|
641
670
|
// Allowlist by fingerprint
|
|
642
671
|
app.use(clientCertificateAuth(allowFingerprints([
|
|
643
|
-
'SHA256:AB:CD:EF:...',
|
|
644
|
-
'AB:CD:EF:...'
|
|
672
|
+
'SHA256:AB:CD:EF:...', // matched against cert.fingerprint256
|
|
673
|
+
'AB:CD:EF:...' // SHA-1, matched against cert.fingerprint
|
|
645
674
|
])));
|
|
646
675
|
|
|
647
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/fetch.js
CHANGED
|
@@ -48,7 +48,7 @@ import { extractClientCertificate } from './extractor.js';
|
|
|
48
48
|
export function extractClientCertificateFromRequest(request, options = {}) {
|
|
49
49
|
// Web Headers normalizes to lowercase, but Map and other iterables
|
|
50
50
|
// preserve casing. Normalize here for the core extractor's lowercase lookup.
|
|
51
|
-
const headers =
|
|
51
|
+
const headers = Object.create(null);
|
|
52
52
|
for (const [name, value] of request.headers) {
|
|
53
53
|
headers[name.toLowerCase()] = value;
|
|
54
54
|
}
|
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",
|