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.
@@ -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 instanceof Promise) {
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 instanceof Promise) {
122
- result.then(doneAuthorizing).catch((err) => {
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 (err) {
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 instanceof Promise) {
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 instanceof Promise) {
181
- callbackResult.then(doneAuthorizing).catch((err) => {
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 (err) {
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:...' // matched against cert.fingerprint (SHA-1)
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.toUpperCase())) {return true;}
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.toUpperCase())) {return true;}
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.5",
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": {