client-certificate-auth 0.2.1 → 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2013-2026 Tony Gies
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,70 +1,470 @@
1
- client-certificate-auth
2
- ========
1
+ # client-certificate-auth
3
2
 
4
- middleware for Node.js implementing client SSL certificate
5
- authentication/authorization
3
+ Express/Connect middleware for client SSL certificate authentication (mTLS).
6
4
 
7
- Copyright © 2013 Tony Gies
5
+ [![CI](https://github.com/tgies/client-certificate-auth/actions/workflows/ci.yml/badge.svg)](https://github.com/tgies/client-certificate-auth/actions/workflows/ci.yml)
6
+ [![npm version](https://badge.fury.io/js/client-certificate-auth.svg)](https://www.npmjs.com/package/client-certificate-auth)
7
+ [![codecov](https://codecov.io/gh/tgies/client-certificate-auth/graph/badge.svg)](https://codecov.io/gh/tgies/client-certificate-auth)
8
8
 
9
- April 30, 2013
9
+ ## Installation
10
10
 
11
- [![Build Status](https://travis-ci.org/tgies/client-certificate-auth.png)](https://travis-ci.org/tgies/client-certificate-auth)
11
+ ```bash
12
+ npm install client-certificate-auth
13
+ ```
14
+
15
+ **Requirements:** Node.js >= 18
16
+
17
+ ## Synopsis
12
18
 
13
- synopsis
14
- --------
19
+ This middleware requires clients to present a valid, verifiable SSL certificate (mutual TLS / mTLS). The certificate is validated at the TLS layer, then passed to your callback for additional authorization logic.
15
20
 
16
- client-certificate-auth provides HTTP middleware for Node.js (in particular
17
- Connect/Express) to require that a valid, verifiable client SSL certificate is
18
- provided, and passes information about that certificate to a callback which must
19
- return `true` for the request to proceed; otherwise, the client is considered
20
- unauthorized and the request is aborted.
21
+ Compatible with Express, Connect, and any Node.js HTTP server framework that uses standard `req.socket` and `req.headers`.
21
22
 
22
- usage
23
- -----
23
+ ## Usage
24
24
 
25
- The https server must be set up to request a client certificate and validate it
26
- against an issuer/CA cert:
25
+ ### Basic Setup
26
+
27
+ Configure your HTTPS server to request and validate client certificates:
27
28
 
28
29
  ```javascript
29
- var express = require('express');
30
- var fs = require('fs');
31
- var https = require('https');
32
- var clientCertificateAuth = require('client-certificate-auth');
30
+ import express from 'express';
31
+ import https from 'node:https';
32
+ import fs from 'node:fs';
33
+ import clientCertificateAuth from 'client-certificate-auth';
33
34
 
34
- var opts = {
35
- key: fs.readFileSync('server.key'),
36
- cert: fs.readFileSync('server.pem'),
37
- ca: fs.readFileSync('cacert.pem'),
38
- requestCert: true,
39
- rejectUnauthorized: false
40
- };
35
+ const app = express();
41
36
 
42
- var app = express();
37
+ // Validate certificate against your authorization rules
38
+ const checkAuth = (cert) => {
39
+ return cert.subject.CN === 'trusted-client';
40
+ };
43
41
 
42
+ // Apply to all routes
44
43
  app.use(clientCertificateAuth(checkAuth));
45
- app.use(app.router);
46
- app.use(function(err, req, res, next) { console.log(err); next(); });
47
44
 
48
- app.get('/', function(req, res) {
45
+ app.get('/', (req, res) => {
49
46
  res.send('Authorized!');
50
47
  });
51
48
 
52
- var checkAuth = function(cert) {
53
- // allow access if certificate subject Common Name is 'Tony Gies'
54
- return cert.subject.CN == 'Tony Gies';
49
+ // HTTPS server configuration
50
+ const opts = {
51
+ key: fs.readFileSync('server.key'),
52
+ cert: fs.readFileSync('server.pem'),
53
+ ca: fs.readFileSync('ca.pem'), // CA that signed client certs
54
+ requestCert: true, // Request client certificate
55
+ rejectUnauthorized: false // Let middleware handle errors
55
56
  };
56
57
 
57
- https.createServer(opts, app).listen(4000);
58
+ https.createServer(opts, app).listen(443);
58
59
  ```
59
60
 
60
- Or secure only certain routes:
61
+ ### Per-Route Protection
61
62
 
62
63
  ```javascript
63
- app.get('/unsecure', function(req, res) {
64
+ app.get('/public', (req, res) => {
64
65
  res.send('Hello world');
65
66
  });
66
67
 
67
- app.get('/secure', clientCertificateAuth(checkAuth), function(req, res) {
68
- res.send('Hello authorized user');
68
+ app.get('/admin', clientCertificateAuth(checkAuth), (req, res) => {
69
+ res.send('Hello admin');
70
+ });
71
+ ```
72
+
73
+ ### Async Authorization
74
+
75
+ ```javascript
76
+ const checkAuth = async (cert) => {
77
+ const user = await db.findByFingerprint(cert.fingerprint);
78
+ return user !== null;
79
+ };
80
+
81
+ app.use(clientCertificateAuth(checkAuth));
82
+ ```
83
+
84
+ ### Custom Error Messages
85
+
86
+ Throw errors for granular authorization feedback instead of returning `false`:
87
+
88
+ ```javascript
89
+ const checkAuth = (cert) => {
90
+ if (isRevoked(cert.serialNumber)) {
91
+ throw new Error('Certificate has been revoked');
92
+ }
93
+ if (!allowlist.includes(cert.fingerprint)) {
94
+ throw new Error('Certificate not in allowlist');
95
+ }
96
+ return true;
97
+ };
98
+
99
+ // Thrown errors are passed to Express error handlers with:
100
+ // - error.message = your custom message
101
+ // - error.status = 401 (unless you set a different status)
102
+ ```
103
+
104
+ To use a different status code, set it on the error before throwing:
105
+
106
+ ```javascript
107
+ const err = new Error('Access forbidden');
108
+ err.status = 403;
109
+ throw err;
110
+ ```
111
+
112
+ ## API
113
+
114
+ ### `clientCertificateAuth(callback, options?)`
115
+
116
+ Returns Express middleware.
117
+
118
+ **Parameters:**
119
+
120
+ | Name | Type | Description |
121
+ |------|------|-------------|
122
+ | `callback` | `(cert) => boolean \| Promise<boolean>` | Receives the client certificate, returns `true` to allow access |
123
+ | `options.certificateSource` | `string` | Use a preset for a known proxy: `'aws-alb'`, `'envoy'`, `'cloudflare'`, `'traefik'` |
124
+ | `options.certificateHeader` | `string` | Custom header name to read certificate from |
125
+ | `options.headerEncoding` | `string` | Encoding format: `'url-pem'`, `'url-pem-aws'`, `'xfcc'`, `'base64-der'`, `'rfc9440'` |
126
+ | `options.fallbackToSocket` | `boolean` | If header extraction fails, try `socket.getPeerCertificate()` (default: `false`) |
127
+ | `options.includeChain` | `boolean` | If `true`, include full certificate chain via `cert.issuerCertificate` (default: `false`) |
128
+ | `options.verifyHeader` | `string` | Header name containing verification status from proxy (e.g., `'X-SSL-Client-Verify'`) |
129
+ | `options.verifyValue` | `string` | Expected value indicating successful verification (e.g., `'SUCCESS'`) |
130
+
131
+ **Certificate Object:**
132
+
133
+ The `cert` parameter contains fields from [`tls.PeerCertificate`](https://nodejs.org/api/tls.html#certificate-object):
134
+
135
+ - `subject.CN` - Common Name
136
+ - `subject.O` - Organization
137
+ - `issuer` - Issuer information
138
+ - `fingerprint` - Certificate fingerprint
139
+ - `valid_from`, `valid_to` - Validity period
140
+ - `issuerCertificate` - Issuer's certificate (only when `includeChain: true`)
141
+
142
+ ### Accessing the Certificate
143
+
144
+ After authentication, the certificate is attached to `req.clientCertificate` for downstream handlers:
145
+
146
+ ```javascript
147
+ app.use(clientCertificateAuth(checkAuth));
148
+
149
+ app.get('/whoami', (req, res) => {
150
+ res.json({
151
+ cn: req.clientCertificate.subject.CN,
152
+ fingerprint: req.clientCertificate.fingerprint
153
+ });
154
+ });
155
+ ```
156
+
157
+ The certificate is attached before the authorization callback runs, so it's available even if authorization fails (useful for logging).
158
+
159
+ ### Certificate Chain Access
160
+
161
+ For enterprise PKI scenarios, you may need to inspect intermediate CAs or the root CA:
162
+
163
+ ```javascript
164
+ app.use(clientCertificateAuth((cert) => {
165
+ // Check issuer's organization
166
+ if (cert.issuerCertificate) {
167
+ return cert.issuerCertificate.subject.O === 'Trusted Root CA';
168
+ }
169
+ return false;
170
+ }, { includeChain: true }));
171
+ ```
172
+
173
+ When `includeChain: true`, the certificate object includes `issuerCertificate` linking to the issuer's certificate (and so on up the chain). This works consistently for both socket-based and header-based extraction.
174
+
175
+ ### User Login
176
+
177
+ Client certificates provide cryptographically-verified identity, making them ideal for user authentication. Map certificate fields to user accounts in your database:
178
+
179
+ ```javascript
180
+ app.use(clientCertificateAuth(async (cert) => {
181
+ // Option 1: Lookup by fingerprint (most secure - immutable per certificate)
182
+ const user = await db.users.findOne({ certFingerprint: cert.fingerprint });
183
+
184
+ // Option 2: Lookup by email (from subject or SAN)
185
+ // const user = await db.users.findOne({ email: cert.subject.emailAddress });
186
+
187
+ // Option 3: Lookup by Common Name
188
+ // const user = await db.users.findOne({ certCN: cert.subject.CN });
189
+
190
+ if (!user) {
191
+ throw new Error('Certificate not registered to any user');
192
+ }
193
+
194
+ return true;
195
+ }));
196
+ ```
197
+
198
+ To make the user available to downstream handlers, attach it to the request:
199
+
200
+ ```javascript
201
+ app.use(clientCertificateAuth(async (cert, req) => {
202
+ const user = await db.users.findOne({ certFingerprint: cert.fingerprint });
203
+ if (!user) throw new Error('Unknown certificate');
204
+
205
+ req.user = user; // Attach for downstream routes
206
+ return true;
207
+ }));
208
+
209
+ app.get('/profile', (req, res) => {
210
+ res.json({
211
+ name: req.user.name,
212
+ certificateCN: req.clientCertificate.subject.CN
213
+ });
214
+ });
215
+ ```
216
+
217
+ **Lookup strategies:**
218
+
219
+ | Field | Pros | Cons |
220
+ |-------|------|------|
221
+ | `fingerprint` | Unique, immutable | Must register each cert |
222
+ | `subject.emailAddress` | Human-readable | Ensure uniqueness |
223
+ | `subject.CN` | Simple to configure | May not be unique |
224
+ | `serialNumber` + issuer | Traceable to your CA | More complex queries |
225
+
226
+ ## Reverse Proxy / Load Balancer Support
227
+
228
+ When your application runs behind a TLS-terminating reverse proxy, the client certificate is available via HTTP headers instead of the TLS socket. This middleware supports reading certificates from headers for common proxies.
229
+
230
+ ### Using Presets
231
+
232
+ For common proxies, use the `certificateSource` option:
233
+
234
+ ```javascript
235
+ // AWS Application Load Balancer
236
+ app.use(clientCertificateAuth(checkAuth, {
237
+ certificateSource: 'aws-alb'
238
+ }));
239
+
240
+ // Envoy / Istio
241
+ app.use(clientCertificateAuth(checkAuth, {
242
+ certificateSource: 'envoy'
243
+ }));
244
+
245
+ // Cloudflare
246
+ app.use(clientCertificateAuth(checkAuth, {
247
+ certificateSource: 'cloudflare'
248
+ }));
249
+
250
+ // Traefik
251
+ app.use(clientCertificateAuth(checkAuth, {
252
+ certificateSource: 'traefik'
253
+ }));
254
+ ```
255
+
256
+ ### Preset Details
257
+
258
+ | Preset | Header | Encoding |
259
+ |--------|--------|----------|
260
+ | `aws-alb` | `X-Amzn-Mtls-Clientcert` | URL-encoded PEM (AWS variant) |
261
+ | `envoy` | `X-Forwarded-Client-Cert` | XFCC structured format |
262
+ | `cloudflare` | `Cf-Client-Cert-Der-Base64` | Base64-encoded DER |
263
+ | `traefik` | `X-Forwarded-Tls-Client-Cert` | Base64-encoded DER |
264
+
265
+ ### Custom Headers
266
+
267
+ For nginx, HAProxy, Google Cloud Load Balancer, or other proxies with configurable headers:
268
+
269
+ ```javascript
270
+ // nginx with $ssl_client_escaped_cert
271
+ app.use(clientCertificateAuth(checkAuth, {
272
+ certificateHeader: 'X-SSL-Whatever-You-Use',
273
+ headerEncoding: 'url-pem'
274
+ }));
275
+
276
+ // Google Cloud Load Balancer (RFC 9440)
277
+ app.use(clientCertificateAuth(checkAuth, {
278
+ certificateHeader: 'X-SSL-Whatever-You-Use',
279
+ headerEncoding: 'rfc9440'
280
+ }));
281
+
282
+ // HAProxy with base64 DER
283
+ app.use(clientCertificateAuth(checkAuth, {
284
+ certificateHeader: 'X-SSL-Whatever-You-Use',
285
+ headerEncoding: 'base64-der'
286
+ }));
287
+ ```
288
+
289
+ ### Encoding Formats
290
+
291
+ | Encoding | Description | Used By |
292
+ |----------|-------------|---------|
293
+ | `url-pem` | URL-encoded PEM certificate | nginx, HAProxy |
294
+ | `url-pem-aws` | URL-encoded PEM (AWS variant, `+` as safe char) | AWS ALB |
295
+ | `xfcc` | Envoy's structured `Key=Value;...` format | Envoy, Istio |
296
+ | `base64-der` | Base64-encoded DER certificate | Cloudflare, Traefik |
297
+ | `rfc9440` | RFC 9440 format: `:base64-der:` | Google Cloud LB |
298
+
299
+ ### Fallback Mode
300
+
301
+ If your proxy might not always forward certificates (e.g., direct connections bypass the proxy), enable fallback:
302
+
303
+ ```javascript
304
+ app.use(clientCertificateAuth(checkAuth, {
305
+ certificateSource: 'aws-alb',
306
+ fallbackToSocket: true // Try socket if header missing
307
+ }));
308
+ ```
309
+
310
+ ### Security Considerations
311
+
312
+ > ⚠️ **Important:** When using header-based authentication, your reverse proxy **must** strip any incoming certificate headers from external requests to prevent spoofing.
313
+
314
+ Configure your proxy to:
315
+ 1. **Strip** the certificate header from incoming requests
316
+ 2. **Set** the header only for authenticated mTLS connections
317
+ 3. **Never** trust certificate headers from untrusted sources
318
+
319
+ #### Verification Header (Defense in Depth)
320
+
321
+ For additional protection, use `verifyHeader` and `verifyValue` to validate that your proxy has actually verified the certificate. This guards against proxy misconfiguration (e.g., `ssl_verify_client optional` passing unverified certs):
322
+
323
+ ```javascript
324
+ app.use(clientCertificateAuth(checkAuth, {
325
+ certificateHeader: 'X-SSL-Client-Cert',
326
+ headerEncoding: 'url-pem',
327
+ verifyHeader: 'X-SSL-Client-Verify',
328
+ verifyValue: 'SUCCESS'
329
+ }));
330
+ ```
331
+
332
+ Example nginx configuration:
333
+ ```nginx
334
+ # Strip any existing headers from clients
335
+ proxy_set_header X-SSL-Client-Cert "";
336
+ proxy_set_header X-SSL-Client-Verify "";
337
+
338
+ # Always send verification status
339
+ proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
340
+
341
+ # Only send cert if verified
342
+ if ($ssl_client_verify = SUCCESS) {
343
+ proxy_set_header X-SSL-Client-Cert $ssl_client_escaped_cert;
344
+ }
345
+ ```
346
+
347
+ ## Authorization Helpers
348
+
349
+ Pre-built validation callbacks for common authorization patterns, available as a separate import:
350
+
351
+ ```javascript
352
+ import clientCertificateAuth from 'client-certificate-auth';
353
+ import { allowCN, allowFingerprints, allowIssuer, allOf, anyOf } from 'client-certificate-auth/helpers';
354
+ ```
355
+
356
+ ### Basic Helpers
357
+
358
+ ```javascript
359
+ // Allowlist by Common Name
360
+ app.use(clientCertificateAuth(allowCN(['service-a', 'service-b'])));
361
+
362
+ // Allowlist by fingerprint
363
+ app.use(clientCertificateAuth(allowFingerprints([
364
+ 'SHA256:AB:CD:EF:...',
365
+ 'AB:CD:EF:...' // SHA256: prefix optional
366
+ ])));
367
+
368
+ // Allowlist by Organization
369
+ app.use(clientCertificateAuth(allowOrganization(['My Company'])));
370
+
371
+ // Allowlist by Organizational Unit
372
+ app.use(clientCertificateAuth(allowOU(['Engineering', 'DevOps'])));
373
+
374
+ // Allowlist by email (checks SAN and subject.emailAddress)
375
+ app.use(clientCertificateAuth(allowEmail(['admin@example.com'])));
376
+
377
+ // Allowlist by serial number
378
+ app.use(clientCertificateAuth(allowSerial(['01:23:45:67:89:AB:CD:EF'])));
379
+
380
+ // Allowlist by Subject Alternative Name
381
+ app.use(clientCertificateAuth(allowSAN(['DNS:api.example.com', 'email:service@example.com'])));
382
+ ```
383
+
384
+ ### Field Matching
385
+
386
+ Match certificates by issuer or subject fields (all specified fields must match):
387
+
388
+ ```javascript
389
+ // Match by issuer
390
+ app.use(clientCertificateAuth(allowIssuer({ O: 'My Company', CN: 'Internal CA' })));
391
+
392
+ // Match by subject
393
+ app.use(clientCertificateAuth(allowSubject({ O: 'Partner Corp', ST: 'California' })));
394
+ ```
395
+
396
+ ### Combining Helpers
397
+
398
+ ```javascript
399
+ // AND - all conditions must pass
400
+ app.use(clientCertificateAuth(allOf(
401
+ allowIssuer({ O: 'My Company' }),
402
+ allowOU(['Engineering', 'DevOps'])
403
+ )));
404
+
405
+ // OR - at least one condition must pass
406
+ app.use(clientCertificateAuth(anyOf(
407
+ allowCN(['admin']),
408
+ allowOU(['Administrators'])
409
+ )));
410
+ ```
411
+
412
+ ### Available Helpers
413
+
414
+ | Helper | Description |
415
+ |--------|-------------|
416
+ | `allowCN(names)` | Match by Common Name |
417
+ | `allowFingerprints(fps)` | Match by certificate fingerprint |
418
+ | `allowIssuer(match)` | Match by issuer fields (partial) |
419
+ | `allowSubject(match)` | Match by subject fields (partial) |
420
+ | `allowOU(ous)` | Match by Organizational Unit |
421
+ | `allowOrganization(orgs)` | Match by Organization |
422
+ | `allowSerial(serials)` | Match by serial number |
423
+ | `allowSAN(values)` | Match by Subject Alternative Name |
424
+ | `allowEmail(emails)` | Match by email (SAN or subject) |
425
+ | `allOf(...callbacks)` | AND combinator |
426
+ | `anyOf(...callbacks)` | OR combinator |
427
+
428
+ ## TypeScript
429
+
430
+ Types are included:
431
+
432
+ ```typescript
433
+ import clientCertificateAuth from 'client-certificate-auth';
434
+ import type { ClientCertRequest } from 'client-certificate-auth';
435
+ import type { PeerCertificate } from 'tls';
436
+
437
+ const checkAuth = (cert: PeerCertificate): boolean => {
438
+ return cert.subject.CN === 'admin';
439
+ };
440
+
441
+ app.use(clientCertificateAuth(checkAuth));
442
+
443
+ // Access certificate in downstream handlers
444
+ app.get('/whoami', (req: ClientCertRequest, res) => {
445
+ res.json({ cn: req.clientCertificate?.subject.CN });
69
446
  });
447
+
448
+ // With reverse proxy
449
+ app.use(clientCertificateAuth(checkAuth, {
450
+ certificateSource: 'aws-alb'
451
+ }));
70
452
  ```
453
+
454
+ ## CommonJS
455
+
456
+ ```javascript
457
+ const clientCertificateAuth = require('client-certificate-auth');
458
+
459
+ app.use(clientCertificateAuth((cert) => cert.subject.CN === 'admin'));
460
+ ```
461
+
462
+ ## Security Notes
463
+
464
+ - Set `rejectUnauthorized: false` on your HTTPS server to let this middleware provide helpful error messages, rather than dropping connections silently
465
+ - **When using header-based auth**, ensure your proxy strips certificate headers from external requests
466
+ - Use `verifyHeader`/`verifyValue` as defense-in-depth when using header-based authentication
467
+
468
+ ## License
469
+
470
+ MIT © Tony Gies
@@ -0,0 +1,115 @@
1
+ /*!
2
+ * client-certificate-auth - CommonJS wrapper
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ // Dynamic import of the ES module
10
+ let _default;
11
+
12
+ async function loadModule() {
13
+ if (!_default) {
14
+ const mod = await import('./clientCertificateAuth.js');
15
+ _default = mod.default;
16
+ }
17
+ return _default;
18
+ }
19
+
20
+ /**
21
+ * Options not supported by the sync CJS wrapper.
22
+ * These require the ESM module's async header parsing.
23
+ */
24
+ const UNSUPPORTED_OPTIONS = [
25
+ 'certificateSource',
26
+ 'certificateHeader',
27
+ 'headerEncoding',
28
+ 'fallbackToSocket',
29
+ 'verifyHeader',
30
+ 'verifyValue'
31
+ ];
32
+
33
+ /**
34
+ * CommonJS wrapper for client-certificate-auth.
35
+ *
36
+ * This sync wrapper supports socket-based certificate extraction only.
37
+ * For full features (header-based extraction, reverse proxy support),
38
+ * use the async loader: `const auth = await require('client-certificate-auth').load();`
39
+ *
40
+ * @param {Function} callback - Validation callback
41
+ * @param {Object} [options] - Options
42
+ * @param {boolean} [options.includeChain=false] - Include certificate chain
43
+ * @returns {Function} Express middleware
44
+ * @throws {Error} If unsupported options are passed
45
+ */
46
+ function clientCertificateAuth(callback, options = {}) {
47
+ // Validate that no unsupported options are passed
48
+ const used = UNSUPPORTED_OPTIONS.filter(k => k in options);
49
+ if (used.length) {
50
+ throw new Error(
51
+ `CJS sync wrapper does not support: ${used.join(', ')}. ` +
52
+ 'Use require(...).load() for full features.'
53
+ );
54
+ }
55
+
56
+ const { includeChain = false } = options;
57
+
58
+ return function middleware(req, res, next) {
59
+ // Ensure that the certificate was validated at the protocol level
60
+ if (!req.socket?.authorized) {
61
+ const authError = req.socket?.authorizationError || 'unknown';
62
+ const e = new Error(`Unauthorized: Client certificate required (${authError})`);
63
+ e.status = 401;
64
+ return next(e);
65
+ }
66
+
67
+ // Obtain certificate details
68
+ const cert = req.socket.getPeerCertificate(includeChain);
69
+ if (!cert || Object.keys(cert).length === 0) {
70
+ const e = new Error(
71
+ 'Client certificate was authenticated but certificate information could not be retrieved.'
72
+ );
73
+ e.status = 500;
74
+ return next(e);
75
+ }
76
+
77
+ // Attach certificate to request for downstream access
78
+ req.clientCertificate = cert;
79
+
80
+ function doneAuthorizing(authorized) {
81
+ if (authorized) {
82
+ return next();
83
+ } else {
84
+ const e = new Error('Unauthorized');
85
+ e.status = 401;
86
+ return next(e);
87
+ }
88
+ }
89
+
90
+ try {
91
+ const result = callback(cert);
92
+ if (result instanceof Promise) {
93
+ result.then(doneAuthorizing).catch((err) => {
94
+ if (err.status === undefined) {
95
+ err.status = 401;
96
+ }
97
+ next(err);
98
+ });
99
+ } else {
100
+ doneAuthorizing(result);
101
+ }
102
+ } catch (err) {
103
+ if (err.status === undefined) {
104
+ err.status = 401;
105
+ }
106
+ next(err);
107
+ }
108
+ };
109
+ }
110
+
111
+ module.exports = clientCertificateAuth;
112
+ module.exports.default = clientCertificateAuth;
113
+
114
+ // Also expose async loader for those who want the ES module (full features)
115
+ module.exports.load = loadModule;