client-certificate-auth 2.1.0 → 2.1.1
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 +20 -8
- package/lib/clientCertificateAuth.cjs +17 -2
- package/lib/clientCertificateAuth.js +17 -2
- package/lib/fetch.js +0 -1
- package/lib/parsers.js +2 -2
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -15,6 +15,26 @@ Comprehensive toolkit for client SSL certificate authentication (mTLS) in Node.j
|
|
|
15
15
|
|
|
16
16
|
**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
17
|
|
|
18
|
+
## What Is This?
|
|
19
|
+
|
|
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.
|
|
21
|
+
|
|
22
|
+
Typical uses:
|
|
23
|
+
|
|
24
|
+
- **Service-to-service APIs**: internal microservices or partner integrations where each caller holds its own certificate rather than a shared secret.
|
|
25
|
+
- **Machine and device authentication**: CI runners, IoT devices, daemons. Certificates are issued per machine and revoked per machine.
|
|
26
|
+
- **Restricting sensitive endpoints**: admin interfaces, metrics, internal tooling that only known clients should reach.
|
|
27
|
+
- **Certificate-based user login**: enterprise PKI and smart-card environments, where certificates map to user accounts.
|
|
28
|
+
|
|
29
|
+
The TLS handshake proves who the client is; your application decides whether that client is allowed in. This library covers the part in between: it extracts the verified certificate from the request wherever your TLS terminates, parses it into a standard [`tls.PeerCertificate`](https://nodejs.org/api/tls.html#certificate-object) object, and passes it to your authorization callback. Supported certificate sources:
|
|
30
|
+
|
|
31
|
+
- **TLS terminated directly in Node.js**: the certificate is read from the TLS socket.
|
|
32
|
+
- **A TLS-terminating reverse proxy or load balancer** that forwards the certificate in an HTTP header: AWS ALB, Envoy/Istio, Cloudflare, Traefik, Azure App Service, nginx, HAProxy, and any proxy that implements RFC 9440 (e.g. Google Cloud Load Balancer).
|
|
33
|
+
- **AWS Lambda** behind API Gateway mTLS, via `client-certificate-auth/lambda`.
|
|
34
|
+
- **Web-standard `Request` runtimes** (Hono, Next.js, SvelteKit, Cloudflare Workers, Bun, Deno), via `client-certificate-auth/fetch`.
|
|
35
|
+
|
|
36
|
+
Express and Connect get drop-in middleware. Other frameworks use the same extraction logic through `extractClientCertificate()`. Pre-built authorization helpers cover the common policies (allowlist by CN, fingerprint, issuer, OU, SAN, and more).
|
|
37
|
+
|
|
18
38
|
## Installation
|
|
19
39
|
|
|
20
40
|
```bash
|
|
@@ -23,14 +43,6 @@ npm install client-certificate-auth
|
|
|
23
43
|
|
|
24
44
|
**Requirements:** Node.js >= 20
|
|
25
45
|
|
|
26
|
-
## Synopsis
|
|
27
|
-
|
|
28
|
-
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).
|
|
29
|
-
|
|
30
|
-
The certificate is parsed into a standard `tls.PeerCertificate` object and passed to your callback for authorization logic.
|
|
31
|
-
|
|
32
|
-
Compatible with Express, Connect, or any Node.js HTTP server framework by using the framework-agnostic `extractClientCertificate` function.
|
|
33
|
-
|
|
34
46
|
## Usage
|
|
35
47
|
|
|
36
48
|
### Basic Setup
|
|
@@ -32,6 +32,19 @@ function isThenable(value) {
|
|
|
32
32
|
&& typeof value.then === 'function';
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Wrap primitive values thrown or rejected by the validation callback in an
|
|
37
|
+
* Error so `.message` and `.status` access is safe; objects pass through.
|
|
38
|
+
* @param {unknown} err
|
|
39
|
+
* @returns {any}
|
|
40
|
+
*/
|
|
41
|
+
function normalizeCallbackError(err) {
|
|
42
|
+
if (err !== null && (typeof err === 'object' || typeof err === 'function')) {
|
|
43
|
+
return err;
|
|
44
|
+
}
|
|
45
|
+
return new Error(err === null || err === undefined ? '' : String(err));
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
/**
|
|
36
49
|
* Options not supported by the sync CJS wrapper.
|
|
37
50
|
* These require the ESM module's async header parsing.
|
|
@@ -145,7 +158,8 @@ function clientCertificateAuth(callback, options = {}) {
|
|
|
145
158
|
try {
|
|
146
159
|
const result = callback(cert, req);
|
|
147
160
|
if (isThenable(result)) {
|
|
148
|
-
Promise.resolve(result).then(doneAuthorizing).catch((
|
|
161
|
+
Promise.resolve(result).then(doneAuthorizing).catch((rejection) => {
|
|
162
|
+
const err = normalizeCallbackError(rejection);
|
|
149
163
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
150
164
|
if (err.status === undefined) {
|
|
151
165
|
err.status = 401;
|
|
@@ -155,7 +169,8 @@ function clientCertificateAuth(callback, options = {}) {
|
|
|
155
169
|
} else {
|
|
156
170
|
doneAuthorizing(result);
|
|
157
171
|
}
|
|
158
|
-
} catch (
|
|
172
|
+
} catch (thrown) {
|
|
173
|
+
const err = normalizeCallbackError(thrown);
|
|
159
174
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
160
175
|
if (err.status === undefined) {
|
|
161
176
|
err.status = 401;
|
|
@@ -20,6 +20,19 @@ function isThenable(value) {
|
|
|
20
20
|
&& typeof /** @type {{then?: unknown}} */ (value).then === 'function';
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Wrap primitive values thrown or rejected by the validation callback in an
|
|
25
|
+
* Error so `.message` and `.status` access is safe; objects pass through.
|
|
26
|
+
* @param {unknown} err
|
|
27
|
+
* @returns {any}
|
|
28
|
+
*/
|
|
29
|
+
function normalizeCallbackError(err) {
|
|
30
|
+
if (err !== null && (typeof err === 'object' || typeof err === 'function')) {
|
|
31
|
+
return err;
|
|
32
|
+
}
|
|
33
|
+
return new Error(err === null || err === undefined ? '' : String(err));
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
/**
|
|
24
37
|
* @typedef {import('http').IncomingMessage & { secure?: boolean; socket: import('net').Socket & { authorized?: boolean; authorizationError?: Error | string; getPeerCertificate?: (detailed?: boolean) => import('tls').PeerCertificate | import('tls').DetailedPeerCertificate }; clientCertificate?: import('tls').PeerCertificate }} ClientCertRequest
|
|
25
38
|
* @typedef {import('http').ServerResponse & { redirect: (statusOrUrl: number | string, url?: string) => void }} ClientCertResponse
|
|
@@ -195,7 +208,8 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
195
208
|
try {
|
|
196
209
|
const callbackResult = callback(cert, req);
|
|
197
210
|
if (isThenable(callbackResult)) {
|
|
198
|
-
Promise.resolve(callbackResult).then(doneAuthorizing).catch((
|
|
211
|
+
Promise.resolve(callbackResult).then(doneAuthorizing).catch((rejection) => {
|
|
212
|
+
const err = normalizeCallbackError(rejection);
|
|
199
213
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
200
214
|
if (err.status === undefined) {
|
|
201
215
|
err.status = 401;
|
|
@@ -205,7 +219,8 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
205
219
|
} else {
|
|
206
220
|
doneAuthorizing(callbackResult);
|
|
207
221
|
}
|
|
208
|
-
} catch (
|
|
222
|
+
} catch (thrown) {
|
|
223
|
+
const err = normalizeCallbackError(thrown);
|
|
209
224
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
210
225
|
if (err.status === undefined) {
|
|
211
226
|
err.status = 401;
|
package/lib/fetch.js
CHANGED
|
@@ -53,7 +53,6 @@ export function extractClientCertificateFromRequest(request, options = {}) {
|
|
|
53
53
|
headers[name.toLowerCase()] = value;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
// Stryker disable next-line ObjectLiteral: destructure-discard strips fallbackToSocket
|
|
57
56
|
const { fallbackToSocket: _ignored, ...rest } = options;
|
|
58
57
|
|
|
59
58
|
return extractClientCertificate({ headers }, rest);
|
package/lib/parsers.js
CHANGED
|
@@ -161,7 +161,7 @@ function chainFromMultiBlockPem(pem) {
|
|
|
161
161
|
try {
|
|
162
162
|
return pemToCertificate(block);
|
|
163
163
|
} catch {
|
|
164
|
-
//
|
|
164
|
+
// Equivalent mutant (undefined also filtered by .filter(Boolean)) — catch-body BlockStatement unsuppressible via Stryker comments
|
|
165
165
|
return null;
|
|
166
166
|
}
|
|
167
167
|
}).filter(Boolean);
|
|
@@ -357,7 +357,7 @@ export function parseRfc9440(headerValue) {
|
|
|
357
357
|
let base64 = headerValue;
|
|
358
358
|
// Stryker disable next-line BlockStatement,ConditionalExpression,StringLiteral,LogicalOperator,MethodExpression: colon-stripping is defensive; all valid RFC 9440 inputs use :base64: format, so base64 decoder ignores colons either way
|
|
359
359
|
if (base64.startsWith(':') && base64.endsWith(':')) {
|
|
360
|
-
// Stryker disable next-line
|
|
360
|
+
// Stryker disable next-line MethodExpression: no-oping the assignment is equivalent — base64 decoder ignores colons
|
|
361
361
|
base64 = base64.slice(1, -1);
|
|
362
362
|
}
|
|
363
363
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "client-certificate-auth",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
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": {
|
|
@@ -99,8 +99,8 @@
|
|
|
99
99
|
},
|
|
100
100
|
"devDependencies": {
|
|
101
101
|
"@arethetypeswrong/cli": "^0.18.2",
|
|
102
|
-
"@commitlint/cli": "^
|
|
103
|
-
"@commitlint/config-conventional": "^
|
|
102
|
+
"@commitlint/cli": "^21.0.0",
|
|
103
|
+
"@commitlint/config-conventional": "^21.0.0",
|
|
104
104
|
"@eslint/js": "^10.0.1",
|
|
105
105
|
"@stryker-mutator/core": "^9.6.0",
|
|
106
106
|
"@stryker-mutator/jest-runner": "^9.6.1",
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"globals": "^17.5.0",
|
|
112
112
|
"husky": "^9.1.7",
|
|
113
113
|
"jest": "^30.3.0",
|
|
114
|
-
"lint-staged": "^
|
|
114
|
+
"lint-staged": "^17.0.4",
|
|
115
115
|
"selfsigned": "^5.5.0",
|
|
116
116
|
"typedoc": "^0.28.19",
|
|
117
117
|
"typedoc-plugin-markdown": "^4.11.0",
|
|
@@ -172,6 +172,7 @@
|
|
|
172
172
|
"lib/**/*.d.cts"
|
|
173
173
|
],
|
|
174
174
|
"overrides": {
|
|
175
|
-
"fflate": "0.8.2"
|
|
175
|
+
"fflate": "0.8.2",
|
|
176
|
+
"qs": "^6.15.2"
|
|
176
177
|
}
|
|
177
178
|
}
|