NodeJS Express Cert Based Mutual Auth
In this post we’ll look at how to add Certificate Based Mutual Authentication to an express HTTPS server. This will allow us to require a User Certificate before being able to communicate with out express server.
This post will assume that you already know how to setup a HTTPS server using express. If you don’t know how to, you can learn here.
Require Certificate
To enable Cert Based Mutual Auth, some additional parameters (ca
, requestCert
& rejectUnauthorized
) need to be provided in the Server Options:
1
2
3
4
5
6
7
8
9
10
const serverOptions = {
// Certificate Settings
ca: fs.readFileSync(path.join(__dirname, 'certs/custom_root_ca.crt')),
cert: fs.readFileSync(path.join(__dirname, 'certs/host.crt')),
key: fs.readFileSync(path.join(__dirname, 'certs/host.key')),
// Cert-Based Mutual Auth settings
requestCert: true,
rejectUnauthorized: true,
}
ca
specifies the Certificate Authority (CA) that was used to sign the User Certificates that will be provided.requestCert
forces the client to send a User Certificate upon connection.rejectUnauthorized
sets whether the client connection should be terminated if the Users Cert verification fails.
Custom Authorisation Logic
Custom logic can be added to the server instead of immediately rejecting the connection. We can do it by setting rejectUnauthorized
to false
and adding the following code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const path = require('path');
const fs = require('fs');
const express = require('express');
const app = express();
const serverOptions = {
// Certificate Settings
ca: fs.readFileSync(path.join(__dirname, 'certs/custom_root_ca.crt')),
cert: fs.readFileSync(path.join(__dirname, 'certs/host.crt')),
key: fs.readFileSync(path.join(__dirname, 'certs/host.key')),
// Cert-Based Mutual Auth settings
requestCert: true,
rejectUnauthorized: false,
}
const server = require('https').Server(serverOptions, app);
app.use((req, res, next) => {
try {
// Get the Certificate the User provided
let cert = req.socket.getPeerCertificate();
// The Certificate is VALID
if (req.client.authorized) {
console.log(`Certificate "${cert.subject.CN}" is VALID and was issued by "${cert.issuer.CN}"`);
next();
}
// The Certificate is NOT VALID
else if (cert.subject) {
console.log(`Certificates from "${cert.issuer.CN}" are NOT VALID. User "${cert.subject.CN}"`);
res.sendStatus(403);
}
// A Certificate was NOT PROVIDED
else {
console.log(`No Certificate provided by the client`);
res.status(403).send(`Certificate Required`);
}
} catch (err) {
console.log(err);
res.sendStatus(404);
}
});
Why Custom Logic?
Why would you do the above logic? Great question!
In general, you shouldn’t. If your application requires a certificate to be accessed period, you SHOULDN’T use custom logic. You should set rejectUnauthorized
to true
and let express do the heavy lifting.
However, there are cases when you might need to handle the logic yourself, such as only requiring a certificate to access certain parts of an application (like /admin
).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
app.use((req, res, next) => {
try {
// Get the Certificate the User provided
let cert = req.socket.getPeerCertificate();
// If the user isn't accessing Admin area, DON'T require a Certificate
if (!req.path.startsWith('/admin')) {
next();
}
// The Certificate is VALID
else if (req.client.authorized) {
console.log(`Certificate "${cert.subject.CN}" is VALID and was issued by "${cert.issuer.CN}"`);
next();
}
// The Certificate is NOT VALID
else if (cert.subject) {
console.log(`Certificates from "${cert.issuer.CN}" are NOT VALID. User "${cert.subject.CN}"`);
res.sendStatus(403);
}
// A Certificate was NOT PROVIDED
else {
console.log(`No Certificate provided by the client`);
res.status(403).send(`Certificate Required`);
}
} catch (err) {
res.sendStatus(404);
}
});
Another example might be having custom logic for logging and/or debugging purposes.
Full Code
1
npm i express
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
const PORT = 8443;
const serverOptions = {
// Certificate Settings
ca: fs.readFileSync(path.join(__dirname, 'certs/custom_root_ca.crt')),
cert: fs.readFileSync(path.join(__dirname, 'certs/host.crt')),
key: fs.readFileSync(path.join(__dirname, 'certs/host.key')),
// Cert-Based Mutual Auth settings
requestCert: true,
rejectUnauthorized: false,
}
const server = require('https').Server(serverOptions, app);
app.use((req, res, next) => {
try {
// Get the Certificate the User provided
let cert = req.socket.getPeerCertificate();
// The Certificate is VALID
if (req.client.authorized) {
console.log(`Certificate "${cert.subject.CN}" is VALID and was issued by "${cert.issuer.CN}"`);
next();
}
// The Certificate is NOT VALID
else if (cert.subject) {
console.log(`Certificates from "${cert.issuer.CN}" are NOT VALID. User "${cert.subject.CN}"`);
res.sendStatus(403);
}
// A Certificate was NOT PROVIDED
else {
console.log(`No Certificate provided by the client`);
res.status(403).send(`Certificate Required`);
}
} catch (err) {
res.sendStatus(404);
}
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
// Catch any unforeseen errors
app.use((err, __, res, ___) => res.send({ success: false }));
// Start the Server
server.listen(PORT, () => {
console.log(`[-] Server Listening on Port ${PORT}`);
});