client-certificate-auth 0.3.0 → 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,114 +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
+ ```
12
14
 
13
- installing
14
- ----------
15
+ **Requirements:** Node.js >= 18
15
16
 
16
- client-certificate-auth is available from [npm](https://npmjs.org/package/client-certificate-auth.).
17
+ ## Synopsis
17
18
 
18
- $ npm install client-certificate-auth
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.
19
20
 
20
- requirements
21
- ------------
21
+ Compatible with Express, Connect, and any Node.js HTTP server framework that uses standard `req.socket` and `req.headers`.
22
22
 
23
- client-certificate-auth is tested against Node.js versions 0.6, 0.8, and 0.10.
24
- It has no external dependencies (other than any middleware framework with which
25
- you may wish to use it); however, to run the tests, you will need [mocha](https://npmjs.org/package/mocha) and
26
- [should](https://npmjs.org/package/should).
23
+ ## Usage
27
24
 
28
- synopsis
29
- --------
25
+ ### Basic Setup
30
26
 
31
- client-certificate-auth provides HTTP middleware for Node.js (in particular
32
- Connect/Express) to require that a valid, verifiable client SSL certificate is
33
- provided, and passes information about that certificate to a callback which must
34
- return `true` for the request to proceed; otherwise, the client is considered
35
- unauthorized and the request is aborted.
27
+ Configure your HTTPS server to request and validate client certificates:
36
28
 
37
- usage
38
- -----
29
+ ```javascript
30
+ import express from 'express';
31
+ import https from 'node:https';
32
+ import fs from 'node:fs';
33
+ import clientCertificateAuth from 'client-certificate-auth';
39
34
 
40
- The https server must be set up to request a client certificate and validate it
41
- against an issuer/CA certificate. What follows is a typical example using
42
- [Express](http://expressjs.com):
35
+ const app = express();
43
36
 
44
- ```javascript
45
- var express = require('express');
46
- var fs = require('fs');
47
- var https = require('https');
48
- var clientCertificateAuth = require('client-certificate-auth');
37
+ // Validate certificate against your authorization rules
38
+ const checkAuth = (cert) => {
39
+ return cert.subject.CN === 'trusted-client';
40
+ };
41
+
42
+ // Apply to all routes
43
+ app.use(clientCertificateAuth(checkAuth));
44
+
45
+ app.get('/', (req, res) => {
46
+ res.send('Authorized!');
47
+ });
49
48
 
50
- var opts = {
51
- // Server SSL private key and certificate
49
+ // HTTPS server configuration
50
+ const opts = {
52
51
  key: fs.readFileSync('server.key'),
53
52
  cert: fs.readFileSync('server.pem'),
54
- // issuer/CA certificate against which the client certificate will be
55
- // validated. A certificate that is not signed by a provided CA will be
56
- // rejected at the protocol layer.
57
- ca: fs.readFileSync('cacert.pem'),
58
- // request a certificate, but don't necessarily reject connections from
59
- // clients providing an untrusted or no certificate. This lets us protect only
60
- // certain routes, or send a helpful error message to unauthenticated clients.
61
- requestCert: true,
62
- rejectUnauthorized: false
53
+ ca: fs.readFileSync('ca.pem'), // CA that signed client certs
54
+ requestCert: true, // Request client certificate
55
+ rejectUnauthorized: false // Let middleware handle errors
63
56
  };
64
57
 
65
- var app = express();
58
+ https.createServer(opts, app).listen(443);
59
+ ```
66
60
 
67
- // add clientCertificateAuth to the middleware stack, passing it a callback
68
- // which will do further examination of the provided certificate.
69
- app.use(clientCertificateAuth(checkAuth));
70
- app.use(app.router);
71
- app.use(function(err, req, res, next) { console.log(err); next(); });
61
+ ### Per-Route Protection
72
62
 
73
- app.get('/', function(req, res) {
74
- res.send('Authorized!');
63
+ ```javascript
64
+ app.get('/public', (req, res) => {
65
+ res.send('Hello world');
75
66
  });
76
67
 
77
- var checkAuth = function(cert) {
78
- /*
79
- * allow access if certificate subject Common Name is 'Doug Prishpreed'.
80
- * this is one of many ways you can authorize only certain authenticated
81
- * certificate-holders; you might instead choose to check the certificate
82
- * fingerprint, or apply some sort of role-based security based on e.g. the OU
83
- * field of the certificate. You can also link into another layer of
84
- * auth or session middleware here; for instance, you might pass the subject CN
85
- * as a username to log the user in to your underlying authentication/session
86
- * management layer.
87
- */
88
- return cert.subject.CN === 'Doug Prishpreed';
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;
89
79
  };
90
80
 
91
- https.createServer(opts, app).listen(4000);
81
+ app.use(clientCertificateAuth(checkAuth));
92
82
  ```
93
83
 
94
- Or secure only certain routes:
84
+ ### Custom Error Messages
85
+
86
+ Throw errors for granular authorization feedback instead of returning `false`:
95
87
 
96
88
  ```javascript
97
- app.get('/unsecure', function(req, res) {
98
- res.send('Hello world');
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
+ });
99
154
  });
155
+ ```
156
+
157
+ The certificate is attached before the authorization callback runs, so it's available even if authorization fails (useful for logging).
100
158
 
101
- app.get('/secure', clientCertificateAuth(checkAuth), function(req, res) {
102
- res.send('Hello authorized user');
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
+ });
103
214
  });
104
215
  ```
105
216
 
106
- `checkAuth` can also be asynchronous:
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):
107
322
 
108
323
  ```javascript
109
- function checkAuth(cert, callback) {
110
- callback(true);
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;
111
344
  }
345
+ ```
346
+
347
+ ## Authorization Helpers
348
+
349
+ Pre-built validation callbacks for common authorization patterns, available as a separate import:
112
350
 
113
- app.use(checkAuth);
351
+ ```javascript
352
+ import clientCertificateAuth from 'client-certificate-auth';
353
+ import { allowCN, allowFingerprints, allowIssuer, allOf, anyOf } from 'client-certificate-auth/helpers';
114
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 });
446
+ });
447
+
448
+ // With reverse proxy
449
+ app.use(clientCertificateAuth(checkAuth, {
450
+ certificateSource: 'aws-alb'
451
+ }));
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;