Working with more advanced services sometimes requires two-way SSL to be established between the services. Let's see how we can do it using fetch.
If you don't know what TLS/SSL with client certificate authentication is, what two-way SSL means or is used for, I'd recommend you read this as a primer: https://stackoverflow.com/a/10725958
There's no lack of resources for how to get two-way SSL (or authorisation using SSL certificates) in node.js but most articles, when referring to the client either use request or plain https.
Here's a few examples of the resources I found to be particularly useful if that is what you're looking for:
request
fetch
and not request
or https
?For me it was the ability to use await
like on the web and because I like the way fetch handles parameters more than https
and that it is lighter as a tool compared to request
.
I'm going to assume you already have your certificates for the client available. If not, please use the first article recommended as a good starting point. In our case the certificates were already provided by a 3rd party so we didn't generate them ourselves. If you're working with Open Banking or other such APIs that frequently want to limit client access you may find yourself in a similar situation.
So, how does it work?
Let's look at the signature that fetch
has in node.js:
// fetch(url[, options])
// Options can be:
{
// These properties are part of the Fetch Standard
method: 'GET',
headers: {}, // request headers. format is the identical to that accepted by the Headers constructor (see below)
body: null, // request body. can be null, a string, a Buffer, a Blob, or a Node.js Readable stream
redirect: 'follow', // set to `manual` to extract redirect headers, `error` to reject redirect
signal: null, // pass an instance of AbortSignal to optionally abort requests
// The following properties are node-fetch extensions
follow: 20, // maximum redirect count. 0 to not follow redirect
timeout: 0, // req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies). Signal is recommended instead.
compress: true, // support gzip/deflate content encoding. false to disable
size: 0, // maximum response body size in bytes. 0 to disable
agent: null // http(s).Agent instance or function that returns an instance (see below)
}
Did you spot it? The gotcha is the agent
option, which as the docs mention doesn't exist on the web.
A node agent is resposible for managing persistence and reuse of client http(s) connections. It also takes care of destroying sockets or keeping them alive in case we make another request to the same address and port (not to be confused with the Connection: Keep-Alive
header).
So all we have to do to use fetch
with SSL client certificate authentication is to create a new https.Agent.
In our case, we had a public key file (.pem) and private key file (.key) with a passphrase so we'll be using those. Here's how that looks like:
// index.js
// using `esm` to get imports to work in node
import path from 'path';
import fs from 'fs';
import https from 'https';
import fetch from 'node-fetch';
const reqUrl = 'https://your-server.com';
const headers = {
Accept: 'application/json',
// add what you need like you would normally
};
async function makeRequest() {
// you can also pass a ca or a pfx cert and much more! https.Agent uses the same options as tls.createSecureContext:
// https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options
const options = {
// when using this code in production, for high throughput you should not read
// from the filesystem for every call, it can be quite expensive. Instead
// consider storing these in memory
cert: fs.readFileSync(
path.resolve(__dirname, './path/to/public-cert.pem'),
`utf-8`,
),
key: fs.readFileSync(
path.resolve(__dirname, './path/to/private-key.key'),
'utf-8',
),
passphrase:
'<your-passphrase>',
// in test, if you're working with self-signed certificates
rejectUnauthorized: false,
// ^ if you intend to use this in production, please implement your own
// `checkServerIdentity` function to check that the certificate is actually
// issued by the host you're connecting to.
//
// eg implementation here:
// https://nodejs.org/api/https.html#https_https_request_url_options_callback
keepAlive: false, // switch to true if you're making a lot of calls from this client
};
// we're creating a new Agent that will now use the certs we have configured
const sslConfiguredAgent = new https.Agent(options);
try {
// make the request just as you would normally ...
const response = await fetch(reqUrl, {
headers: headers, // ... pass everything just as you usually would
agent: sslConfiguredAgent, // ... but add the agent we initialised
});
const responseBody = await response.json();
// handle the response as you would see fit
console.log(responseBody);
} catch (error) {
console.log(error);
}
}
makeRequest();
That's all, you should be able to swap in the path for where your certificates are, the passphrase (if you have one) and the URL and you're good to go.
In order to have this really be useful in production, I would really recommend implementing your own checkServerIdentity
function and preferably pin the certificate based on a fingerprint.
If you end up finding this useful (or have issues with this) please reach out, ping me on twitter and I'll help you out!
Good luck!