client-certificate-auth 1.3.4 → 1.3.6
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 +18 -6
- package/lib/clientCertificateAuth.js +21 -12
- package/lib/helpers.js +9 -6
- package/package.json +6 -2
|
@@ -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.
|
|
@@ -72,8 +85,8 @@ function clientCertificateAuth(callback, options = {}) {
|
|
|
72
85
|
queueMicrotask(() => {
|
|
73
86
|
try {
|
|
74
87
|
const result = hook(...args);
|
|
75
|
-
if (result
|
|
76
|
-
result.catch(err => console.error('client-certificate-auth: hook error:', err));
|
|
88
|
+
if (isThenable(result)) {
|
|
89
|
+
Promise.resolve(result).catch(err => console.error('client-certificate-auth: hook error:', err));
|
|
77
90
|
}
|
|
78
91
|
} catch (err) {
|
|
79
92
|
console.error('client-certificate-auth: hook error:', err);
|
|
@@ -85,8 +98,7 @@ function clientCertificateAuth(callback, options = {}) {
|
|
|
85
98
|
// Ensure that the certificate was validated at the protocol level
|
|
86
99
|
if (!req.socket?.authorized) {
|
|
87
100
|
safeCallHook(onRejected, null, req, 'socket_not_authorized');
|
|
88
|
-
const
|
|
89
|
-
const e = new Error(`Unauthorized: Client certificate required (${authError})`);
|
|
101
|
+
const e = new Error('Unauthorized: Client certificate required');
|
|
90
102
|
e.status = 401;
|
|
91
103
|
return next(e);
|
|
92
104
|
}
|
|
@@ -119,8 +131,8 @@ function clientCertificateAuth(callback, options = {}) {
|
|
|
119
131
|
|
|
120
132
|
try {
|
|
121
133
|
const result = callback(cert, req);
|
|
122
|
-
if (result
|
|
123
|
-
result.then(doneAuthorizing).catch((err) => {
|
|
134
|
+
if (isThenable(result)) {
|
|
135
|
+
Promise.resolve(result).then(doneAuthorizing).catch((err) => {
|
|
124
136
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
125
137
|
if (err.status === undefined) {
|
|
126
138
|
err.status = 401;
|
|
@@ -7,6 +7,19 @@
|
|
|
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
|
+
|
|
10
23
|
/**
|
|
11
24
|
* @typedef {import('http').IncomingMessage & { secure?: boolean; socket: import('tls').TLSSocket & { authorized?: boolean; authorizationError?: string }; clientCertificate?: import('tls').PeerCertificate }} ClientCertRequest
|
|
12
25
|
* @typedef {import('http').ServerResponse & { redirect: (statusOrUrl: number | string, url?: string) => void }} ClientCertResponse
|
|
@@ -107,8 +120,8 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
107
120
|
queueMicrotask(() => {
|
|
108
121
|
try {
|
|
109
122
|
const result = hook(...args);
|
|
110
|
-
if (result
|
|
111
|
-
result.catch(err => console.error('client-certificate-auth: hook error:', err));
|
|
123
|
+
if (isThenable(result)) {
|
|
124
|
+
Promise.resolve(result).catch(err => console.error('client-certificate-auth: hook error:', err));
|
|
112
125
|
}
|
|
113
126
|
} catch (err) {
|
|
114
127
|
console.error('client-certificate-auth: hook error:', err);
|
|
@@ -136,19 +149,15 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
136
149
|
let statusCode = 401;
|
|
137
150
|
|
|
138
151
|
switch (result.reason) {
|
|
139
|
-
case 'verification_header_mismatch':
|
|
140
|
-
|
|
141
|
-
errorMessage = `Unauthorized: Certificate verification failed (${verifyStatus || 'header missing'})`;
|
|
152
|
+
case 'verification_header_mismatch':
|
|
153
|
+
errorMessage = 'Unauthorized: Certificate verification failed';
|
|
142
154
|
break;
|
|
143
|
-
}
|
|
144
155
|
case 'header_missing_or_malformed':
|
|
145
156
|
errorMessage = 'Unauthorized: Client certificate header missing or malformed';
|
|
146
157
|
break;
|
|
147
|
-
case 'socket_not_authorized':
|
|
148
|
-
|
|
149
|
-
errorMessage = `Unauthorized: Client certificate required (${authError})`;
|
|
158
|
+
case 'socket_not_authorized':
|
|
159
|
+
errorMessage = 'Unauthorized: Client certificate required';
|
|
150
160
|
break;
|
|
151
|
-
}
|
|
152
161
|
case 'certificate_not_retrievable':
|
|
153
162
|
errorMessage = 'Client certificate was authenticated but certificate information could not be retrieved.';
|
|
154
163
|
statusCode = 500;
|
|
@@ -181,8 +190,8 @@ export default function clientCertificateAuth(callback, options = {}) {
|
|
|
181
190
|
|
|
182
191
|
try {
|
|
183
192
|
const callbackResult = callback(cert, req);
|
|
184
|
-
if (callbackResult
|
|
185
|
-
callbackResult.then(doneAuthorizing).catch((err) => {
|
|
193
|
+
if (isThenable(callbackResult)) {
|
|
194
|
+
Promise.resolve(callbackResult).then(doneAuthorizing).catch((err) => {
|
|
186
195
|
safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
|
|
187
196
|
if (err.status === undefined) {
|
|
188
197
|
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.6",
|
|
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": {
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
"node": ">= 20"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
+
"@arethetypeswrong/cli": "^0.18.2",
|
|
75
76
|
"@commitlint/cli": "^20.4.3",
|
|
76
77
|
"@commitlint/config-conventional": "^20.5.0",
|
|
77
78
|
"@stryker-mutator/core": "^9.6.0",
|
|
@@ -142,5 +143,8 @@
|
|
|
142
143
|
"lib/**/*.cjs",
|
|
143
144
|
"lib/**/*.d.ts",
|
|
144
145
|
"lib/**/*.d.cts"
|
|
145
|
-
]
|
|
146
|
+
],
|
|
147
|
+
"overrides": {
|
|
148
|
+
"fflate": "0.8.2"
|
|
149
|
+
}
|
|
146
150
|
}
|