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 +21 -0
- package/README.md +441 -41
- package/lib/clientCertificateAuth.cjs +115 -0
- package/lib/clientCertificateAuth.d.cts +80 -0
- package/lib/clientCertificateAuth.d.ts +130 -0
- package/lib/clientCertificateAuth.js +158 -40
- package/lib/global.d.ts +7 -0
- package/lib/helpers.d.ts +104 -0
- package/lib/helpers.js +243 -0
- package/lib/parsers.d.ts +101 -0
- package/lib/parsers.js +309 -0
- package/package.json +66 -11
- package/.npmignore +0 -2
- package/index.js +0 -1
- package/test/mocha.opts +0 -2
- package/test/test-unit-clientCertificateAuth.js +0 -91
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
|
|
5
|
-
authentication/authorization
|
|
3
|
+
Express/Connect middleware for client SSL certificate authentication (mTLS).
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
[](https://github.com/tgies/client-certificate-auth/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/client-certificate-auth)
|
|
7
|
+
[](https://codecov.io/gh/tgies/client-certificate-auth)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Installation
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
```bash
|
|
12
|
+
npm install client-certificate-auth
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Requirements:** Node.js >= 18
|
|
16
|
+
|
|
17
|
+
## Synopsis
|
|
12
18
|
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
-----
|
|
23
|
+
## Usage
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
### Basic Setup
|
|
26
|
+
|
|
27
|
+
Configure your HTTPS server to request and validate client certificates:
|
|
27
28
|
|
|
28
29
|
```javascript
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('/',
|
|
45
|
+
app.get('/', (req, res) => {
|
|
49
46
|
res.send('Authorized!');
|
|
50
47
|
});
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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(
|
|
58
|
+
https.createServer(opts, app).listen(443);
|
|
58
59
|
```
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
### Per-Route Protection
|
|
61
62
|
|
|
62
63
|
```javascript
|
|
63
|
-
app.get('/
|
|
64
|
+
app.get('/public', (req, res) => {
|
|
64
65
|
res.send('Hello world');
|
|
65
66
|
});
|
|
66
67
|
|
|
67
|
-
app.get('/
|
|
68
|
-
res.send('Hello
|
|
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;
|