client-certificate-auth 1.3.5 → 1.3.7
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/lib/clientCertificateAuth.cjs +33 -5
- package/lib/clientCertificateAuth.js +33 -5
- package/lib/helpers.js +9 -6
- package/package.json +1 -1
|
@@ -19,6 +19,32 @@ 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
|
+
|
|
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
|
+
|
|
22
48
|
/**
|
|
23
49
|
* Options not supported by the sync CJS wrapper.
|
|
24
50
|
* These require the ESM module's async header parsing.
|
|
@@ -72,8 +98,8 @@ function clientCertificateAuth(callback, options = {}) {
|
|
|
72
98
|
queueMicrotask(() => {
|
|
73
99
|
try {
|
|
74
100
|
const result = hook(...args);
|
|
75
|
-
if (result
|
|
76
|
-
result.catch(err => console.error('client-certificate-auth: hook error:', err));
|
|
101
|
+
if (isThenable(result)) {
|
|
102
|
+
Promise.resolve(result).catch(err => console.error('client-certificate-auth: hook error:', err));
|
|
77
103
|
}
|
|
78
104
|
} catch (err) {
|
|
79
105
|
console.error('client-certificate-auth: hook error:', err);
|
|
@@ -118,8 +144,9 @@ function clientCertificateAuth(callback, options = {}) {
|
|
|
118
144
|
|
|
119
145
|
try {
|
|
120
146
|
const result = callback(cert, req);
|
|
121
|
-
if (result
|
|
122
|
-
result.then(doneAuthorizing).catch((
|
|
147
|
+
if (isThenable(result)) {
|
|
148
|
+
Promise.resolve(result).then(doneAuthorizing).catch((rejection) => {
|
|
149
|
+
const err = normalizeCallbackError(rejection);
|
|
123
150
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
124
151
|
if (err.status === undefined) {
|
|
125
152
|
err.status = 401;
|
|
@@ -129,7 +156,8 @@ function clientCertificateAuth(callback, options = {}) {
|
|
|
129
156
|
} else {
|
|
130
157
|
doneAuthorizing(result);
|
|
131
158
|
}
|
|
132
|
-
} catch (
|
|
159
|
+
} catch (thrown) {
|
|
160
|
+
const err = normalizeCallbackError(thrown);
|
|
133
161
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
134
162
|
if (err.status === undefined) {
|
|
135
163
|
err.status = 401;
|
|
@@ -7,6 +7,32 @@
|
|
|
7
7
|
|
|
8
8
|
import { extractClientCertificate } from './extractor.js';
|
|
9
9
|
|
|
10
|
+
/**
|
|
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
|
+
* 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
|
+
|
|
10
36
|
/**
|
|
11
37
|
* @typedef {import('http').IncomingMessage & { secure?: boolean; socket: import('tls').TLSSocket & { authorized?: boolean; authorizationError?: string }; clientCertificate?: import('tls').PeerCertificate }} ClientCertRequest
|
|
12
38
|
* @typedef {import('http').ServerResponse & { redirect: (statusOrUrl: number | string, url?: string) => void }} ClientCertResponse
|
|
@@ -107,8 +133,8 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
107
133
|
queueMicrotask(() => {
|
|
108
134
|
try {
|
|
109
135
|
const result = hook(...args);
|
|
110
|
-
if (result
|
|
111
|
-
result.catch(err => console.error('client-certificate-auth: hook error:', err));
|
|
136
|
+
if (isThenable(result)) {
|
|
137
|
+
Promise.resolve(result).catch(err => console.error('client-certificate-auth: hook error:', err));
|
|
112
138
|
}
|
|
113
139
|
} catch (err) {
|
|
114
140
|
console.error('client-certificate-auth: hook error:', err);
|
|
@@ -177,8 +203,9 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
177
203
|
|
|
178
204
|
try {
|
|
179
205
|
const callbackResult = callback(cert, req);
|
|
180
|
-
if (callbackResult
|
|
181
|
-
callbackResult.then(doneAuthorizing).catch((
|
|
206
|
+
if (isThenable(callbackResult)) {
|
|
207
|
+
Promise.resolve(callbackResult).then(doneAuthorizing).catch((rejection) => {
|
|
208
|
+
const err = normalizeCallbackError(rejection);
|
|
182
209
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
183
210
|
if (err.status === undefined) {
|
|
184
211
|
err.status = 401;
|
|
@@ -188,7 +215,8 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
188
215
|
} else {
|
|
189
216
|
doneAuthorizing(callbackResult);
|
|
190
217
|
}
|
|
191
|
-
} catch (
|
|
218
|
+
} catch (thrown) {
|
|
219
|
+
const err = normalizeCallbackError(thrown);
|
|
192
220
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
193
221
|
if (err.status === undefined) {
|
|
194
222
|
err.status = 401;
|
package/lib/helpers.js
CHANGED
|
@@ -38,7 +38,8 @@ export function allowCN(names) {
|
|
|
38
38
|
* Create a validation callback that allows certificates with matching fingerprints.
|
|
39
39
|
* Supports SHA-1 fingerprints (compared against cert.fingerprint) and SHA-256
|
|
40
40
|
* fingerprints with "SHA256:" prefix (compared against cert.fingerprint256).
|
|
41
|
-
* Fingerprints without a prefix are treated as SHA-1.
|
|
41
|
+
* Fingerprints without a prefix are treated as SHA-1. Hex inputs are
|
|
42
|
+
* normalized: case and colon delimiters are ignored on both sides.
|
|
42
43
|
*
|
|
43
44
|
* @param {string[]} fingerprints - Allowed fingerprints
|
|
44
45
|
* @returns {ValidationCallback}
|
|
@@ -46,28 +47,30 @@ export function allowCN(names) {
|
|
|
46
47
|
* @example
|
|
47
48
|
* app.use(clientCertificateAuth(allowFingerprints([
|
|
48
49
|
* 'SHA256:AB:CD:EF:...', // matched against cert.fingerprint256
|
|
49
|
-
* 'AB:CD:EF:...'
|
|
50
|
+
* 'AB:CD:EF:...', // colon-delimited, matched against cert.fingerprint
|
|
51
|
+
* 'ABCDEF...' // contiguous hex also matches cert.fingerprint
|
|
50
52
|
* ])));
|
|
51
53
|
*/
|
|
52
54
|
export function allowFingerprints(fingerprints) {
|
|
55
|
+
const normalize = (fp) => fp.toUpperCase().replace(/:/g, '');
|
|
53
56
|
const sha256Allowed = new Set();
|
|
54
57
|
const sha1Allowed = new Set();
|
|
55
58
|
|
|
56
59
|
for (const fp of fingerprints) {
|
|
57
60
|
const upper = fp.toUpperCase();
|
|
58
61
|
if (upper.startsWith('SHA256:')) {
|
|
59
|
-
sha256Allowed.add(upper.slice(7));
|
|
62
|
+
sha256Allowed.add(normalize(upper.slice(7)));
|
|
60
63
|
} else {
|
|
61
|
-
sha1Allowed.add(upper);
|
|
64
|
+
sha1Allowed.add(normalize(upper));
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
return (cert) => {
|
|
66
69
|
if (sha1Allowed.size > 0 && cert.fingerprint) {
|
|
67
|
-
if (sha1Allowed.has(cert.fingerprint
|
|
70
|
+
if (sha1Allowed.has(normalize(cert.fingerprint))) {return true;}
|
|
68
71
|
}
|
|
69
72
|
if (sha256Allowed.size > 0 && cert.fingerprint256) {
|
|
70
|
-
if (sha256Allowed.has(cert.fingerprint256
|
|
73
|
+
if (sha256Allowed.has(normalize(cert.fingerprint256))) {return true;}
|
|
71
74
|
}
|
|
72
75
|
return false;
|
|
73
76
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "client-certificate-auth",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.7",
|
|
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": {
|