Post

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.

  1. Require Certificate
  2. Custom Authorisation Logic
  3. Full Code

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}`);
});
This post is licensed under CC BY 4.0 by the author.