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.
@@ -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 instanceof Promise) {
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 authError = req.socket?.authorizationError || 'unknown';
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 instanceof Promise) {
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 instanceof Promise) {
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
- const verifyStatus = req.headers[verifyHeader.toLowerCase()];
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
- const authError = req.socket?.authorizationError || 'unknown';
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 instanceof Promise) {
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:...' // 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.4",
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
  }