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 \| Promise<boolean>` | Receives the client certificate and request, returns `true` to allow access |
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 instanceof Promise) {
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 | Promise<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 | Promise<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 `Promise<boolean>` (async).
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 Promise resolving to `true`) for the request to proceed.
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 | Promise<boolean>} callback
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 `Promise<boolean>` (async) to
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 {boolean} authorized
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 instanceof Promise) {
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": "1.3.4",
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": "^9.39.4",
83
+ "eslint": "^10.2.1",
83
84
  "globals": "^17.5.0",
84
85
  "husky": "^9.1.7",
85
86
  "jest": "^30.3.0",