Skip to main content

SSL for Local NodeJS Development

More often than not, running a NodeJS process on a port of localhost is enough for your development environment. If you ever need more than one service running at the same time, you can set a unique port for each of them. localhost:3000 for App 1, localhost:3001 for App 2, and so on.

I've recently started working on an authentication service for a set of applications, and I chose Open ID Connect (OIDC) to do it. It's a pretty big topic, but written into the specification is a requirement that redirect URLs have to start with https.

I could deploy these to a test server on a hosting provider, that would work. I'm still learning and making small, quick changes, so waiting for the deploy each time would slow things down. Until the service is stable, running everything locally would speed the process up a lot.

# The Approach

The idea here is to run a lightweight web server on https and have it act as an intermediary to our usual locally-running Node process. This is very similar to how a production environment works - the approach is called "proxying", making the server doing it a "proxy server". I'm going to use Caddy for this.

We can't use a free Let's Encrypt certificate to secure our server, and a self-signed certificate will be flagged by Node and the browser as insecure. Instead, we'll create our own local Certificate Authority and generate a certificate that way.

# Install the Web Server

Caddy is designed to be SSL by default, so it will try to use Let's Encrypt when it starts. We'll deal with that in the next step. For now, we'll install it using Homebrew.

brew install caddy

# Create a local Certificate Authority

Next, we need to install a tool called mkcert. This tool will install a local, trusted CA and we can use it to create our own certificates. There are installation instructions (including Windows) on the GitHub project.

If you're using Firefox, like I am, you'll also need NSS.

brew install mkcert
brew install nss # Only for Firefox

Once installed, run the -install action to create a local CA in your system trust store. It will create a root certificate and you should pay close attention to the warning about not sharing it.

mkcert -install

# Restart your Browser

This might only be specific to Firefox, because of the additional install requirement.

# Create a Certificate

I'm going to do this in my project folder, because running this next command creates the certificate in the current directory.

I'll be using howdee.test throughout this example, so swap this out for your own domain. I need subdomains for my purposes, but you could use different domain names for each service.

mkcert "*.howdee.test"

I don't want these in the root of my project, so I'm going to put them in a folder.

mkdir -p .caddy/certs
mv _wildcard.howdee.test* ./.caddy/certs

# Create a caddyfile

Caddy can be used purely with command line arguments, but it also offers a caddyfile as a small, convenient configuration file. If you're familiar with other servers, it's like setting up a <VirtualHost> in Apache server or a Server Block in nginx.

I'm going to put this in the project's ./.caddy folder. In it, we'll define our two local sites, proxy them to our Node services running on each port, and give it the certificate it needs to secure the connection.

# Our main Web application
app.howdee.test {
# Our App runs on port 3001, forward everything there
reverse_proxy localhost:3001

# Path to my App service public folder
root * ./apps/app/public

# Serve files like images, CSS files, and other assets

# Specify the certificate to use
tls ./certs/_wildcard.howdee.test.pem ./certs/_wildcard.howdee.test-key.pem

# The Authentication service running an OIDC Provider
auth.howdee.test {
# Our Auth service runs on port 3002, forward everything there
reverse_proxy localhost:3002

# Path to my Auth service public folder
root * ./apps/auth/public

# Serve files like images, CSS files, and other assets

# Specify the certificate to use
tls ./certs/_wildcard.howdee.test.pem ./certs/_wildcard.howdee.test-key.pem

# Point our .test domains to localhost

If you haven't put anything else in place, you'll need to edit your /etc/hosts file to point your test domains to localhost. This will stop your browser going to the wider Web to look for an address that doesn't exist.

# Host Database
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
## localhost broadcasthost
::1 localhost

+ app.howdee.test
+ auth.howdee.test

# Using in a Node project

Now, when you're working on your project, you can run the server alongside your local processes. So, if you have your Node processes running in on terminal tab, in another you'd run:

# From your project root folder...
cd .caddy
caddy run

This is will start Caddy in your terminal using the caddyfile to define the servers above. If you now visit your local domain, you should get a response from a secured HTTPS connection.

If you hover over the padlock, or view the secure link information, you should see that it is verified by our "mkcert development CA".

# Running remote calls from NodeJS

Running this in the browser was fine, but my app runs a remote request to one of my secure services - like an asynchronous fetch request. I was getting an error along these lines:

Error: unable to verify the first certificate
at TLSSocket.onConnectSecure (node:_tls_wrap:1540:34)
at TLSSocket.emit (node:events:513:28)
at TLSSocket._finishInit (node:_tls_wrap:959:8)
at ssl.onhandshakedone (node:_tls_wrap:743:12) {

Node has its own set of trusted certificate authorities and doesn't automatically know about our's. You can make it aware by putting the path to your mkcert root certificate in an environment var.

I added the following line to my .env file, restarted the service, and it all worked correctly.

NODE_EXTRA_CA_CERTS: '/Users/{YOUR_MACOS_USER}/Library/Application Support/mkcert/rootCA.pem'