client-certificate-auth 1.3.4 → 2.0.0
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
|
@@ -171,7 +171,7 @@ Returns Express middleware.
|
|
|
171
171
|
|
|
172
172
|
| Name | Type | Description |
|
|
173
173
|
|------|------|-------------|
|
|
174
|
-
| `callback` | `(cert, req?) => boolean \|
|
|
174
|
+
| `callback` | `(cert, req?) => boolean \| PromiseLike<boolean>` | Receives the client certificate and request, returns `true` to allow access |
|
|
175
175
|
| `options.certificateSource` | `string` | Use a preset for a known proxy: `'aws-alb'`, `'envoy'`, `'cloudflare'`, `'traefik'` |
|
|
176
176
|
| `options.certificateHeader` | `string` | Custom header name to read certificate from |
|
|
177
177
|
| `options.headerEncoding` | `string` | Encoding format: `'url-pem'`, `'url-pem-aws'`, `'xfcc'`, `'base64-der'`, `'rfc9440'` |
|
|
@@ -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.
|
|
@@ -106,7 +119,7 @@ function clientCertificateAuth(callback, options = {}) {
|
|
|
106
119
|
req.clientCertificate = cert;
|
|
107
120
|
|
|
108
121
|
function doneAuthorizing(authorized) {
|
|
109
|
-
if (authorized) {
|
|
122
|
+
if (authorized === true) {
|
|
110
123
|
safeCallHook(onAuthenticated, cert, req);
|
|
111
124
|
return next();
|
|
112
125
|
} else {
|
|
@@ -119,8 +132,8 @@ function clientCertificateAuth(callback, options = {}) {
|
|
|
119
132
|
|
|
120
133
|
try {
|
|
121
134
|
const result = callback(cert, req);
|
|
122
|
-
if (result
|
|
123
|
-
result.then(doneAuthorizing).catch((err) => {
|
|
135
|
+
if (isThenable(result)) {
|
|
136
|
+
Promise.resolve(result).then(doneAuthorizing).catch((err) => {
|
|
124
137
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
125
138
|
if (err.status === undefined) {
|
|
126
139
|
err.status = 401;
|
|
@@ -94,7 +94,7 @@ export interface ClientCertificateAuthOptions {
|
|
|
94
94
|
export type ValidationCallback = (
|
|
95
95
|
cert: PeerCertificate | DetailedPeerCertificate,
|
|
96
96
|
req?: ClientCertRequest
|
|
97
|
-
) => boolean |
|
|
97
|
+
) => boolean | PromiseLike<boolean>;
|
|
98
98
|
|
|
99
99
|
export type Middleware = (
|
|
100
100
|
req: ClientCertRequest,
|
|
@@ -123,7 +123,7 @@ export interface ClientCertificateAuthOptions {
|
|
|
123
123
|
export type ValidationCallback = (
|
|
124
124
|
cert: PeerCertificate | DetailedPeerCertificate,
|
|
125
125
|
req?: ClientCertRequest
|
|
126
|
-
) => boolean |
|
|
126
|
+
) => boolean | PromiseLike<boolean>;
|
|
127
127
|
|
|
128
128
|
export type Middleware = (
|
|
129
129
|
req: ClientCertRequest,
|
|
@@ -135,7 +135,8 @@ export type Middleware = (
|
|
|
135
135
|
* Express/Connect middleware for client SSL certificate authentication.
|
|
136
136
|
*
|
|
137
137
|
* @param callback - Validation function that receives the client certificate
|
|
138
|
-
* and returns true/false (sync) or `
|
|
138
|
+
* and returns true/false (sync) or `PromiseLike<boolean>` (async,
|
|
139
|
+
* including native Promises and any thenable resolving to a boolean).
|
|
139
140
|
* @param options - Configuration options
|
|
140
141
|
* @returns Express middleware function
|
|
141
142
|
*
|
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { extractClientCertificate } from './extractor.js';
|
|
9
|
+
import { PRESETS } from './parsers.js';
|
|
10
|
+
|
|
11
|
+
const VALID_ENCODINGS = ['url-pem', 'url-pem-aws', 'xfcc', 'base64-der', 'rfc9440'];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Duck-typed thenable check per Promises/A+: an object or function with
|
|
15
|
+
* a callable `.then` method. Matches the set of values `Promise.resolve`
|
|
16
|
+
* natively adopts as thenables.
|
|
17
|
+
* @param {unknown} value
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function isThenable(value) {
|
|
21
|
+
return value !== null
|
|
22
|
+
&& (typeof value === 'object' || typeof value === 'function')
|
|
23
|
+
&& typeof /** @type {{then?: unknown}} */ (value).then === 'function';
|
|
24
|
+
}
|
|
9
25
|
|
|
10
26
|
/**
|
|
11
27
|
* @typedef {import('http').IncomingMessage & { secure?: boolean; socket: import('tls').TLSSocket & { authorized?: boolean; authorizationError?: string }; clientCertificate?: import('tls').PeerCertificate }} ClientCertRequest
|
|
@@ -44,12 +60,13 @@ import { extractClientCertificate } from './extractor.js';
|
|
|
44
60
|
* passed the client certificate information for additional validation.
|
|
45
61
|
*
|
|
46
62
|
* The callback receives the certificate (as obtained through
|
|
47
|
-
* `req.socket.getPeerCertificate()` or extracted from headers) and must
|
|
48
|
-
* return `true` (or a
|
|
63
|
+
* `req.socket.getPeerCertificate()` or extracted from headers) and must
|
|
64
|
+
* return `true` (or a thenable resolving to `true`) for the request to proceed.
|
|
49
65
|
*
|
|
50
|
-
* @param {(cert: import('tls').PeerCertificate, req: ClientCertRequest) => boolean |
|
|
66
|
+
* @param {(cert: import('tls').PeerCertificate, req: ClientCertRequest) => boolean | PromiseLike<boolean>} callback
|
|
51
67
|
* Validation function that receives the client certificate and the request
|
|
52
|
-
* object. Returns true/false (sync) or a `
|
|
68
|
+
* object. Returns true/false (sync) or a `PromiseLike<boolean>` (async,
|
|
69
|
+
* including native Promises and any thenable resolving to a boolean) to
|
|
53
70
|
* allow/deny access.
|
|
54
71
|
* @param {ClientCertificateAuthOptions} [options={}]
|
|
55
72
|
* @returns {Middleware}
|
|
@@ -94,6 +111,24 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
94
111
|
);
|
|
95
112
|
}
|
|
96
113
|
|
|
114
|
+
if (certificateSource && !Object.hasOwn(PRESETS, certificateSource)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`client-certificate-auth: unknown certificateSource '${certificateSource}'. Valid values: ${Object.keys(PRESETS).join(', ')}`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (headerEncoding && !VALID_ENCODINGS.includes(headerEncoding)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`client-certificate-auth: unknown headerEncoding '${headerEncoding}'. Valid values: ${VALID_ENCODINGS.join(', ')}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (certificateHeader && !certificateSource && !headerEncoding) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
'client-certificate-auth: certificateHeader requires headerEncoding (or a certificateSource preset that supplies one)'
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
97
132
|
/**
|
|
98
133
|
* Safely call a hook function without blocking or throwing.
|
|
99
134
|
* Deferred via queueMicrotask to ensure truly non-blocking behavior.
|
|
@@ -165,10 +200,10 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
165
200
|
req.clientCertificate = cert;
|
|
166
201
|
|
|
167
202
|
/**
|
|
168
|
-
* @param {
|
|
203
|
+
* @param {unknown} authorized
|
|
169
204
|
*/
|
|
170
205
|
function doneAuthorizing(authorized) {
|
|
171
|
-
if (authorized) {
|
|
206
|
+
if (authorized === true) {
|
|
172
207
|
safeCallHook(onAuthenticated, cert, req);
|
|
173
208
|
return next();
|
|
174
209
|
} else {
|
|
@@ -181,8 +216,8 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
181
216
|
|
|
182
217
|
try {
|
|
183
218
|
const callbackResult = callback(cert, req);
|
|
184
|
-
if (callbackResult
|
|
185
|
-
callbackResult.then(doneAuthorizing).catch((err) => {
|
|
219
|
+
if (isThenable(callbackResult)) {
|
|
220
|
+
Promise.resolve(callbackResult).then(doneAuthorizing).catch((err) => {
|
|
186
221
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
187
222
|
if (err.status === undefined) {
|
|
188
223
|
err.status = 401;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "client-certificate-auth",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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": {
|
|
@@ -74,12 +74,13 @@
|
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@commitlint/cli": "^20.4.3",
|
|
76
76
|
"@commitlint/config-conventional": "^20.5.0",
|
|
77
|
+
"@eslint/js": "^10.0.1",
|
|
77
78
|
"@stryker-mutator/core": "^9.6.0",
|
|
78
79
|
"@stryker-mutator/jest-runner": "^9.6.1",
|
|
79
80
|
"@types/express": "^5.0.6",
|
|
80
81
|
"@types/node": "^25.6.0",
|
|
81
82
|
"c8": "^11.0.0",
|
|
82
|
-
"eslint": "^
|
|
83
|
+
"eslint": "^10.2.1",
|
|
83
84
|
"globals": "^17.5.0",
|
|
84
85
|
"husky": "^9.1.7",
|
|
85
86
|
"jest": "^30.3.0",
|