I'm trying to set up a https server with mutual authentication.
I created the key and the certificate for the server (auto-signed).
Now I use firefox to connect to the server without providing any client certificate.
This should result in the req.socket.authorized
being false
(as stated here), but for some reason after some refreshes (and without changing anything) the message change from the right
Unauthorized: Client certificate required (UNABLE_TO_GET_ISSUER_CERT)
to
Client certificate was authenticated but certificate information could not be retrieved.
To me this is unexpected, because it means that req.socket.authorized == true
even without client certificates. Can someone explain me why is this happening?
Here my code:
const express = require('express')
const app = express()
const fs = require('fs')
const https = require('https')
// ...
const opts = { key: fs.readFileSync('./cryptoMaterial/private_key.pem'),
cert: fs.readFileSync('./cryptoMaterial/certificate.pem'),
requestCert: true,
rejectUnauthorized: false,
ca: [ fs.readFileSync('./cryptoMaterial/certificate.pem') ]
}
const clientAuthMiddleware = () => (req, res, next) => {
if (!req.secure && req.header('x-forwarded-proto') != 'https') {
return res.redirect('https://' + req.header('host') + req.url);
}
// Ensure that the certificate was validated at the protocol level
if (!req.socket.authorized) { // <-- THIS SHOULD BE ALWAYS FALSE
res.status(401).send(
'Unauthorized: Client certificate required ' +
'(' + req.socket.authorizationError + ')'
);
return
}
// Obtain certificate details
var cert = req.socket.getPeerCertificate();
if (!cert || !Object.keys(cert).length) {
// Handle the bizarre and probably not-real case that a certificate was
// validated but we can't actually inspect it
res.status(500).send(
'Client certificate was authenticated but certificate ' +
'information could not be retrieved.'
);
return
}
return next();
};
app.use(clientAuthMiddleware());
// ...
https.createServer(opts, app).listen(PORT)
I encountered the same issue a while ago and created an issue on github. It seems like it's intentional behaviour. See https://github.com/nodejs/node/issues/35317
To quote bnoordhuis answering to "I'm guessing it might be due to some TLS connection reuse logic." in the issue:
Close, it's not the connection but the TLS session that's reused. :-)
Reuse cuts the handshake short (and cuts out the client certificate exchange) because it reuses the previously established session parameters. That's per spec and normally what you want. Chromium probably creates a new session when you reload.
[...]
socket.authorized
isfalse
when a verification error happened during the handshake (e.g. invalid or untrusted certificate) buttrue
otherwise.A new connection started from a resumed session doesn't do that verification and hence assumes
socket.authorized = true
. The nature of TLS sessions is such that I'm not sure this can be fixed even if we wanted to.
As a workaround, you should disable TLS renegotiation and force a new TLS session for each connection, which can be done only on TLSv1.2 as far as I know.
Here's an example on how I'm achieving it with Typescript:
import fs from 'fs';
import path from 'path';
import https from 'https';
import tls from 'tls';
import express from 'express';
const expressApp = express();
if (tls.DEFAULT_MAX_VERSION !== "TLSv1.2") {
throw Error('Specify --tls-max-v1.2 as a node option (see https://github.com/nodejs/node/issues/35317)');
}
const httpsOptions = {
key: fs.readFileSync(path.join('certs', 'key')),
cert: fs.readFileSync(path.join('certs', 'cert')),
ca: fs.readFileSync(path.join('certs', 'ca')),
// crl: fs.readFileSync(path.join('certs', 'crl')), /* Enable this if you have a CRL */
requestCert: true,
rejectUnauthorized: false
};
https.createServer(httpsOptions, expressApp);
/* Authentication middleware */
expressApp.use((req, res, next) => {
let tlsSocket = (req.socket as tls.TLSSocket);
if (tlsSocket.isSessionReused()) {
/* Force renegotiation (see https://github.com/nodejs/node/issues/35317) */
tlsSocket.renegotiate({ rejectUnauthorized: false, requestCert: true }, (err) => {
if (!(tlsSocket as tls.TLSSocket).authorized) {
console.log('Unauthorized');
return res.status(401).send('Unauthorized');
}
});
}
else {
if (!(tlsSocket as tls.TLSSocket).authorized) {
console.log('Unauthorized');
return res.status(401).send('Unauthorized');
}
}
next();
});