{"id":44027999,"url":"https://github.com/tgies/client-certificate-auth","last_synced_at":"2026-04-29T06:01:10.274Z","repository":{"id":57200268,"uuid":"9786511","full_name":"tgies/client-certificate-auth","owner":"tgies","description":"Node.js middleware and toolkit for client SSL certificate (mTLS) auth","archived":false,"fork":false,"pushed_at":"2026-04-26T19:31:34.000Z","size":1004,"stargazers_count":80,"open_issues_count":1,"forks_count":10,"subscribers_count":4,"default_branch":"master","last_synced_at":"2026-04-26T21:13:15.259Z","etag":null,"topics":["authentication","authorization","client-certificate","client-certificate-authentication","client-certificates","express","express-middleware","expressjs","expressjs-middleware","middleware","security","ssl","tls","tls-certificate"],"latest_commit_sha":null,"homepage":"http://tgies.github.io/client-certificate-auth/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tgies.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["tgies"]}},"created_at":"2013-05-01T05:50:08.000Z","updated_at":"2026-04-26T19:30:19.000Z","dependencies_parsed_at":"2022-09-16T15:01:04.704Z","dependency_job_id":null,"html_url":"https://github.com/tgies/client-certificate-auth","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/tgies/client-certificate-auth","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tgies%2Fclient-certificate-auth","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tgies%2Fclient-certificate-auth/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tgies%2Fclient-certificate-auth/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tgies%2Fclient-certificate-auth/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tgies","download_url":"https://codeload.github.com/tgies/client-certificate-auth/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tgies%2Fclient-certificate-auth/sbom","scorecard":{"id":876099,"data":{"date":"2025-08-11","repo":{"name":"github.com/tgies/client-certificate-auth","commit":"63e6184c474fbbc1719d979eb15430c81b3f3ca0"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.6,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Code-Review","score":0,"reason":"Found 0/27 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"License","score":0,"reason":"license file not detected","details":["Warn: project does not have a license file"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}}]},"last_synced_at":"2025-08-24T06:03:36.470Z","repository_id":57200268,"created_at":"2025-08-24T06:03:36.470Z","updated_at":"2025-08-24T06:03:36.470Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32339398,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-26T23:26:28.701Z","status":"online","status_checked_at":"2026-04-27T02:00:06.769Z","response_time":128,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["authentication","authorization","client-certificate","client-certificate-authentication","client-certificates","express","express-middleware","expressjs","expressjs-middleware","middleware","security","ssl","tls","tls-certificate"],"created_at":"2026-02-07T18:14:42.035Z","updated_at":"2026-04-29T06:01:10.259Z","avatar_url":"https://github.com/tgies.png","language":"JavaScript","funding_links":["https://github.com/sponsors/tgies","https://tidelift.com/"],"categories":["Authentication"],"sub_categories":["\u003ca name=\"authN-node\"\u003e\u003c/a\u003eNode.js"],"readme":"# client-certificate-auth\n\nComprehensive toolkit for client SSL certificate authentication (mTLS) in Node.js. Includes Express/Connect middleware, framework-agnostic certificate extraction for reverse proxies (AWS ALB, Envoy, Cloudflare, Traefik, and more), and pre-built authorization helpers.\n\n[![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)\n[![npm version](https://img.shields.io/npm/v/client-certificate-auth.svg)](https://www.npmjs.com/package/client-certificate-auth)\n[![codecov](https://codecov.io/gh/tgies/client-certificate-auth/graph/badge.svg)](https://codecov.io/gh/tgies/client-certificate-auth)\n[![stryker mutation testing](https://img.shields.io/endpoint?style=flat\u0026url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Ftgies%2Fclient-certificate-auth%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/tgies/client-certificate-auth/master)\n\n[**Full Documentation**](https://tgies.github.io/client-certificate-auth/) - guides, API reference, and runnable examples\n[**Commercial Support**](#commercial-support) - consulting, custom features, and priority support for production deployments\n\n**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).\n\n**Fanatically Tested** - 100% line/branch/function/statement coverage, plus mutation testing and E2E tests against real nginx/Envoy/Traefik containers. ~4,776 lines of test code for ~711 lines of source (measured by [cloc](https://github.com/AlDanial/cloc)).\n\n## Installation\n\n```bash\nnpm install client-certificate-auth\n```\n\n**Requirements:** Node.js \u003e= 20\n\n## Synopsis\n\nThis library provides everything you need to implement mutual TLS (mTLS) authentication in Node.js. It extracts client certificates from direct TLS connections (`req.socket`) or from HTTP headers forwarded by reverse proxies (AWS ALB, Envoy, Cloudflare, Traefik, nginx, HAProxy).\n\nThe certificate is parsed into a standard `tls.PeerCertificate` object and passed to your callback for authorization logic.\n\nCompatible with Express, Connect, or any Node.js HTTP server framework by using the framework-agnostic `extractClientCertificate` function.\n\n## Usage\n\n### Basic Setup\n\nConfigure your HTTPS server to request and validate client certificates:\n\n```javascript\nimport express from 'express';\nimport https from 'node:https';\nimport fs from 'node:fs';\nimport clientCertificateAuth from 'client-certificate-auth';\n\nconst app = express();\n\n// Validate certificate against your authorization rules\nconst checkAuth = (cert) =\u003e {\n  return cert.subject.CN === 'trusted-client';\n};\n\n// Apply to all routes\napp.use(clientCertificateAuth(checkAuth));\n\napp.get('/', (req, res) =\u003e {\n  res.send('Authorized!');\n});\n\n// HTTPS server configuration\nconst opts = {\n  key: fs.readFileSync('server.key'),\n  cert: fs.readFileSync('server.pem'),\n  ca: fs.readFileSync('ca.pem'),       // CA that signed client certs\n  requestCert: true,                    // Request client certificate\n  rejectUnauthorized: false             // Let middleware handle errors\n};\n\nhttps.createServer(opts, app).listen(443);\n```\n\n### Per-Route Protection\n\n```javascript\napp.get('/public', (req, res) =\u003e {\n  res.send('Hello world');\n});\n\napp.get('/admin', clientCertificateAuth(checkAuth), (req, res) =\u003e {\n  res.send('Hello admin');\n});\n```\n\n### Async Authorization\n\n```javascript\nconst checkAuth = async (cert) =\u003e {\n  const user = await db.findByFingerprint(cert.fingerprint);\n  return user !== null;\n};\n\napp.use(clientCertificateAuth(checkAuth));\n```\n\n### Custom Error Messages\n\nThrow errors for granular authorization feedback instead of returning `false`:\n\n```javascript\nconst checkAuth = (cert) =\u003e {\n  if (isRevoked(cert.serialNumber)) {\n    throw new Error('Certificate has been revoked');\n  }\n  if (!allowlist.includes(cert.fingerprint)) {\n    throw new Error('Certificate not in allowlist');\n  }\n  return true;\n};\n\n// Thrown errors are passed to Express error handlers with:\n// - error.message = your custom message\n// - error.status = 401 (unless you set a different status)\n```\n\nTo use a different status code, set it on the error before throwing:\n\n```javascript\nconst err = new Error('Access forbidden');\nerr.status = 403;\nthrow err;\n```\n\n### Audit Logging Hooks\n\nUse `onAuthenticated` and `onRejected` hooks to log authentication decisions without affecting request processing:\n\n```javascript\napp.use(clientCertificateAuth(checkAuth, {\n  onAuthenticated: (cert, req) =\u003e {\n    logger.info('mTLS auth success', {\n      cn: cert.subject.CN,\n      fingerprint: cert.fingerprint,\n      path: req.url,\n      ip: req.ip\n    });\n  },\n  onRejected: (cert, req, reason) =\u003e {\n    logger.warn('mTLS auth failed', {\n      cn: cert?.subject?.CN,\n      reason,\n      path: req.url,\n      ip: req.ip\n    });\n  }\n}));\n```\n\n**Hook characteristics:**\n\n- **Fire-and-forget**: Hooks don't block request processing. Async hooks run in the background.\n- **Error-safe**: Hook errors are caught and logged to `console.error`, never affecting the request.\n- **Cert may be null**: In `onRejected`, `cert` is `null` when certificate extraction failed (socket not authorized, header missing, etc.)\n\n**Rejection reasons:**\n\n| Reason | Description |\n|--------|-------------|\n| `socket_not_authorized` | TLS socket authorization failed |\n| `certificate_not_retrievable` | Socket authorized but cert couldn't be read |\n| `header_missing_or_malformed` | Certificate header absent or unparseable |\n| `verification_header_mismatch` | Proxy verify header didn't match expected value |\n| `callback_returned_false` | Your callback returned `false` |\n| *(error message)* | Your callback threw an error |\n\n## API\n\n### `clientCertificateAuth(callback, options?)`\n\nReturns Express middleware.\n\n**Parameters:**\n\n| Name | Type | Description |\n|------|------|-------------|\n| `callback` | `(cert, req?) =\u003e boolean \\| Promise\u003cboolean\u003e` | Receives the client certificate and request, returns `true` to allow access |\n| `options.certificateSource` | `string` | Use a preset for a known proxy: `'aws-alb'`, `'envoy'`, `'cloudflare'`, `'traefik'` |\n| `options.certificateHeader` | `string` | Custom header name to read certificate from |\n| `options.headerEncoding` | `string` | Encoding format: `'url-pem'`, `'url-pem-aws'`, `'xfcc'`, `'base64-der'`, `'rfc9440'` |\n| `options.fallbackToSocket` | `boolean` | If header extraction fails, try `socket.getPeerCertificate()` (default: `false`) |\n| `options.includeChain` | `boolean` | If `true`, include full certificate chain via `cert.issuerCertificate` (default: `false`) |\n| `options.verifyHeader` | `string` | Header name containing verification status from proxy (e.g., `'X-SSL-Client-Verify'`) |\n| `options.verifyValue` | `string` | Expected value indicating successful verification (e.g., `'SUCCESS'`) |\n| `options.onAuthenticated` | `(cert, req) =\u003e void` | Called on successful authentication (fire-and-forget) |\n| `options.onRejected` | `(cert, req, reason) =\u003e void` | Called on authentication failure (fire-and-forget) |\n\n**Certificate Object:**\n\nThe `cert` parameter contains fields from [`tls.PeerCertificate`](https://nodejs.org/api/tls.html#certificate-object):\n\n- `subject.CN` - Common Name\n- `subject.O` - Organization\n- `issuer` - Issuer information\n- `fingerprint` - Certificate fingerprint\n- `valid_from`, `valid_to` - Validity period\n- `issuerCertificate` - Issuer's certificate (only when `includeChain: true`)\n\n### `extractClientCertificate(req, options?)`\n\nFramework-agnostic certificate extraction function exported from `client-certificate-auth/extractor`. Use this when building adapters for non-Express frameworks or when you need certificate extraction without middleware.\n\n**Parameters:**\n\n| Name | Type | Description |\n|------|------|-------------|\n| `req` | `Object` | Request object with `headers` and optional `socket` |\n| `req.headers` | `Record\u003cstring, string \\| string[]\u003e` | HTTP headers object |\n| `req.socket` | `Object` | Optional TLS socket (for socket-based extraction) |\n| `options` | `Object` | Same options as middleware (except `onAuthenticated`/`onRejected`) |\n\n**Returns:** `ExtractionResult`\n\n```typescript\n{\n  success: boolean;\n  certificate: PeerCertificate | null;\n  reason: string | null;  // Rejection reason if success is false\n}\n```\n\n**Rejection reasons:**\n\n- `'verification_header_mismatch'` - Proxy verify header didn't match expected value\n- `'header_missing_or_malformed'` - Header extraction failed and no fallback configured\n- `'socket_not_authorized'` - Socket not authorized for TLS client cert\n- `'certificate_not_retrievable'` - Socket authorized but getPeerCertificate() returned empty\n\n**Example - Building a Koa adapter:**\n\n```javascript\nimport { extractClientCertificate } from 'client-certificate-auth/extractor';\n\nfunction koaClientCert(checkAuth, options = {}) {\n  return async (ctx, next) =\u003e {\n    const result = extractClientCertificate(ctx.req, options);\n\n    if (!result.success) {\n      ctx.throw(401, result.reason);\n    }\n\n    ctx.state.clientCertificate = result.certificate;\n\n    const allowed = await checkAuth(result.certificate, ctx.req);\n    if (!allowed) {\n      ctx.throw(401, 'Certificate not authorized');\n    }\n\n    await next();\n  };\n}\n\n// Usage\napp.use(koaClientCert(\n  (cert) =\u003e cert.subject.CN === 'admin',\n  { certificateSource: 'aws-alb' }\n));\n```\n\n**Example - Custom authentication flow:**\n\n```javascript\nimport { extractClientCertificate } from 'client-certificate-auth/extractor';\n\napp.post('/api/login', (req, res) =\u003e {\n  // Extract certificate without middleware\n  const result = extractClientCertificate(req, {\n    certificateSource: 'envoy',\n    fallbackToSocket: true\n  });\n\n  if (!result.success) {\n    return res.status(401).json({ error: result.reason });\n  }\n\n  // Custom auth logic\n  const user = lookupUserByCertFingerprint(result.certificate.fingerprint);\n  if (!user) {\n    return res.status(403).json({ error: 'Certificate not registered' });\n  }\n\n  // Issue session token\n  const token = createSessionToken(user);\n  res.json({ token, user });\n});\n```\n\n### Ecosystem\n\nThis package provides everything you need to build mTLS authentication for any Node.js framework:\n\n- **Certificate extraction** via `extractClientCertificate()` - handles both socket and header-based extraction\n- **Authorization helpers** - reusable validation callbacks for common patterns (`allowCN`, `allowFingerprints`, etc.)\n- **Parser library** - decode certificates from various reverse proxy formats (Envoy XFCC, AWS ALB, Cloudflare, etc.)\n- **Type definitions** - full TypeScript support\n\n**Official framework adapters:**\n\n- **[passport-client-certificate-auth](https://www.npmjs.com/package/passport-client-certificate-auth)** - Passport.js strategy for mTLS authentication\n\n**Community adapters:**\n\nIf you build an adapter for another framework (Koa, Fastify, Hapi, NestJS, etc.), please open an issue or PR to get it listed here!\n\n\u003e For complete API documentation with all types, parameters, and examples, see the [API Reference](https://tgies.github.io/client-certificate-auth/api/).\n\n### Accessing the Certificate\n\nAfter authentication, the certificate is attached to `req.clientCertificate` for downstream handlers:\n\n```javascript\napp.use(clientCertificateAuth(checkAuth));\n\napp.get('/whoami', (req, res) =\u003e {\n  res.json({\n    cn: req.clientCertificate.subject.CN,\n    fingerprint: req.clientCertificate.fingerprint\n  });\n});\n```\n\nThe certificate is attached before the authorization callback runs, so it's available even if authorization fails (useful for logging).\n\n### Certificate Chain Access\n\nFor enterprise PKI scenarios, you may need to inspect intermediate CAs or the root CA:\n\n```javascript\napp.use(clientCertificateAuth((cert) =\u003e {\n  // Check issuer's organization\n  if (cert.issuerCertificate) {\n    return cert.issuerCertificate.subject.O === 'Trusted Root CA';\n  }\n  return false;\n}, { includeChain: true }));\n```\n\nWhen `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.\n\n### User Login\n\nClient certificates provide cryptographically-verified identity, making them ideal for user authentication. Map certificate fields to user accounts in your database:\n\n```javascript\napp.use(clientCertificateAuth(async (cert) =\u003e {\n  // Option 1: Lookup by fingerprint (most secure - immutable per certificate)\n  const user = await db.users.findOne({ certFingerprint: cert.fingerprint });\n  \n  // Option 2: Lookup by email (from subject or SAN)\n  // const user = await db.users.findOne({ email: cert.subject.emailAddress });\n  \n  // Option 3: Lookup by Common Name\n  // const user = await db.users.findOne({ certCN: cert.subject.CN });\n  \n  if (!user) {\n    throw new Error('Certificate not registered to any user');\n  }\n  \n  return true;\n}));\n```\n\nTo make the user available to downstream handlers, attach it to the request:\n\n```javascript\napp.use(clientCertificateAuth(async (cert, req) =\u003e {\n  const user = await db.users.findOne({ certFingerprint: cert.fingerprint });\n  if (!user) throw new Error('Unknown certificate');\n  \n  req.user = user;  // Attach for downstream routes\n  return true;\n}));\n\napp.get('/profile', (req, res) =\u003e {\n  res.json({ \n    name: req.user.name,\n    certificateCN: req.clientCertificate.subject.CN \n  });\n});\n```\n\n**Lookup strategies:**\n\n| Field | Pros | Cons |\n|-------|------|------|\n| `fingerprint` | Unique, immutable | Must register each cert |\n| `subject.emailAddress` | Human-readable | Ensure uniqueness |\n| `subject.CN` | Simple to configure | May not be unique |\n| `serialNumber` + issuer | Traceable to your CA | More complex queries |\n\n## Reverse Proxy / Load Balancer Support\n\nWhen 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.\n\n### Using Presets\n\nFor common proxies, use the `certificateSource` option:\n\n```javascript\n// AWS Application Load Balancer\napp.use(clientCertificateAuth(checkAuth, {\n  certificateSource: 'aws-alb'\n}));\n\n// Envoy / Istio\napp.use(clientCertificateAuth(checkAuth, {\n  certificateSource: 'envoy'\n}));\n\n// Cloudflare\napp.use(clientCertificateAuth(checkAuth, {\n  certificateSource: 'cloudflare'\n}));\n\n// Traefik\napp.use(clientCertificateAuth(checkAuth, {\n  certificateSource: 'traefik'\n}));\n```\n\n### Preset Details\n\n| Preset | Header | Encoding |\n|--------|--------|----------|\n| `aws-alb` | `X-Amzn-Mtls-Clientcert` | URL-encoded PEM (AWS variant) |\n| `envoy` | `X-Forwarded-Client-Cert` | XFCC structured format |\n| `cloudflare` | `Cf-Client-Cert-Der-Base64` | Base64-encoded DER |\n| `traefik` | `X-Forwarded-Tls-Client-Cert` | Base64-encoded DER \\* |\n\n\u003e \\* **Traefik note:** The `traefik` preset targets Traefik v3's `PassTLSClientCert` middleware with `pem: true`. Despite Traefik's docs describing this as \"PEM format\", the wire format is the base64 body without PEM headers — equivalent to base64-encoded DER. Behavior may differ in Traefik v2.\n\n\u003e **Cloudflare note:** Cloudflare also provides certificates via the `CF-Client-Cert-PEM` header (URL-encoded PEM). If you use that header instead, configure manually with `certificateHeader: 'CF-Client-Cert-PEM'` and `headerEncoding: 'url-pem'`.\n\n### Custom Headers\n\nFor nginx, HAProxy, Google Cloud Load Balancer, or other proxies with configurable headers:\n\n```javascript\n// nginx with $ssl_client_escaped_cert\napp.use(clientCertificateAuth(checkAuth, {\n  certificateHeader: 'X-SSL-Whatever-You-Use',\n  headerEncoding: 'url-pem'\n}));\n\n// Google Cloud Load Balancer (RFC 9440)\napp.use(clientCertificateAuth(checkAuth, {\n  certificateHeader: 'X-SSL-Whatever-You-Use',\n  headerEncoding: 'rfc9440'\n}));\n\n// HAProxy with base64 DER\napp.use(clientCertificateAuth(checkAuth, {\n  certificateHeader: 'X-SSL-Whatever-You-Use',\n  headerEncoding: 'base64-der'\n}));\n```\n\n### Encoding Formats\n\n| Encoding | Description | Used By |\n|----------|-------------|---------|\n| `url-pem` | URL-encoded PEM certificate | nginx, HAProxy |\n| `url-pem-aws` | URL-encoded PEM (AWS variant, `+` as safe char) | AWS ALB |\n| `xfcc` | Envoy's structured `Key=Value;...` format | Envoy, Istio |\n| `base64-der` | Base64-encoded DER certificate | Cloudflare, Traefik |\n| `rfc9440` | RFC 9440 format: `:base64-der:` | Google Cloud LB |\n\n### Fallback Mode\n\nIf your proxy might not always forward certificates (e.g., direct connections bypass the proxy), enable fallback:\n\n```javascript\napp.use(clientCertificateAuth(checkAuth, {\n  certificateSource: 'aws-alb',\n  fallbackToSocket: true  // Try socket if header missing\n}));\n```\n\n### Security Considerations\n\n\u003e ⚠️ **Important:** When using header-based authentication, your reverse proxy **must** strip any incoming certificate headers from external requests to prevent spoofing.\n\nConfigure your proxy to:\n1. **Strip** the certificate header from incoming requests\n2. **Set** the header only for authenticated mTLS connections\n3. **Never** trust certificate headers from untrusted sources\n\n#### Verification Header (Defense in Depth)\n\nFor 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):\n\n```javascript\napp.use(clientCertificateAuth(checkAuth, {\n  certificateHeader: 'X-SSL-Client-Cert',\n  headerEncoding: 'url-pem',\n  verifyHeader: 'X-SSL-Client-Verify',\n  verifyValue: 'SUCCESS'\n}));\n```\n\nExample nginx configuration:\n```nginx\n# Strip any existing headers from clients\nproxy_set_header X-SSL-Client-Cert \"\";\nproxy_set_header X-SSL-Client-Verify \"\";\n\n# Always send verification status\nproxy_set_header X-SSL-Client-Verify $ssl_client_verify;\n\n# Only send cert if verified\nif ($ssl_client_verify = SUCCESS) {\n    proxy_set_header X-SSL-Client-Cert $ssl_client_escaped_cert;\n}\n```\n\n## WebSocket Support\n\nWebSocket connections work seamlessly with mTLS because the TLS handshake occurs before the HTTP upgrade request. The middleware authenticates the upgrade request just like any other HTTP request.\n\n### With the `ws` Library\n\n```javascript\nimport https from 'node:https';\nimport { WebSocketServer } from 'ws';\nimport clientCertificateAuth from 'client-certificate-auth';\n\nconst server = https.createServer({\n  key: fs.readFileSync('server.key'),\n  cert: fs.readFileSync('server.pem'),\n  ca: fs.readFileSync('ca.pem'),\n  requestCert: true,\n  rejectUnauthorized: false,\n});\n\nconst wss = new WebSocketServer({ noServer: true });\n\nwss.on('connection', (ws, req) =\u003e {\n  // Certificate is available on req.clientCertificate\n  console.log(`Client connected: ${req.clientCertificate.subject.CN}`);\n\n  ws.on('message', (data) =\u003e {\n    ws.send(`Echo: ${data}`);\n  });\n});\n\n// Authenticate upgrade requests\nserver.on('upgrade', (req, socket, head) =\u003e {\n  const middleware = clientCertificateAuth((cert) =\u003e {\n    return cert.subject.CN === 'trusted-client';\n  });\n\n  // Minimal response object for middleware compatibility\n  const res = { writeHead: () =\u003e {}, end: () =\u003e {}, redirect: () =\u003e {} };\n\n  middleware(req, res, (err) =\u003e {\n    if (err) {\n      socket.write(`HTTP/1.1 ${err.status} ${err.message}\\r\\n\\r\\n`);\n      socket.destroy();\n      return;\n    }\n\n    wss.handleUpgrade(req, socket, head, (ws) =\u003e {\n      wss.emit('connection', ws, req);\n    });\n  });\n});\n\nserver.listen(443);\n```\n\n### With Socket.IO\n\n```javascript\nimport https from 'node:https';\nimport { Server } from 'socket.io';\nimport clientCertificateAuth from 'client-certificate-auth';\n\nconst server = https.createServer({\n  key: fs.readFileSync('server.key'),\n  cert: fs.readFileSync('server.pem'),\n  ca: fs.readFileSync('ca.pem'),\n  requestCert: true,\n  rejectUnauthorized: false,\n});\n\nconst io = new Server(server);\n\n// Socket.IO middleware for mTLS authentication\nio.use((socket, next) =\u003e {\n  const req = socket.request;\n  const res = { writeHead: () =\u003e {}, end: () =\u003e {}, redirect: () =\u003e {} };\n\n  const middleware = clientCertificateAuth((cert) =\u003e {\n    return cert.subject.CN === 'trusted-client';\n  });\n\n  middleware(req, res, (err) =\u003e {\n    if (err) {\n      return next(new Error('Authentication failed'));\n    }\n    // Attach certificate info to socket for later use\n    socket.clientCert = req.clientCertificate;\n    next();\n  });\n});\n\nio.on('connection', (socket) =\u003e {\n  console.log(`Client connected: ${socket.clientCert.subject.CN}`);\n});\n\nserver.listen(443);\n```\n\n## Authorization Helpers\n\nPre-built validation callbacks for common authorization patterns, available as a separate import:\n\n```javascript\nimport clientCertificateAuth from 'client-certificate-auth';\nimport { allowCN, allowFingerprints, allowIssuer, allOf, anyOf } from 'client-certificate-auth/helpers';\n```\n\n\u003e **Note:** In CommonJS, the `/helpers`, `/parsers`, and `/extractor` subpath exports provide a `load()` function for async access. See the [CommonJS](#commonjs) section for details.\n\n### Basic Helpers\n\n```javascript\n// Allowlist by Common Name\napp.use(clientCertificateAuth(allowCN(['service-a', 'service-b'])));\n\n// Allowlist by fingerprint\napp.use(clientCertificateAuth(allowFingerprints([\n  'SHA256:AB:CD:EF:...',\n  'AB:CD:EF:...'  // SHA256: prefix optional\n])));\n\n// Allowlist by Organization\napp.use(clientCertificateAuth(allowOrganization(['My Company'])));\n\n// Allowlist by Organizational Unit\napp.use(clientCertificateAuth(allowOU(['Engineering', 'DevOps'])));\n\n// Allowlist by email (checks SAN and subject.emailAddress)\napp.use(clientCertificateAuth(allowEmail(['admin@example.com'])));\n\n// Allowlist by serial number\napp.use(clientCertificateAuth(allowSerial(['01:23:45:67:89:AB:CD:EF'])));\n\n// Allowlist by Subject Alternative Name\napp.use(clientCertificateAuth(allowSAN(['DNS:api.example.com', 'email:service@example.com'])));\n```\n\n### Field Matching\n\nMatch certificates by issuer or subject fields (all specified fields must match):\n\n```javascript\n// Match by issuer\napp.use(clientCertificateAuth(allowIssuer({ O: 'My Company', CN: 'Internal CA' })));\n\n// Match by subject\napp.use(clientCertificateAuth(allowSubject({ O: 'Partner Corp', ST: 'California' })));\n```\n\n### Combining Helpers\n\n```javascript\n// AND - all conditions must pass\napp.use(clientCertificateAuth(allOf(\n  allowIssuer({ O: 'My Company' }),\n  allowOU(['Engineering', 'DevOps'])\n)));\n\n// OR - at least one condition must pass\napp.use(clientCertificateAuth(anyOf(\n  allowCN(['admin']),\n  allowOU(['Administrators'])\n)));\n```\n\n### Available Helpers\n\n| Helper | Description |\n|--------|-------------|\n| `allowCN(names)` | Match by Common Name |\n| `allowFingerprints(fps)` | Match by certificate fingerprint |\n| `allowIssuer(match)` | Match by issuer fields (partial) |\n| `allowSubject(match)` | Match by subject fields (partial) |\n| `allowOU(ous)` | Match by Organizational Unit |\n| `allowOrganization(orgs)` | Match by Organization |\n| `allowSerial(serials)` | Match by serial number |\n| `allowSAN(values)` | Match by Subject Alternative Name |\n| `allowEmail(emails)` | Match by email (SAN or subject) |\n| `allOf(...callbacks)` | AND combinator |\n| `anyOf(...callbacks)` | OR combinator |\n\n## TypeScript\n\nTypes are included:\n\n```typescript\nimport clientCertificateAuth from 'client-certificate-auth';\nimport type { ClientCertRequest } from 'client-certificate-auth';\nimport type { PeerCertificate } from 'tls';\n\nconst checkAuth = (cert: PeerCertificate): boolean =\u003e {\n  return cert.subject.CN === 'admin';\n};\n\napp.use(clientCertificateAuth(checkAuth));\n\n// Access certificate in downstream handlers\napp.get('/whoami', (req: ClientCertRequest, res) =\u003e {\n  res.json({ cn: req.clientCertificate?.subject.CN });\n});\n\n// With reverse proxy\napp.use(clientCertificateAuth(checkAuth, {\n  certificateSource: 'aws-alb'\n}));\n```\n\n## CommonJS\n\nThe main entry point works with `require()` out of the box for socket-based mTLS:\n\n```javascript\nconst clientCertificateAuth = require('client-certificate-auth');\n\napp.use(clientCertificateAuth((cert) =\u003e cert.subject.CN === 'admin'));\n```\n\nThe sync CJS wrapper supports `includeChain`, `onAuthenticated`, and `onRejected` options:\n\n```javascript\nconst clientCertificateAuth = require('client-certificate-auth');\n\napp.use(clientCertificateAuth(\n  (cert) =\u003e cert.subject.CN === 'admin',\n  {\n    includeChain: true,\n    onAuthenticated: (cert, req) =\u003e {\n      console.log(`Authenticated: ${cert.subject.CN}`);\n    }\n  }\n));\n```\n\n### Full Features via `load()`\n\nReverse proxy support (header-based certificate extraction) requires async initialization. Use the `load()` function to get the full-featured ESM module:\n\n```javascript\nconst { load } = require('client-certificate-auth');\n\nasync function setup() {\n  const clientCertificateAuth = await load();\n\n  app.use(clientCertificateAuth(checkAuth, {\n    certificateSource: 'aws-alb'  // Now supported\n  }));\n}\n\nsetup();\n```\n\nThe `load()` function dynamically imports the ESM module and caches it. Subsequent calls return the cached module immediately.\n\n### CJS Limitations\n\n| Feature | `require()` (sync) | `load()` (async) |\n|---------|---------------------|-------------------|\n| Socket-based mTLS | Yes | Yes |\n| `includeChain` | Yes | Yes |\n| `onAuthenticated` / `onRejected` | Yes | Yes |\n| `certificateSource` presets | No | Yes |\n| `certificateHeader` / `headerEncoding` | No | Yes |\n| `verifyHeader` / `verifyValue` | No | Yes |\n| `fallbackToSocket` | No | Yes |\n\n### Subpath Exports in CJS\n\nThe `/helpers`, `/parsers`, and `/extractor` subpath exports each provide a `load()` function for async access in CommonJS. The individual functions are not synchronously available via `require()`.\n\n```javascript\n// Helpers\nconst { load } = require('client-certificate-auth/helpers');\nconst { allowCN, allOf, allowIssuer } = await load();\n\n// Extractor\nconst { load: loadExtractor } = require('client-certificate-auth/extractor');\nconst { extractClientCertificate } = await loadExtractor();\n```\n\nAlternatively, you can use dynamic `import()`:\n\n```javascript\nconst { allowCN } = await import('client-certificate-auth/helpers');\n```\n\n## Testing\n\nThis library has comprehensive test coverage across multiple layers:\n\n| Layer | Description |\n|-------|-------------|\n| **Unit tests** | 100% line/branch/function/statement coverage, enforced in CI |\n| **Integration tests** | Real HTTPS servers with mTLS handshakes |\n| **E2E proxy tests** | Docker containers running nginx, Envoy, and Traefik with actual certificate forwarding |\n| **Mutation testing** | [Stryker](https://dashboard.stryker-mutator.io/reports/github.com/tgies/client-certificate-auth/master) verifies tests detect code changes |\n\nThe E2E tests spin up real reverse proxies, generate fresh certificates, and verify the middleware correctly parses each proxy's header format through a variety of successful and failed authentication attempts.\n\n## Security Notes\n\n- Set `rejectUnauthorized: false` on your HTTPS server to let this middleware provide helpful error messages, rather than dropping connections silently\n- **When using header-based auth**, ensure your proxy strips certificate headers from external requests\n- Use `verifyHeader`/`verifyValue` as defense-in-depth when using header-based authentication\n\n## Troubleshooting\n\n### `DEPTH_ZERO_SELF_SIGNED_CERT` error\n\nThis error occurs when the TLS layer rejects a self-signed client certificate. Set `rejectUnauthorized: false` in your HTTPS server options to let the middleware handle authorization instead of dropping the connection:\n\n```javascript\nconst opts = {\n  key: fs.readFileSync('server.key'),\n  cert: fs.readFileSync('server.pem'),\n  ca: fs.readFileSync('ca.pem'),\n  requestCert: true,\n  rejectUnauthorized: false  // Required for self-signed certs\n};\n\nhttps.createServer(opts, app).listen(443);\n```\n\n\u003e **Warning:** In production, prefer certificates signed by your own CA rather than self-signed certificates. If you must use self-signed certs, ensure you set `ca` to the self-signed certificate so Node.js can verify the chain.\n\n### Certificate not reaching middleware\n\nIf the middleware always rejects with \"socket not authorized\", verify that your HTTPS server has `requestCert: true` set. Without this option, Node.js will not ask clients for a certificate during the TLS handshake:\n\n```javascript\nconst opts = {\n  // ...\n  requestCert: true,           // Must be true\n  rejectUnauthorized: false\n};\n```\n\nAlso confirm that the client is actually sending a certificate. Tools like `openssl s_client` can verify this:\n\n```bash\nopenssl s_client -connect localhost:443 -cert client.pem -key client.key\n```\n\n### Reverse proxy headers not working\n\nWhen using header-based certificate extraction behind a reverse proxy:\n\n1. **Verify the proxy is setting the correct header.** Check your proxy logs or use a test endpoint to inspect incoming headers.\n\n2. **Ensure the `certificateSource` or `certificateHeader`/`headerEncoding` options match your proxy's configuration.** A mismatch will result in unparseable or missing certificate data.\n\n3. **Confirm the proxy strips certificate headers from external requests.** If external clients can set these headers directly, they can bypass authentication. See [Security Considerations](#security-considerations).\n\n4. **Consider using `verifyHeader`/`verifyValue`** for defense-in-depth, so the middleware validates that the proxy actually verified the certificate.\n\n### WebSocket authentication failing\n\nFor WebSocket connections using the `ws` library with `noServer: true`, you must handle the `upgrade` event yourself and run the middleware manually. The middleware needs a response-like object and a `next` callback:\n\n```javascript\nserver.on('upgrade', (req, socket, head) =\u003e {\n  const middleware = clientCertificateAuth(checkAuth);\n  const res = { writeHead: () =\u003e {}, end: () =\u003e {}, redirect: () =\u003e {} };\n\n  middleware(req, res, (err) =\u003e {\n    if (err) {\n      socket.write(`HTTP/1.1 ${err.status} ${err.message}\\r\\n\\r\\n`);\n      socket.destroy();\n      return;\n    }\n    wss.handleUpgrade(req, socket, head, (ws) =\u003e {\n      wss.emit('connection', ws, req);\n    });\n  });\n});\n```\n\nSee the full [WebSocket Support](#websocket-support) section for complete examples with `ws` and Socket.IO.\n\n### ESM vs CJS import differences\n\nThis package is an ES module (`\"type\": \"module\"` in package.json) with a CJS compatibility wrapper.\n\n**ESM** (recommended):\n```javascript\nimport clientCertificateAuth from 'client-certificate-auth';\nimport { allowCN } from 'client-certificate-auth/helpers';\n```\n\n**CJS** (sync, socket-only):\n```javascript\nconst clientCertificateAuth = require('client-certificate-auth');\n```\n\n**CJS** (async, full features):\n```javascript\nconst clientCertificateAuth = await require('client-certificate-auth').load();\n```\n\nThe sync CJS wrapper does not support reverse proxy options (`certificateSource`, `certificateHeader`, etc.). Passing these options will throw a descriptive error. Use `load()` to access the full ESM module from CJS code. See the [CommonJS](#commonjs) section for details.\n\nThe `/helpers`, `/parsers`, and `/extractor` subpath exports each provide a `load()` function in CJS. See [Subpath Exports in CJS](#subpath-exports-in-cjs) for details.\n\n## Commercial Support\n\n`client-certificate-auth` is built and maintained by [Tony Gies](https://github.com/tgies). For organizations running it in production, commercial support is available through his consultancy, Crash United, LLC.\n\n### Support Offerings\n\n| Service | Description |\n|---------|-------------|\n| **Priority bug fixes** | Reported issues triaged and patched ahead of the public queue |\n| **Custom features \u0026 integrations** | Adapters for new reverse proxies, encoding formats, or framework wrappers |\n| **mTLS architecture consulting** | Review of your certificate issuance, rotation, and trust-chain design |\n| **Deployment security review** | Threat modeling for your specific proxy + middleware + auth flow |\n| **Private security advisories** | Coordinated disclosure for vulnerabilities affecting your deployment |\n\nFor pricing, scoping, or anything not listed above, email **[support@crashunited.com](mailto:support@crashunited.com)** to discuss your needs.\n\n### Sponsorship\n\nTo support ongoing development without a formal contract, [GitHub Sponsors](https://github.com/sponsors/tgies) is the simplest path.\n\n### Enterprise Procurement\n\nThis package is enrolled in [Tidelift](https://tidelift.com/) (now part of SonarQube Advanced Security). If your organization already subscribes, `client-certificate-auth` is included in your coverage for security disclosures, license compliance, and version metadata.\n\n## License\n\nMIT © Tony Gies\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftgies%2Fclient-certificate-auth","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftgies%2Fclient-certificate-auth","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftgies%2Fclient-certificate-auth/lists"}