client-certificate-auth 1.3.5 → 2.0.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 +12 -2
- package/lib/clientCertificateAuth.cjs +28 -3
- package/lib/clientCertificateAuth.d.cts +24 -9
- package/lib/clientCertificateAuth.d.ts +27 -10
- package/lib/clientCertificateAuth.js +27 -16
- package/lib/extractor.d.ts +7 -0
- package/lib/extractor.js +43 -8
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -8,11 +8,12 @@ Comprehensive toolkit for client SSL certificate authentication (mTLS) in Node.j
|
|
|
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
|
+
|
|
11
12
|
[**Commercial Support**](#commercial-support) - consulting, custom features, and priority support for production deployments
|
|
12
13
|
|
|
13
14
|
**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
|
|
|
15
|
-
**Fanatically Tested** - 100% line/branch/function/statement coverage, plus mutation testing and E2E tests against real nginx/Envoy/Traefik containers. ~
|
|
16
|
+
**Fanatically Tested** - 100% line/branch/function/statement coverage, plus mutation testing and E2E tests against real nginx/Envoy/Traefik containers. ~5,224 lines of test code for ~788 lines of source (measured by [cloc](https://github.com/AlDanial/cloc)).
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
18
19
|
|
|
@@ -171,7 +172,7 @@ Returns Express middleware.
|
|
|
171
172
|
|
|
172
173
|
| Name | Type | Description |
|
|
173
174
|
|------|------|-------------|
|
|
174
|
-
| `callback` | `(cert, req?) => boolean \|
|
|
175
|
+
| `callback` | `(cert, req?) => boolean \| PromiseLike<boolean>` | Receives the client certificate and request, returns `true` to allow access |
|
|
175
176
|
| `options.certificateSource` | `string` | Use a preset for a known proxy: `'aws-alb'`, `'envoy'`, `'cloudflare'`, `'traefik'` |
|
|
176
177
|
| `options.certificateHeader` | `string` | Custom header name to read certificate from |
|
|
177
178
|
| `options.headerEncoding` | `string` | Encoding format: `'url-pem'`, `'url-pem-aws'`, `'xfcc'`, `'base64-der'`, `'rfc9440'` |
|
|
@@ -814,6 +815,15 @@ The E2E tests spin up real reverse proxies, generate fresh certificates, and ver
|
|
|
814
815
|
- **When using header-based auth**, ensure your proxy strips certificate headers from external requests
|
|
815
816
|
- Use `verifyHeader`/`verifyValue` as defense-in-depth when using header-based authentication
|
|
816
817
|
|
|
818
|
+
## Upgrading from 1.x
|
|
819
|
+
|
|
820
|
+
v2.0.0 introduced two behavior changes:
|
|
821
|
+
|
|
822
|
+
- **Validation callbacks must return exactly `true`.** Previously any truthy value (`'admin'`, `1`, an object) authorized; now only `true` (or a `Promise`/thenable resolving to `true`) does. Callbacks that returned ad-hoc truthy values (e.g., `return cert.subject.CN`) must be rewritten to explicit `return true` / `return false`.
|
|
823
|
+
- **Header options are validated at construction.** Typos like `certificateSource: 'aws-alp'` now throw at app startup instead of failing at request time.
|
|
824
|
+
|
|
825
|
+
See the [CHANGELOG](./CHANGELOG.md) for the full v2.0.0 entry.
|
|
826
|
+
|
|
817
827
|
## Troubleshooting
|
|
818
828
|
|
|
819
829
|
### `DEPTH_ZERO_SELF_SIGNED_CERT` error
|
|
@@ -19,6 +19,19 @@ async function loadModule() {
|
|
|
19
19
|
return _default;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Duck-typed thenable check per Promises/A+: an object or function with
|
|
24
|
+
* a callable `.then` method. Matches the set of values `Promise.resolve`
|
|
25
|
+
* natively adopts as thenables.
|
|
26
|
+
* @param {unknown} value
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
function isThenable(value) {
|
|
30
|
+
return value !== null
|
|
31
|
+
&& (typeof value === 'object' || typeof value === 'function')
|
|
32
|
+
&& typeof value.then === 'function';
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
/**
|
|
23
36
|
* Options not supported by the sync CJS wrapper.
|
|
24
37
|
* These require the ESM module's async header parsing.
|
|
@@ -90,6 +103,18 @@ function clientCertificateAuth(callback, options = {}) {
|
|
|
90
103
|
return next(e);
|
|
91
104
|
}
|
|
92
105
|
|
|
106
|
+
// Socket may report authorized: true without exposing TLS methods (mocks,
|
|
107
|
+
// misconfigured non-TLS path that nonetheless sets authorized). Mirrors the
|
|
108
|
+
// guard in the ESM extractor (lib/extractor.js).
|
|
109
|
+
if (typeof req.socket.getPeerCertificate !== 'function') {
|
|
110
|
+
safeCallHook(onRejected, null, req, 'certificate_not_retrievable');
|
|
111
|
+
const e = new Error(
|
|
112
|
+
'Client certificate was authenticated but certificate information could not be retrieved.'
|
|
113
|
+
);
|
|
114
|
+
e.status = 500;
|
|
115
|
+
return next(e);
|
|
116
|
+
}
|
|
117
|
+
|
|
93
118
|
// Obtain certificate details
|
|
94
119
|
const cert = req.socket.getPeerCertificate(includeChain);
|
|
95
120
|
if (!cert || Object.keys(cert).length === 0) {
|
|
@@ -105,7 +130,7 @@ function clientCertificateAuth(callback, options = {}) {
|
|
|
105
130
|
req.clientCertificate = cert;
|
|
106
131
|
|
|
107
132
|
function doneAuthorizing(authorized) {
|
|
108
|
-
if (authorized) {
|
|
133
|
+
if (authorized === true) {
|
|
109
134
|
safeCallHook(onAuthenticated, cert, req);
|
|
110
135
|
return next();
|
|
111
136
|
} else {
|
|
@@ -118,8 +143,8 @@ function clientCertificateAuth(callback, options = {}) {
|
|
|
118
143
|
|
|
119
144
|
try {
|
|
120
145
|
const result = callback(cert, req);
|
|
121
|
-
if (result
|
|
122
|
-
result.then(doneAuthorizing).catch((err) => {
|
|
146
|
+
if (isThenable(result)) {
|
|
147
|
+
Promise.resolve(result).then(doneAuthorizing).catch((err) => {
|
|
123
148
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
124
149
|
if (err.status === undefined) {
|
|
125
150
|
err.status = 401;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Socket } from 'net';
|
|
3
|
+
import type { PeerCertificate, DetailedPeerCertificate } from 'tls';
|
|
3
4
|
import type {
|
|
4
5
|
ClientCertificateAuthOptions as EsmOptions,
|
|
5
6
|
ValidationCallback as EsmValidationCallback,
|
|
@@ -24,17 +25,31 @@ export interface HttpError extends Error {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
|
-
* Extended request object with
|
|
28
|
+
* Extended request object compatible with Node's `http.IncomingMessage`,
|
|
29
|
+
* Express's `Request`, and Connect-style request objects.
|
|
30
|
+
*
|
|
31
|
+
* The socket is typed as the broader `net.Socket` with TLS-specific fields
|
|
32
|
+
* marked optional so the middleware accepts requests from any of those
|
|
33
|
+
* frameworks without a framework-specific type dependency. The runtime
|
|
34
|
+
* guards in the middleware check for `getPeerCertificate` before calling it.
|
|
28
35
|
*/
|
|
29
36
|
export interface ClientCertRequest extends IncomingMessage {
|
|
30
|
-
/** True if connection is over HTTPS (Express-specific) */
|
|
37
|
+
/** True if connection is over HTTPS (Express-specific, optional). */
|
|
31
38
|
secure?: boolean;
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Underlying socket. Typed as the broader `net.Socket` so this interface
|
|
41
|
+
* is satisfied by both Node's `IncomingMessage.socket` and Express's
|
|
42
|
+
* `Request.socket`. TLS-specific fields (`authorized`, `authorizationError`,
|
|
43
|
+
* `getPeerCertificate`) are present at runtime when the request arrived
|
|
44
|
+
* over TLS and are detected by the middleware via runtime feature checks.
|
|
45
|
+
*/
|
|
46
|
+
socket: Socket & {
|
|
47
|
+
/** Whether the client certificate was authorized at the TLS layer. */
|
|
35
48
|
authorized?: boolean;
|
|
36
|
-
/** Error
|
|
37
|
-
authorizationError?: string;
|
|
49
|
+
/** Error from TLS authorization, if any. */
|
|
50
|
+
authorizationError?: Error | string;
|
|
51
|
+
/** TLS getPeerCertificate, present on TLSSocket only. */
|
|
52
|
+
getPeerCertificate?: (detailed?: boolean) => PeerCertificate | DetailedPeerCertificate;
|
|
38
53
|
};
|
|
39
54
|
/**
|
|
40
55
|
* Client certificate attached by clientCertificateAuth middleware.
|
|
@@ -94,7 +109,7 @@ export interface ClientCertificateAuthOptions {
|
|
|
94
109
|
export type ValidationCallback = (
|
|
95
110
|
cert: PeerCertificate | DetailedPeerCertificate,
|
|
96
111
|
req?: ClientCertRequest
|
|
97
|
-
) => boolean |
|
|
112
|
+
) => boolean | PromiseLike<boolean>;
|
|
98
113
|
|
|
99
114
|
export type Middleware = (
|
|
100
115
|
req: ClientCertRequest,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Socket } from 'net';
|
|
3
|
+
import type { PeerCertificate, DetailedPeerCertificate } from 'tls';
|
|
3
4
|
import type { CertificateSource, HeaderEncoding } from './parsers.js';
|
|
4
5
|
|
|
5
6
|
export type { CertificateSource, HeaderEncoding };
|
|
@@ -22,17 +23,31 @@ export interface HttpError extends Error {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
|
-
* Extended request object with
|
|
26
|
+
* Extended request object compatible with Node's `http.IncomingMessage`,
|
|
27
|
+
* Express's `Request`, and Connect-style request objects.
|
|
28
|
+
*
|
|
29
|
+
* The socket is typed as the broader `net.Socket` with TLS-specific fields
|
|
30
|
+
* marked optional so the middleware accepts requests from any of those
|
|
31
|
+
* frameworks without a framework-specific type dependency. The runtime
|
|
32
|
+
* guards in the middleware check for `getPeerCertificate` before calling it.
|
|
26
33
|
*/
|
|
27
34
|
export interface ClientCertRequest extends IncomingMessage {
|
|
28
|
-
/** True if connection is over HTTPS (Express-specific) */
|
|
35
|
+
/** True if connection is over HTTPS (Express-specific, optional). */
|
|
29
36
|
secure?: boolean;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Underlying socket. Typed as the broader `net.Socket` so this interface
|
|
39
|
+
* is satisfied by both Node's `IncomingMessage.socket` and Express's
|
|
40
|
+
* `Request.socket`. TLS-specific fields (`authorized`, `authorizationError`,
|
|
41
|
+
* `getPeerCertificate`) are present at runtime when the request arrived
|
|
42
|
+
* over TLS and are detected by the middleware via runtime feature checks.
|
|
43
|
+
*/
|
|
44
|
+
socket: Socket & {
|
|
45
|
+
/** Whether the client certificate was authorized at the TLS layer. */
|
|
33
46
|
authorized?: boolean;
|
|
34
|
-
/** Error
|
|
35
|
-
authorizationError?: string;
|
|
47
|
+
/** Error from TLS authorization, if any. */
|
|
48
|
+
authorizationError?: Error | string;
|
|
49
|
+
/** TLS getPeerCertificate, present on TLSSocket only. */
|
|
50
|
+
getPeerCertificate?: (detailed?: boolean) => PeerCertificate | DetailedPeerCertificate;
|
|
36
51
|
};
|
|
37
52
|
/**
|
|
38
53
|
* Client certificate attached by clientCertificateAuth middleware.
|
|
@@ -91,6 +106,7 @@ export interface ClientCertificateAuthOptions {
|
|
|
91
106
|
/**
|
|
92
107
|
* Expected value indicating successful certificate verification.
|
|
93
108
|
* If verifyHeader is set, requests are rejected unless the header matches this value.
|
|
109
|
+
* Comparison is exact (case-sensitive, no whitespace trimming).
|
|
94
110
|
* Example: 'SUCCESS' for nginx.
|
|
95
111
|
*/
|
|
96
112
|
verifyValue?: string;
|
|
@@ -123,7 +139,7 @@ export interface ClientCertificateAuthOptions {
|
|
|
123
139
|
export type ValidationCallback = (
|
|
124
140
|
cert: PeerCertificate | DetailedPeerCertificate,
|
|
125
141
|
req?: ClientCertRequest
|
|
126
|
-
) => boolean |
|
|
142
|
+
) => boolean | PromiseLike<boolean>;
|
|
127
143
|
|
|
128
144
|
export type Middleware = (
|
|
129
145
|
req: ClientCertRequest,
|
|
@@ -135,7 +151,8 @@ export type Middleware = (
|
|
|
135
151
|
* Express/Connect middleware for client SSL certificate authentication.
|
|
136
152
|
*
|
|
137
153
|
* @param callback - Validation function that receives the client certificate
|
|
138
|
-
* and returns true/false (sync) or `
|
|
154
|
+
* and returns true/false (sync) or `PromiseLike<boolean>` (async,
|
|
155
|
+
* including native Promises and any thenable resolving to a boolean).
|
|
139
156
|
* @param options - Configuration options
|
|
140
157
|
* @returns Express middleware function
|
|
141
158
|
*
|
|
@@ -5,10 +5,23 @@
|
|
|
5
5
|
* @license MIT
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { extractClientCertificate } from './extractor.js';
|
|
8
|
+
import { extractClientCertificate, validateExtractorOptions } from './extractor.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Duck-typed thenable check per Promises/A+: an object or function with
|
|
12
|
+
* a callable `.then` method. Matches the set of values `Promise.resolve`
|
|
13
|
+
* natively adopts as thenables.
|
|
14
|
+
* @param {unknown} value
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
function isThenable(value) {
|
|
18
|
+
return value !== null
|
|
19
|
+
&& (typeof value === 'object' || typeof value === 'function')
|
|
20
|
+
&& typeof /** @type {{then?: unknown}} */ (value).then === 'function';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @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
|
|
12
25
|
* @typedef {import('http').ServerResponse & { redirect: (statusOrUrl: number | string, url?: string) => void }} ClientCertResponse
|
|
13
26
|
* @typedef {(req: ClientCertRequest, res: ClientCertResponse, next: (err?: Error) => void) => void} Middleware
|
|
14
27
|
*/
|
|
@@ -33,6 +46,7 @@ import { extractClientCertificate } from './extractor.js';
|
|
|
33
46
|
* upstream proxy (e.g., 'X-SSL-Client-Verify'). Must be used with verifyValue.
|
|
34
47
|
* @property {string} [verifyValue] - Expected value indicating successful verification (e.g., 'SUCCESS').
|
|
35
48
|
* If verifyHeader is set, requests are rejected unless the header matches this value.
|
|
49
|
+
* Comparison is exact (case-sensitive, no whitespace trimming); set this to the exact string your proxy emits.
|
|
36
50
|
* @property {(cert: import('tls').PeerCertificate, req: ClientCertRequest) => void | Promise<void>} [onAuthenticated] -
|
|
37
51
|
* Called when a client is successfully authenticated. Fire-and-forget.
|
|
38
52
|
* @property {(cert: import('tls').PeerCertificate | null, req: ClientCertRequest, reason: string) => void | Promise<void>} [onRejected] -
|
|
@@ -44,12 +58,13 @@ import { extractClientCertificate } from './extractor.js';
|
|
|
44
58
|
* passed the client certificate information for additional validation.
|
|
45
59
|
*
|
|
46
60
|
* The callback receives the certificate (as obtained through
|
|
47
|
-
* `req.socket.getPeerCertificate()` or extracted from headers) and must
|
|
48
|
-
* return `true` (or a
|
|
61
|
+
* `req.socket.getPeerCertificate()` or extracted from headers) and must
|
|
62
|
+
* return `true` (or a thenable resolving to `true`) for the request to proceed.
|
|
49
63
|
*
|
|
50
|
-
* @param {(cert: import('tls').PeerCertificate, req: ClientCertRequest) => boolean |
|
|
64
|
+
* @param {(cert: import('tls').PeerCertificate, req: ClientCertRequest) => boolean | PromiseLike<boolean>} callback
|
|
51
65
|
* Validation function that receives the client certificate and the request
|
|
52
|
-
* object. Returns true/false (sync) or a `
|
|
66
|
+
* object. Returns true/false (sync) or a `PromiseLike<boolean>` (async,
|
|
67
|
+
* including native Promises and any thenable resolving to a boolean) to
|
|
53
68
|
* allow/deny access.
|
|
54
69
|
* @param {ClientCertificateAuthOptions} [options={}]
|
|
55
70
|
* @returns {Middleware}
|
|
@@ -76,6 +91,8 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
76
91
|
throw new TypeError('client-certificate-auth: callback must be a function');
|
|
77
92
|
}
|
|
78
93
|
|
|
94
|
+
validateExtractorOptions(options);
|
|
95
|
+
|
|
79
96
|
const {
|
|
80
97
|
certificateSource,
|
|
81
98
|
certificateHeader,
|
|
@@ -88,12 +105,6 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
88
105
|
onRejected,
|
|
89
106
|
} = options;
|
|
90
107
|
|
|
91
|
-
if ((verifyHeader && !verifyValue) || (!verifyHeader && verifyValue)) {
|
|
92
|
-
throw new Error(
|
|
93
|
-
'client-certificate-auth: verifyHeader and verifyValue must both be provided together, or both omitted'
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
108
|
/**
|
|
98
109
|
* Safely call a hook function without blocking or throwing.
|
|
99
110
|
* Deferred via queueMicrotask to ensure truly non-blocking behavior.
|
|
@@ -161,10 +172,10 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
161
172
|
req.clientCertificate = cert;
|
|
162
173
|
|
|
163
174
|
/**
|
|
164
|
-
* @param {
|
|
175
|
+
* @param {unknown} authorized
|
|
165
176
|
*/
|
|
166
177
|
function doneAuthorizing(authorized) {
|
|
167
|
-
if (authorized) {
|
|
178
|
+
if (authorized === true) {
|
|
168
179
|
safeCallHook(onAuthenticated, cert, req);
|
|
169
180
|
return next();
|
|
170
181
|
} else {
|
|
@@ -177,8 +188,8 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
177
188
|
|
|
178
189
|
try {
|
|
179
190
|
const callbackResult = callback(cert, req);
|
|
180
|
-
if (callbackResult
|
|
181
|
-
callbackResult.then(doneAuthorizing).catch((err) => {
|
|
191
|
+
if (isThenable(callbackResult)) {
|
|
192
|
+
Promise.resolve(callbackResult).then(doneAuthorizing).catch((err) => {
|
|
182
193
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
183
194
|
if (err.status === undefined) {
|
|
184
195
|
err.status = 401;
|
package/lib/extractor.d.ts
CHANGED
|
@@ -58,6 +58,13 @@ export function extractClientCertificate(req: {
|
|
|
58
58
|
getPeerCertificate?: (detailed: boolean) => import("tls").PeerCertificate;
|
|
59
59
|
};
|
|
60
60
|
}, options?: ExtractorOptions): ExtractionResult;
|
|
61
|
+
/**
|
|
62
|
+
* Validate options shared by `extractClientCertificate` and the middleware constructor.
|
|
63
|
+
* Throws on unknown `certificateSource`, unknown `headerEncoding`,
|
|
64
|
+
* `certificateHeader` without an encoding source, or only one of `verifyHeader`/`verifyValue`.
|
|
65
|
+
* Omitted or `undefined` options are treated as the empty object; explicit `null` throws.
|
|
66
|
+
*/
|
|
67
|
+
export function validateExtractorOptions(options?: ExtractorOptions): void;
|
|
61
68
|
export type ExtractionResult = {
|
|
62
69
|
/**
|
|
63
70
|
* - Whether extraction succeeded
|
package/lib/extractor.js
CHANGED
|
@@ -4,7 +4,47 @@
|
|
|
4
4
|
* @license MIT
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { getCertificateFromHeaders } from './parsers.js';
|
|
7
|
+
import { getCertificateFromHeaders, PRESETS } from './parsers.js';
|
|
8
|
+
|
|
9
|
+
const VALID_ENCODINGS = ['url-pem', 'url-pem-aws', 'xfcc', 'base64-der', 'rfc9440'];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate options shared by `extractClientCertificate` and the middleware
|
|
13
|
+
* constructor. Throws on misconfiguration (unknown preset, unknown encoding,
|
|
14
|
+
* `certificateHeader` without an encoding source, or only one of
|
|
15
|
+
* `verifyHeader`/`verifyValue`). Omitted or `undefined` options are treated
|
|
16
|
+
* as the empty object; explicit `null` will throw a `TypeError`.
|
|
17
|
+
*
|
|
18
|
+
* @param {ExtractorOptions} [options]
|
|
19
|
+
* @throws {Error} when options are malformed
|
|
20
|
+
*/
|
|
21
|
+
export function validateExtractorOptions(options = {}) {
|
|
22
|
+
const { certificateSource, certificateHeader, headerEncoding, verifyHeader, verifyValue } = options;
|
|
23
|
+
|
|
24
|
+
if ((verifyHeader && !verifyValue) || (!verifyHeader && verifyValue)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'client-certificate-auth: verifyHeader and verifyValue must both be provided together, or both omitted'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (certificateSource && !Object.hasOwn(PRESETS, certificateSource)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`client-certificate-auth: unknown certificateSource '${certificateSource}'. Valid values: ${Object.keys(PRESETS).join(', ')}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (headerEncoding && !VALID_ENCODINGS.includes(headerEncoding)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`client-certificate-auth: unknown headerEncoding '${headerEncoding}'. Valid values: ${VALID_ENCODINGS.join(', ')}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (certificateHeader && !certificateSource && !headerEncoding) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'client-certificate-auth: certificateHeader requires headerEncoding (or a certificateSource preset that supplies one)'
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
8
48
|
|
|
9
49
|
/**
|
|
10
50
|
* @typedef {Object} ExtractionResult
|
|
@@ -66,6 +106,8 @@ import { getCertificateFromHeaders } from './parsers.js';
|
|
|
66
106
|
* });
|
|
67
107
|
*/
|
|
68
108
|
export function extractClientCertificate(req, options = {}) {
|
|
109
|
+
validateExtractorOptions(options);
|
|
110
|
+
|
|
69
111
|
const {
|
|
70
112
|
certificateSource,
|
|
71
113
|
certificateHeader,
|
|
@@ -76,13 +118,6 @@ export function extractClientCertificate(req, options = {}) {
|
|
|
76
118
|
verifyValue,
|
|
77
119
|
} = options;
|
|
78
120
|
|
|
79
|
-
// Validate verifyHeader/verifyValue pairing
|
|
80
|
-
if ((verifyHeader && !verifyValue) || (!verifyHeader && verifyValue)) {
|
|
81
|
-
throw new Error(
|
|
82
|
-
'extractClientCertificate: verifyHeader and verifyValue must both be provided together, or both omitted'
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
121
|
const useHeaders = Boolean(certificateSource || certificateHeader);
|
|
87
122
|
let cert = null;
|
|
88
123
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "client-certificate-auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.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": {
|
|
@@ -75,12 +75,13 @@
|
|
|
75
75
|
"@arethetypeswrong/cli": "^0.18.2",
|
|
76
76
|
"@commitlint/cli": "^20.4.3",
|
|
77
77
|
"@commitlint/config-conventional": "^20.5.0",
|
|
78
|
+
"@eslint/js": "^10.0.1",
|
|
78
79
|
"@stryker-mutator/core": "^9.6.0",
|
|
79
80
|
"@stryker-mutator/jest-runner": "^9.6.1",
|
|
80
81
|
"@types/express": "^5.0.6",
|
|
81
82
|
"@types/node": "^25.6.0",
|
|
82
83
|
"c8": "^11.0.0",
|
|
83
|
-
"eslint": "^
|
|
84
|
+
"eslint": "^10.2.1",
|
|
84
85
|
"globals": "^17.5.0",
|
|
85
86
|
"husky": "^9.1.7",
|
|
86
87
|
"jest": "^30.3.0",
|