Getting Started
Installation
npm install client-certificate-authRequirements: Node.js >= 20
What This Library Does
This 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).
The certificate is parsed into a standard tls.PeerCertificate object and passed to your callback for authorization logic.
Compatible with Express, Connect, or any Node.js HTTP server framework by using the framework-agnostic extractClientCertificate function.
Basic Setup
Configure your HTTPS server to request and validate client certificates:
import express from 'express';
import https from 'node:https';
import fs from 'node:fs';
import clientCertificateAuth from 'client-certificate-auth';
const app = express();
// Validate certificate against your authorization rules
const checkAuth = (cert) => {
return cert.subject.CN === 'trusted-client';
};
// Apply to all routes
app.use(clientCertificateAuth(checkAuth));
app.get('/', (req, res) => {
res.send('Authorized!');
});
// HTTPS server configuration
const opts = {
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.pem'),
ca: fs.readFileSync('ca.pem'), // CA that signed client certs
requestCert: true, // Request client certificate
rejectUnauthorized: false // Let middleware handle errors
};
https.createServer(opts, app).listen(443);The key points in this setup:
requestCert: truetells Node.js to ask the client for a certificate during the TLS handshake.rejectUnauthorized: falseprevents Node.js from silently dropping connections when certificate validation fails, letting the middleware provide helpful error messages instead.caspecifies the CA certificate(s) that signed your client certificates.
Per-Route Protection
You don't have to protect every route. Use clientCertificateAuth as per-route middleware to protect only specific endpoints:
app.get('/public', (req, res) => {
res.send('Hello world');
});
app.get('/admin', clientCertificateAuth(checkAuth), (req, res) => {
res.send('Hello admin');
});Async Authorization
The authorization callback supports async functions, letting you check certificates against databases, external services, or other asynchronous sources:
const checkAuth = async (cert) => {
const user = await db.findByFingerprint(cert.fingerprint);
return user !== null;
};
app.use(clientCertificateAuth(checkAuth));Custom Error Messages
Throw errors for granular authorization feedback instead of returning false:
const checkAuth = (cert) => {
if (isRevoked(cert.serialNumber)) {
throw new Error('Certificate has been revoked');
}
if (!allowlist.includes(cert.fingerprint)) {
throw new Error('Certificate not in allowlist');
}
return true;
};
// Thrown errors are passed to Express error handlers with:
// - error.message = your custom message
// - error.status = 401 (unless you set a different status)To use a different status code, set it on the error before throwing:
const err = new Error('Access forbidden');
err.status = 403;
throw err;Audit Logging Hooks
Use onAuthenticated and onRejected hooks to log authentication decisions without affecting request processing:
app.use(clientCertificateAuth(checkAuth, {
onAuthenticated: (cert, req) => {
logger.info('mTLS auth success', {
cn: cert.subject.CN,
fingerprint: cert.fingerprint,
path: req.url,
ip: req.ip
});
},
onRejected: (cert, req, reason) => {
logger.warn('mTLS auth failed', {
cn: cert?.subject?.CN,
reason,
path: req.url,
ip: req.ip
});
}
}));Hook characteristics:
- Fire-and-forget: Hooks don't block request processing. Async hooks run in the background.
- Error-safe: Hook errors are caught and logged to
console.error, never affecting the request. - Cert may be null: In
onRejected,certisnullwhen certificate extraction failed (socket not authorized, header missing, etc.)
Rejection reasons:
| Reason | Description |
|---|---|
socket_not_authorized | TLS socket authorization failed |
certificate_not_retrievable | Socket authorized but cert couldn't be read |
header_missing_or_malformed | Certificate header absent or unparseable |
verification_header_mismatch | Proxy verify header didn't match expected value |
callback_returned_false | Your callback returned false |
| (error message) | Your callback threw an error |
API Overview
clientCertificateAuth(callback, options?)
Returns Express middleware that extracts a client certificate and passes it to your callback for authorization.
Parameters:
| Name | Type | Description |
|---|---|---|
callback | (cert, req?) => boolean | Promise<boolean> | Receives the client certificate and request, returns true to allow access |
options.certificateSource | string | Use a preset for a known proxy: 'aws-alb', 'envoy', 'cloudflare', 'traefik' |
options.certificateHeader | string | Custom header name to read certificate from |
options.headerEncoding | string | Encoding format: 'url-pem', 'url-pem-aws', 'xfcc', 'base64-der', 'rfc9440' |
options.fallbackToSocket | boolean | If header extraction fails, try socket.getPeerCertificate() (default: false) |
options.includeChain | boolean | If true, include full certificate chain via cert.issuerCertificate (default: false) |
options.verifyHeader | string | Header name containing verification status from proxy (e.g., 'X-SSL-Client-Verify') |
options.verifyValue | string | Expected value indicating successful verification (e.g., 'SUCCESS') |
options.onAuthenticated | (cert, req) => void | Called on successful authentication (fire-and-forget) |
options.onRejected | (cert, req, reason) => void | Called on authentication failure (fire-and-forget) |
Certificate Object:
The cert parameter contains fields from tls.PeerCertificate:
subject.CN- Common Namesubject.O- Organizationissuer- Issuer informationfingerprint- Certificate fingerprintvalid_from,valid_to- Validity periodissuerCertificate- Issuer's certificate (only whenincludeChain: true)
For the full generated API reference, see the API docs.
extractClientCertificate(req, options?)
Framework-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.
Parameters:
| Name | Type | Description |
|---|---|---|
req | Object | Request object with headers and optional socket |
req.headers | Record<string, string | string[]> | HTTP headers object |
req.socket | Object | Optional TLS socket (for socket-based extraction) |
options | Object | Same options as middleware (except onAuthenticated/onRejected) |
Returns: ExtractionResult
{
success: boolean;
certificate: PeerCertificate | null;
reason: string | null; // Rejection reason if success is false
}Rejection reasons:
'verification_header_mismatch'— Proxy verify header didn't match expected value'header_missing_or_malformed'— Header extraction failed and no fallback configured'socket_not_authorized'— Socket not authorized for TLS client cert'certificate_not_retrievable'— Socket authorized butgetPeerCertificate()returned empty
Example - Building a Koa adapter:
import { extractClientCertificate } from 'client-certificate-auth/extractor';
function koaClientCert(checkAuth, options = {}) {
return async (ctx, next) => {
const result = extractClientCertificate(ctx.req, options);
if (!result.success) {
ctx.throw(401, result.reason);
}
ctx.state.clientCertificate = result.certificate;
const allowed = await checkAuth(result.certificate, ctx.req);
if (!allowed) {
ctx.throw(401, 'Certificate not authorized');
}
await next();
};
}
// Usage
app.use(koaClientCert(
(cert) => cert.subject.CN === 'admin',
{ certificateSource: 'aws-alb' }
));Example - Custom authentication flow:
import { extractClientCertificate } from 'client-certificate-auth/extractor';
app.post('/api/login', (req, res) => {
// Extract certificate without middleware
const result = extractClientCertificate(req, {
certificateSource: 'envoy',
fallbackToSocket: true
});
if (!result.success) {
return res.status(401).json({ error: result.reason });
}
// Custom auth logic
const user = lookupUserByCertFingerprint(result.certificate.fingerprint);
if (!user) {
return res.status(403).json({ error: 'Certificate not registered' });
}
// Issue session token
const token = createSessionToken(user);
res.json({ token, user });
});For the full generated API reference, see the extractor docs.
Accessing the Certificate
After authentication, the certificate is attached to req.clientCertificate for downstream handlers:
app.use(clientCertificateAuth(checkAuth));
app.get('/whoami', (req, res) => {
res.json({
cn: req.clientCertificate.subject.CN,
fingerprint: req.clientCertificate.fingerprint
});
});The certificate is attached before the authorization callback runs, so it's available even if authorization fails (useful for logging).
Certificate Chain Access
For enterprise PKI scenarios, you may need to inspect intermediate CAs or the root CA:
app.use(clientCertificateAuth((cert) => {
// Check issuer's organization
if (cert.issuerCertificate) {
return cert.issuerCertificate.subject.O === 'Trusted Root CA';
}
return false;
}, { includeChain: true }));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.
User Login Patterns
Client certificates provide cryptographically-verified identity, making them ideal for user authentication. Map certificate fields to user accounts in your database:
app.use(clientCertificateAuth(async (cert) => {
// Option 1: Lookup by fingerprint (most secure - immutable per certificate)
const user = await db.users.findOne({ certFingerprint: cert.fingerprint });
// Option 2: Lookup by email (from subject or SAN)
// const user = await db.users.findOne({ email: cert.subject.emailAddress });
// Option 3: Lookup by Common Name
// const user = await db.users.findOne({ certCN: cert.subject.CN });
if (!user) {
throw new Error('Certificate not registered to any user');
}
return true;
}));To make the user available to downstream handlers, attach it to the request:
app.use(clientCertificateAuth(async (cert, req) => {
const user = await db.users.findOne({ certFingerprint: cert.fingerprint });
if (!user) throw new Error('Unknown certificate');
req.user = user; // Attach for downstream routes
return true;
}));
app.get('/profile', (req, res) => {
res.json({
name: req.user.name,
certificateCN: req.clientCertificate.subject.CN
});
});Lookup strategies:
| Field | Pros | Cons |
|---|---|---|
fingerprint | Unique, immutable | Must register each cert |
subject.emailAddress | Human-readable | Ensure uniqueness |
subject.CN | Simple to configure | May not be unique |
serialNumber + issuer | Traceable to your CA | More complex queries |
Ecosystem
This package provides everything you need to build mTLS authentication for any Node.js framework:
- Certificate extraction via
extractClientCertificate()- handles both socket and header-based extraction - Authorization helpers - reusable validation callbacks for common patterns (
allowCN,allowFingerprints, etc.) - Parser library - decode certificates from various reverse proxy formats (Envoy XFCC, AWS ALB, Cloudflare, etc.)
- Type definitions - full TypeScript support
Official framework adapters:
- passport-client-certificate-auth - Passport.js strategy for mTLS authentication
Community adapters:
If you build an adapter for another framework (Koa, Fastify, Hapi, NestJS, etc.), please open an issue or PR to get it listed here!
Test Coverage
This library has comprehensive test coverage across multiple layers:
| Layer | Description |
|---|---|
| Unit tests | 100% line/branch/function/statement coverage, enforced in CI |
| Integration tests | Real HTTPS servers with mTLS handshakes |
| E2E proxy tests | Docker containers running nginx, Envoy, and Traefik with actual certificate forwarding |
| Mutation testing | Stryker verifies tests detect code changes |
The 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.
Next Steps
Now that you have the basics, explore the rest of the guide:
- Reverse Proxy Support - Configure header-based certificate extraction for AWS ALB, Envoy, Cloudflare, Traefik, and custom proxies
- WebSocket Support - Authenticate WebSocket connections with
wsand Socket.IO - Authorization Helpers - Use pre-built validators for CN, fingerprint, issuer, OU, and more
- TypeScript & CJS - TypeScript types and CommonJS usage
- Troubleshooting - Common issues and solutions
- End-to-End mTLS Demo - A runnable example demonstrating the full mTLS flow