client-certificate-auth 2.0.2 → 2.1.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
@@ -13,7 +13,7 @@ Comprehensive toolkit for client SSL certificate authentication (mTLS) in Node.j
13
13
 
14
14
  **Recommended by AWS** - Featured in the [AWS API Gateway documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-client-side-ssl-authentication.html#certificate-validation).
15
15
 
16
- **Fanatically Tested** - 100% line/branch/function/statement coverage, plus mutation testing and E2E tests against real nginx/Envoy/Traefik containers. ~5,224 lines of test code for ~788 lines of source (measured by [cloc](https://github.com/AlDanial/cloc)).
16
+ **Fanatically Tested** - 100% line/branch/function/statement coverage, plus mutation testing and E2E tests against real nginx/Envoy/Traefik containers. ~6,207 lines of test code for ~937 lines of source (measured by [cloc](https://github.com/AlDanial/cloc)).
17
17
 
18
18
  ## Installation
19
19
 
@@ -39,6 +39,7 @@ function isThenable(value) {
39
39
  const UNSUPPORTED_OPTIONS = [
40
40
  'certificateSource',
41
41
  'certificateHeader',
42
+ 'chainHeader',
42
43
  'headerEncoding',
43
44
  'fallbackToSocket',
44
45
  'verifyHeader',
@@ -77,6 +77,15 @@ export interface ClientCertificateAuthOptions {
77
77
  */
78
78
  certificateHeader?: string;
79
79
 
80
+ /**
81
+ * Optional second header carrying the certificate chain alongside the leaf.
82
+ * Split on commas per RFC 9440, each item parsed with the same
83
+ * headerEncoding, results linked via issuerCertificate. For non-RFC-9440
84
+ * encodings the comma split may not match the encoding's list convention.
85
+ * Same trust boundary as certificateHeader.
86
+ */
87
+ chainHeader?: string;
88
+
80
89
  /**
81
90
  * How to decode the header value.
82
91
  * Required when using certificateHeader without certificateSource.
@@ -28,7 +28,7 @@ function isThenable(value) {
28
28
 
29
29
  /**
30
30
  * @typedef {Object} ClientCertificateAuthOptions
31
- * @property {'aws-alb' | 'envoy' | 'cloudflare' | 'traefik'} [certificateSource] - Use a preset
31
+ * @property {'aws-alb' | 'aws-alb-verify' | 'azure-app-service' | 'cloudflare' | 'cloudflare-rfc9440' | 'envoy' | 'traefik'} [certificateSource] - Use a preset
32
32
  * configuration for a known reverse proxy. Header-based certs are only checked if this or
33
33
  * certificateHeader is set. Trust boundary: the proxy must strip the preset's header from
34
34
  * external requests; any source that can set it is trusted to assert client identity.
@@ -36,7 +36,11 @@ function isThenable(value) {
36
36
  * Overrides preset header name if also using certificateSource. Trust boundary: the proxy
37
37
  * must strip this header from external requests; any source that can set it is trusted to
38
38
  * assert client identity.
39
- * @property {'url-pem' | 'url-pem-aws' | 'xfcc' | 'base64-der' | 'rfc9440'} [headerEncoding] -
39
+ * @property {string} [chainHeader] - Optional second header carrying the certificate chain
40
+ * alongside the leaf in certificateHeader (or the preset's header). Split on commas per
41
+ * RFC 9440, each item parsed with the same headerEncoding, results linked via
42
+ * issuerCertificate. Same trust boundary as certificateHeader.
43
+ * @property {'url-pem' | 'url-pem-aws' | 'xfcc' | 'base64-der' | 'rfc9440'} [headerEncoding] -
40
44
  * How to decode the header value. Required when using certificateHeader without certificateSource.
41
45
  * @property {boolean} [fallbackToSocket=false] - If header-based extraction is configured but
42
46
  * fails (header absent or malformed), try socket.getPeerCertificate() instead of returning 401.
@@ -96,6 +100,7 @@ export default function clientCertificateAuth(callback, options = {}) {
96
100
  const {
97
101
  certificateSource,
98
102
  certificateHeader,
103
+ chainHeader,
99
104
  headerEncoding,
100
105
  fallbackToSocket = false,
101
106
  includeChain = false,
@@ -132,6 +137,7 @@ export default function clientCertificateAuth(callback, options = {}) {
132
137
  const result = extractClientCertificate(req, {
133
138
  certificateSource,
134
139
  certificateHeader,
140
+ chainHeader,
135
141
  headerEncoding,
136
142
  fallbackToSocket,
137
143
  includeChain,
@@ -12,8 +12,9 @@
12
12
  */
13
13
  /**
14
14
  * @typedef {Object} ExtractorOptions
15
- * @property {'aws-alb' | 'envoy' | 'cloudflare' | 'traefik'} [certificateSource] - Preset configuration
15
+ * @property {'aws-alb' | 'aws-alb-verify' | 'azure-app-service' | 'cloudflare' | 'cloudflare-rfc9440' | 'envoy' | 'traefik'} [certificateSource] - Preset configuration
16
16
  * @property {string} [certificateHeader] - Custom header name
17
+ * @property {string} [chainHeader] - Optional second header carrying the certificate chain
17
18
  * @property {'url-pem' | 'url-pem-aws' | 'xfcc' | 'base64-der' | 'rfc9440'} [headerEncoding] - Header encoding
18
19
  * @property {boolean} [fallbackToSocket=false] - Try socket if header extraction fails
19
20
  * @property {boolean} [includeChain=false] - Include issuerCertificate chain
@@ -89,11 +90,15 @@ export type ExtractorOptions = {
89
90
  /**
90
91
  * - Preset configuration
91
92
  */
92
- certificateSource?: "aws-alb" | "envoy" | "cloudflare" | "traefik";
93
+ certificateSource?: "aws-alb" | "aws-alb-verify" | "azure-app-service" | "cloudflare" | "cloudflare-rfc9440" | "envoy" | "traefik";
93
94
  /**
94
95
  * - Custom header name
95
96
  */
96
97
  certificateHeader?: string;
98
+ /**
99
+ * - Optional second header carrying the certificate chain
100
+ */
101
+ chainHeader?: string;
97
102
  /**
98
103
  * - Header encoding
99
104
  */
package/lib/extractor.js CHANGED
@@ -4,22 +4,23 @@
4
4
  * @license MIT
5
5
  */
6
6
 
7
- import { getCertificateFromHeaders, PRESETS } from './parsers.js';
7
+ import { getCertificateFromHeaders, parseHeaderValue, PRESETS } from './parsers.js';
8
8
 
9
9
  const VALID_ENCODINGS = ['url-pem', 'url-pem-aws', 'xfcc', 'base64-der', 'rfc9440'];
10
10
 
11
11
  /**
12
12
  * Validate options shared by `extractClientCertificate` and the middleware
13
13
  * constructor. Throws on misconfiguration (unknown preset, unknown encoding,
14
- * `certificateHeader` without an encoding source, or only one of
15
- * `verifyHeader`/`verifyValue`). Omitted or `undefined` options are treated
16
- * as the empty object; explicit `null` will throw a `TypeError`.
14
+ * `certificateHeader` without an encoding source, `chainHeader` without a
15
+ * leaf header source, or only one of `verifyHeader`/`verifyValue`). Omitted
16
+ * or `undefined` options are treated as the empty object; explicit `null`
17
+ * will throw a `TypeError`.
17
18
  *
18
19
  * @param {ExtractorOptions} [options]
19
20
  * @throws {Error} when options are malformed
20
21
  */
21
22
  export function validateExtractorOptions(options = {}) {
22
- const { certificateSource, certificateHeader, headerEncoding, verifyHeader, verifyValue } = options;
23
+ const { certificateSource, certificateHeader, chainHeader, headerEncoding, verifyHeader, verifyValue } = options;
23
24
 
24
25
  if ((verifyHeader && !verifyValue) || (!verifyHeader && verifyValue)) {
25
26
  throw new Error(
@@ -44,6 +45,12 @@ export function validateExtractorOptions(options = {}) {
44
45
  'client-certificate-auth: certificateHeader requires headerEncoding (or a certificateSource preset that supplies one)'
45
46
  );
46
47
  }
48
+
49
+ if (chainHeader && !certificateSource && !certificateHeader) {
50
+ throw new Error(
51
+ 'client-certificate-auth: chainHeader requires certificateSource or certificateHeader (chain header must accompany a leaf header configuration)'
52
+ );
53
+ }
47
54
  }
48
55
 
49
56
  /**
@@ -61,12 +68,16 @@ export function validateExtractorOptions(options = {}) {
61
68
 
62
69
  /**
63
70
  * @typedef {Object} ExtractorOptions
64
- * @property {'aws-alb' | 'envoy' | 'cloudflare' | 'traefik'} [certificateSource] - Preset
71
+ * @property {'aws-alb' | 'aws-alb-verify' | 'azure-app-service' | 'cloudflare' | 'cloudflare-rfc9440' | 'envoy' | 'traefik'} [certificateSource] - Preset
65
72
  * configuration. Trust boundary: the proxy must strip the preset's header from external
66
73
  * requests; any source that can set it is trusted to assert client identity.
67
74
  * @property {string} [certificateHeader] - Custom header name. Trust boundary: the proxy must
68
75
  * strip this header from external requests; any source that can set it is trusted to assert
69
76
  * client identity.
77
+ * @property {string} [chainHeader] - Optional second header carrying the certificate chain
78
+ * alongside the leaf. Split on commas per RFC 9440, each item parsed with the same
79
+ * `headerEncoding`, results linked via `issuerCertificate`. For non-RFC-9440 encodings
80
+ * the comma split may not match the encoding's list convention.
70
81
  * @property {'url-pem' | 'url-pem-aws' | 'xfcc' | 'base64-der' | 'rfc9440'} [headerEncoding] - Header encoding
71
82
  * @property {boolean} [fallbackToSocket=false] - Try socket if header extraction fails
72
83
  * @property {boolean} [includeChain=false] - Include issuerCertificate chain
@@ -111,6 +122,7 @@ export function extractClientCertificate(req, options = {}) {
111
122
  const {
112
123
  certificateSource,
113
124
  certificateHeader,
125
+ chainHeader,
114
126
  headerEncoding,
115
127
  fallbackToSocket = false,
116
128
  includeChain = false,
@@ -142,6 +154,32 @@ export function extractClientCertificate(req, options = {}) {
142
154
  headerEncoding,
143
155
  });
144
156
 
157
+ // Append chain from a separate chain header if configured.
158
+ // Resolve: explicit chainHeader > preset.chainHeader; explicit encoding > preset.encoding.
159
+ // Split on commas per RFC 9440, parse each item, link via issuerCertificate.
160
+ if (cert) {
161
+ const preset = certificateSource ? PRESETS[certificateSource] : null;
162
+ const chainHeaderName = (chainHeader ?? preset?.chainHeader)?.toLowerCase();
163
+ const chainEncoding = headerEncoding ?? preset?.encoding;
164
+ if (chainHeaderName && chainEncoding) {
165
+ const chainHeaderValue = req.headers[chainHeaderName];
166
+ if (chainHeaderValue && !Array.isArray(chainHeaderValue)) {
167
+ const chainCerts = chainHeaderValue
168
+ .split(',')
169
+ .map(item => item.trim())
170
+ .filter(Boolean)
171
+ .map(item => parseHeaderValue(item, chainEncoding))
172
+ .filter(Boolean);
173
+ if (chainCerts.length > 0) {
174
+ cert.issuerCertificate = chainCerts[0];
175
+ for (let i = 0; i < chainCerts.length - 1; i++) {
176
+ chainCerts[i].issuerCertificate = chainCerts[i + 1];
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+
145
183
  // Normalize: strip chain unless includeChain is true
146
184
  if (cert && !includeChain && 'issuerCertificate' in cert) {
147
185
  delete cert.issuerCertificate;
package/lib/fetch.cjs ADDED
@@ -0,0 +1,20 @@
1
+ /*!
2
+ * client-certificate-auth/fetch - CommonJS wrapper
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ let _module;
10
+
11
+ async function load() {
12
+ // Stryker disable next-line ConditionalExpression,BlockStatement: test ordering caches _module from prior test; ConditionalExpression→true (always re-import) re-imports same module successfully; BlockStatement→{} uses cached value
13
+ if (!_module) {
14
+ // Stryker disable next-line StringLiteral: test ordering caches _module; StringLiteral→"" fails import but cached value masks it
15
+ _module = await import('./fetch.js');
16
+ }
17
+ return _module;
18
+ }
19
+
20
+ module.exports = { load };
@@ -0,0 +1,13 @@
1
+ /*!
2
+ * client-certificate-auth/fetch - CommonJS type declarations
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ import type * as FetchModule from './fetch.js';
8
+
9
+ declare const fetchAdapter: {
10
+ load(): Promise<typeof FetchModule>;
11
+ };
12
+
13
+ export = fetchAdapter;
package/lib/fetch.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ /*!
2
+ * client-certificate-auth/fetch - TypeScript declarations
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ import type { ExtractorOptions, ExtractionResult } from './extractor.js';
8
+
9
+ /**
10
+ * Any object whose `headers` field iterates as `[name, value]` tuples.
11
+ * Web `Request`, undici `Headers`, Bun, Deno, and Node 18+ `Headers`
12
+ * all work without modification.
13
+ */
14
+ export interface RequestLike {
15
+ headers: Iterable<[string, string]>;
16
+ }
17
+
18
+ /**
19
+ * Extract a client certificate from a Web standard `Request` or any
20
+ * object with iterable headers. Header-only (no TLS socket in Web Request).
21
+ *
22
+ * @param request - A Web Request or any object with iterable headers
23
+ * @param options - Same options as `extractClientCertificate`. Header options only.
24
+ */
25
+ export declare function extractClientCertificateFromRequest(
26
+ request: RequestLike,
27
+ options?: ExtractorOptions
28
+ ): ExtractionResult;
package/lib/fetch.js ADDED
@@ -0,0 +1,60 @@
1
+ /*!
2
+ * client-certificate-auth/fetch - Web Request adapter
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ import { extractClientCertificate } from './extractor.js';
8
+
9
+ /**
10
+ * @typedef {import('./extractor.js').ExtractorOptions} ExtractorOptions
11
+ * @typedef {import('./extractor.js').ExtractionResult} ExtractionResult
12
+ */
13
+
14
+ /**
15
+ * Extract a client certificate from a Web standard `Request` (or any object
16
+ * with an iterable `headers` field that yields `[name, value]` tuples).
17
+ *
18
+ * Normalizes header names to lowercase and delegates to the core
19
+ * `extractClientCertificate`. Header-only: Web `Request` has no TLS socket,
20
+ * so `fallbackToSocket` is stripped and has no effect.
21
+ *
22
+ * @param {{ headers: Iterable<[string, string]> }} request - A Web Request or any object whose `headers` iterates `[name, value]` pairs.
23
+ * @param {ExtractorOptions} [options={}] - Same options as `extractClientCertificate`. Header-extraction options only; socket options are ignored.
24
+ * @returns {ExtractionResult}
25
+ *
26
+ * @example
27
+ * // Hono
28
+ * import { extractClientCertificateFromRequest } from 'client-certificate-auth/fetch';
29
+ *
30
+ * app.get('/secure', async (c) => {
31
+ * const result = extractClientCertificateFromRequest(c.req.raw, {
32
+ * certificateSource: 'cloudflare-rfc9440',
33
+ * });
34
+ * if (!result.success) return c.text('Unauthorized', 401);
35
+ * return c.text(`Hello ${result.certificate.subject.CN}`);
36
+ * });
37
+ *
38
+ * @example
39
+ * // Next.js Route Handler
40
+ * export async function GET(request) {
41
+ * const result = extractClientCertificateFromRequest(request, {
42
+ * certificateSource: 'aws-alb',
43
+ * });
44
+ * if (!result.success) return new Response('Unauthorized', { status: 401 });
45
+ * return Response.json({ user: result.certificate.subject.CN });
46
+ * }
47
+ */
48
+ export function extractClientCertificateFromRequest(request, options = {}) {
49
+ // Web Headers normalizes to lowercase, but Map and other iterables
50
+ // preserve casing. Normalize here for the core extractor's lowercase lookup.
51
+ const headers = {};
52
+ for (const [name, value] of request.headers) {
53
+ headers[name.toLowerCase()] = value;
54
+ }
55
+
56
+ // Stryker disable next-line ObjectLiteral: destructure-discard strips fallbackToSocket
57
+ const { fallbackToSocket: _ignored, ...rest } = options;
58
+
59
+ return extractClientCertificate({ headers }, rest);
60
+ }
package/lib/lambda.cjs ADDED
@@ -0,0 +1,20 @@
1
+ /*!
2
+ * client-certificate-auth/lambda - CommonJS wrapper
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ let _module;
10
+
11
+ async function load() {
12
+ // Stryker disable next-line ConditionalExpression,BlockStatement: test ordering caches _module from prior test; ConditionalExpression→true (always re-import) re-imports same module successfully; BlockStatement→{} uses cached value
13
+ if (!_module) {
14
+ // Stryker disable next-line StringLiteral: test ordering caches _module; StringLiteral→"" fails import but cached value masks it
15
+ _module = await import('./lambda.js');
16
+ }
17
+ return _module;
18
+ }
19
+
20
+ module.exports = { load };
@@ -0,0 +1,13 @@
1
+ /*!
2
+ * client-certificate-auth/lambda - CommonJS type declarations
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ import type * as LambdaModule from './lambda.js';
8
+
9
+ declare const lambda: {
10
+ load(): Promise<typeof LambdaModule>;
11
+ };
12
+
13
+ export = lambda;
@@ -0,0 +1,78 @@
1
+ /*!
2
+ * client-certificate-auth/lambda - TypeScript declarations
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ import type { PeerCertificate } from 'tls';
8
+
9
+ /**
10
+ * Client certificate object from the API Gateway Lambda event.
11
+ * Common to v1 and v2 payload formats. Compatible with `@types/aws-lambda`
12
+ * without requiring it as a dependency.
13
+ */
14
+ export interface LambdaClientCert {
15
+ /** PEM-encoded client certificate with BEGIN/END delimiters. */
16
+ clientCertPem: string;
17
+ /** Optional subject distinguished name string, parsed by API Gateway. */
18
+ subjectDN?: string;
19
+ /** Optional issuer distinguished name string, parsed by API Gateway. */
20
+ issuerDN?: string;
21
+ /** Optional certificate serial number string, parsed by API Gateway. */
22
+ serialNumber?: string;
23
+ /** Optional validity window parsed by API Gateway. */
24
+ validity?: {
25
+ notBefore?: string;
26
+ notAfter?: string;
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Lambda event subset declaring only the cert-bearing paths.
32
+ * Accepts HTTP API (v2.0) and REST API (v1.0) payload formats.
33
+ */
34
+ export interface LambdaEventWithClientCert {
35
+ requestContext?: {
36
+ /** v2.0 payload format: HTTP API. */
37
+ authentication?: {
38
+ clientCert?: LambdaClientCert;
39
+ };
40
+ /** v1.0 payload format: REST API. */
41
+ identity?: {
42
+ clientCert?: LambdaClientCert;
43
+ };
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Result returned by `extractClientCertificateFromLambdaEvent`.
49
+ * Matches the core extractor's `ExtractionResult`.
50
+ */
51
+ export interface LambdaExtractionResult {
52
+ /** Whether extraction succeeded. */
53
+ success: boolean;
54
+ /** Extracted certificate (null on failure). */
55
+ certificate: PeerCertificate | null;
56
+ /**
57
+ * Rejection reason code (null on success). Lambda-specific reasons:
58
+ * - 'lambda_event_missing_clientcert'
59
+ * - 'lambda_event_clientcert_malformed'
60
+ */
61
+ reason: string | null;
62
+ }
63
+
64
+ /**
65
+ * Extract a client certificate from an AWS API Gateway Lambda event. Handles
66
+ * both v2.0 payload format (`event.requestContext.authentication.clientCert`)
67
+ * and v1.0 payload format (`event.requestContext.identity.clientCert`).
68
+ *
69
+ * Also accepts `null` and `undefined`, returning
70
+ * `lambda_event_missing_clientcert`.
71
+ *
72
+ * @param event - The Lambda event from API Gateway, or null/undefined
73
+ *
74
+ * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-mutual-tls.html
75
+ */
76
+ export declare function extractClientCertificateFromLambdaEvent(
77
+ event: LambdaEventWithClientCert | null | undefined
78
+ ): LambdaExtractionResult;
package/lib/lambda.js ADDED
@@ -0,0 +1,73 @@
1
+ /*!
2
+ * client-certificate-auth/lambda - AWS Lambda event adapter
3
+ * Copyright (C) 2013-2026 Tony Gies
4
+ * @license MIT
5
+ */
6
+
7
+ import { pemToCertificate } from './parsers.js';
8
+
9
+ /**
10
+ * @typedef {import('./extractor.js').ExtractionResult} ExtractionResult
11
+ */
12
+
13
+ /**
14
+ * Extract a client certificate from an AWS API Gateway Lambda event.
15
+ *
16
+ * API Gateway HTTP API (v2.0 payload) delivers the validated mTLS client
17
+ * certificate as a pre-parsed object at
18
+ * `event.requestContext.authentication.clientCert`. The legacy REST API
19
+ * (v1.0 payload) delivers it at `event.requestContext.identity.clientCert`.
20
+ * Both payloads carry a `clientCertPem` field plus parsed `subjectDN`,
21
+ * `issuerDN`, `serialNumber`, and `validity` fields.
22
+ *
23
+ * Parses `clientCertPem` into a `PeerCertificate` so the same validation
24
+ * logic used with `getPeerCertificate()` or `extractClientCertificate()`
25
+ * works inside a Lambda handler. If both v1 and v2 fields are present,
26
+ * v2 takes precedence.
27
+ *
28
+ * @param {object | null | undefined} event - The Lambda event object from API Gateway (also accepts null/undefined)
29
+ * @returns {ExtractionResult}
30
+ *
31
+ * Rejection reasons:
32
+ * - 'lambda_event_missing_clientcert' - No clientCertPem at either v1 or v2 location
33
+ * - 'lambda_event_clientcert_malformed' - clientCertPem present but parsing failed
34
+ *
35
+ * @example
36
+ * import { extractClientCertificateFromLambdaEvent } from 'client-certificate-auth/lambda';
37
+ *
38
+ * export const handler = async (event) => {
39
+ * const result = extractClientCertificateFromLambdaEvent(event);
40
+ * if (!result.success) return { statusCode: 401, body: result.reason };
41
+ * if (result.certificate.subject.CN !== 'authorized-client') {
42
+ * return { statusCode: 403 };
43
+ * }
44
+ * return { statusCode: 200, body: 'OK' };
45
+ * };
46
+ */
47
+ export function extractClientCertificateFromLambdaEvent(event) {
48
+ const v2 = event?.requestContext?.authentication?.clientCert;
49
+ const v1 = event?.requestContext?.identity?.clientCert;
50
+ const clientCert = v2 ?? v1;
51
+
52
+ if (!clientCert?.clientCertPem) {
53
+ return {
54
+ success: false,
55
+ certificate: null,
56
+ reason: 'lambda_event_missing_clientcert',
57
+ };
58
+ }
59
+
60
+ try {
61
+ return {
62
+ success: true,
63
+ certificate: pemToCertificate(clientCert.clientCertPem),
64
+ reason: null,
65
+ };
66
+ } catch {
67
+ return {
68
+ success: false,
69
+ certificate: null,
70
+ reason: 'lambda_event_clientcert_malformed',
71
+ };
72
+ }
73
+ }
package/lib/parsers.d.ts CHANGED
@@ -14,7 +14,14 @@ export type HeaderEncoding = 'url-pem' | 'url-pem-aws' | 'xfcc' | 'base64-der' |
14
14
  /**
15
15
  * Supported certificate source presets.
16
16
  */
17
- export type CertificateSource = 'aws-alb' | 'envoy' | 'cloudflare' | 'traefik';
17
+ export type CertificateSource =
18
+ | 'aws-alb'
19
+ | 'aws-alb-verify'
20
+ | 'azure-app-service'
21
+ | 'cloudflare'
22
+ | 'cloudflare-rfc9440'
23
+ | 'envoy'
24
+ | 'traefik';
18
25
 
19
26
  /**
20
27
  * Preset configuration for a reverse proxy.
@@ -22,6 +29,12 @@ export type CertificateSource = 'aws-alb' | 'envoy' | 'cloudflare' | 'traefik';
22
29
  export interface PresetConfig {
23
30
  /** HTTP header name (lowercase) */
24
31
  header: string;
32
+ /**
33
+ * Optional second header carrying the certificate chain. Set on
34
+ * two-header schemes like RFC 9440 (Client-Cert + Client-Cert-Chain).
35
+ * Lowercased per Node convention.
36
+ */
37
+ chainHeader?: string;
25
38
  /** Encoding format used by this proxy */
26
39
  encoding: HeaderEncoding;
27
40
  }
package/lib/parsers.js CHANGED
@@ -13,7 +13,8 @@ import { X509Certificate } from 'node:crypto';
13
13
 
14
14
  /**
15
15
  * Preset configurations for common reverse proxies.
16
- * Maps preset name to { header, encoding } configuration.
16
+ * Maps preset name to { header, encoding } configuration, with optional
17
+ * `chainHeader` for two-header schemes (RFC 9440).
17
18
  */
18
19
  export const PRESETS = {
19
20
  /**
@@ -25,21 +26,55 @@ export const PRESETS = {
25
26
  encoding: 'url-pem-aws',
26
27
  },
27
28
  /**
28
- * Envoy proxy / Istio service mesh using XFCC header.
29
- * @see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-client-cert
29
+ * AWS Application Load Balancer in mTLS verify mode. ALB validates the
30
+ * client certificate against a configured trust store and forwards the
31
+ * leaf as URL-encoded PEM (the leaf only, not the chain) plus parsed
32
+ * subject/issuer/serial/validity headers for convenience.
33
+ * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/mutual-authentication.html
30
34
  */
31
- 'envoy': {
32
- header: 'x-forwarded-client-cert',
33
- encoding: 'xfcc',
35
+ 'aws-alb-verify': {
36
+ header: 'x-amzn-mtls-clientcert-leaf',
37
+ encoding: 'url-pem-aws',
38
+ },
39
+ /**
40
+ * Azure App Service mTLS forwarding. App Service injects the bare
41
+ * base64-encoded DER (the body of a PEM cert without delimiters) into
42
+ * `X-ARR-ClientCert`. Same header convention is used by IIS/ARR.
43
+ * @see https://learn.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth
44
+ */
45
+ 'azure-app-service': {
46
+ header: 'x-arr-clientcert',
47
+ encoding: 'base64-der',
34
48
  },
35
49
  /**
36
- * Cloudflare with client_certificate_forwarding enabled.
50
+ * Cloudflare with client_certificate_forwarding enabled (legacy
51
+ * Cf-Client-Cert-* header family).
37
52
  * @see https://developers.cloudflare.com/api-shield/security/mtls/configure/
38
53
  */
39
54
  'cloudflare': {
40
55
  header: 'cf-client-cert-der-base64',
41
56
  encoding: 'base64-der',
42
57
  },
58
+ /**
59
+ * Cloudflare with RFC 9440 forwarding enabled (March 2026 feature).
60
+ * Operators set `Client-Cert` and `Client-Cert-Chain` headers via
61
+ * Transform Rules. Leaf is `:base64:`-wrapped; chain is a structured
62
+ * field list of `:base64:` items separated by commas.
63
+ * @see https://developers.cloudflare.com/changelog/post/2026-03-25-rfc9440-mtls-fields/
64
+ */
65
+ 'cloudflare-rfc9440': {
66
+ header: 'client-cert',
67
+ chainHeader: 'client-cert-chain',
68
+ encoding: 'rfc9440',
69
+ },
70
+ /**
71
+ * Envoy proxy / Istio service mesh using XFCC header.
72
+ * @see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-client-cert
73
+ */
74
+ 'envoy': {
75
+ header: 'x-forwarded-client-cert',
76
+ encoding: 'xfcc',
77
+ },
43
78
  /**
44
79
  * Traefik with PassTLSClientCert middleware (pem: true).
45
80
  * Traefik sends raw base64 (no PEM delimiters, not URL-encoded).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "client-certificate-auth",
3
- "version": "2.0.2",
3
+ "version": "2.1.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": {
@@ -51,6 +51,26 @@
51
51
  "types": "./lib/extractor.d.cts",
52
52
  "default": "./lib/extractor.cjs"
53
53
  }
54
+ },
55
+ "./lambda": {
56
+ "import": {
57
+ "types": "./lib/lambda.d.ts",
58
+ "default": "./lib/lambda.js"
59
+ },
60
+ "require": {
61
+ "types": "./lib/lambda.d.cts",
62
+ "default": "./lib/lambda.cjs"
63
+ }
64
+ },
65
+ "./fetch": {
66
+ "import": {
67
+ "types": "./lib/fetch.d.ts",
68
+ "default": "./lib/fetch.js"
69
+ },
70
+ "require": {
71
+ "types": "./lib/fetch.d.cts",
72
+ "default": "./lib/fetch.cjs"
73
+ }
54
74
  }
55
75
  },
56
76
  "main": "./lib/clientCertificateAuth.cjs",
@@ -65,6 +85,12 @@
65
85
  ],
66
86
  "extractor": [
67
87
  "./lib/extractor.d.ts"
88
+ ],
89
+ "lambda": [
90
+ "./lib/lambda.d.ts"
91
+ ],
92
+ "fetch": [
93
+ "./lib/fetch.d.ts"
68
94
  ]
69
95
  }
70
96
  },