loading...

Node, Express, SSL Certificate: Run HTTPS Server from scratch in 5 steps

omergulen profile image Ömer Gülen ・8 min read

Node, Express, SSL Certificate: Run HTTPS Server from scratch in 5 steps

I've decided to write about this tutorial after I struggled while I was coding one of my web apps for a customer. It was a simple chart for the web but it was collecting data on a Telegram Bot. Held the data in MongoDB and prepared a quick API for fetching the data but got many problems on the way and SSL Certificate was one of them.

So in this tutorial, I will go through my mistakes & problems and my solutions to them, if you want to skip straight to the short version, you can continue from here.

In this article I will not mention MongoDB related code or problems.

1. Creating My Basic API Server with Express

In my projects, I prefer creating an npm or yarn environment after creating the project folder.

So, I've done it with the following commands:

mkdir my-project && cd my-project
yarn init

Just spammed Enter after yarn init and created the project environment with default settings.

(I prefer yarn over npm if there are no obstacles to use it.)

Then, I installed express to my project, locally with:

yarn add express

You can also use:

npm install express

Then, I created my single source file index.js and inserted these lines below:

// import express
const express = require('express');

// create new express app and assign it to `app` constant
const app = express();

// server port configuration
const PORT = 8080;

// create a route for the app
app.get('/', (req, res) => {
  res.send('Hello dev.to!');
});

// server starts listening the `PORT`
app.listen(PORT, () => {
  console.log(`Server running at: http://localhost:${PORT}/`);
});

So far, I imported the express package, created an instance of it and assigned it to the app. Set my PORT variable, and created a route for endpoint handling in my API Server and called app.list(PORT, callback()) method to start my server listening on the specified port.

Went back to my terminal and executed the command below in my project directory:

node index.js

which starts my server and logs to the console as below:

Server running at http://localhost:8080/

Then, I switched to my browser and browsed to http://localhost:8080/ and the following page appeared:

Alt Text

So far so good. My app is correctly listening to my port.
Afterwards, I've tested my initial trial works and wanted to test if I can handle more endpoints. So I've just added another route to my code.

app.get('/omergulen', (req, res) => {
  res.send('Hello Omer! Welcome to dev.to!');
});

I expect this to work only when I entered /omergulen endpoint in my browser.

So, I've stopped my running server with Control+C and re-started again, since hot-reloading is not inherent with how I run my app. Switched to my browser and visited the http://localhost:8080/omergulen and it was working, to be sure I re-visited the http://localhost:8080/ and it was also working as expected.

Alt Text

2. Why and how to use middleware with Express?

After my first API server deploys, I switched to my web app project and sent a fetch request to my API endpoint.

fetch('MY_API_URL')
  .then(function (response) {
    console.log(response);
    return response.json();
  })
  .then(...);

Nothing was happening in my DOM, but the console message was frustrating.

Access to fetch at 'MY_API_URL' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

App.js:34 Cross-Origin Read Blocking (CORB) blocked cross-origin response MY_API_URL with MIME type application/json. See https://www.chromestatus.com/feature/5629709824032768 for more details.

After doing some quick research, I've realized I needed to configure my API Server according to the CORS Policy.

First, I've added added mode: 'cors' to my fetch request:

fetch('MY_API_URL', {
  mode: 'cors'
})
.then(function (response) {
  console.log(response);
  return response.json();
})
.then(...);

It was alone no use to my problem. Then, I've added my cors middleware to my API server with only two lines actually.

After installing cors package with:

yarn add cors

I just added these lines to my code:

// import `cors` package
const cors = require('cors');

// use middleware
app.use(cors());

And after running with these configurations, my problem was solved, for now.

3. How to serve Express API Server as HTTPS?

To deploy, I moved my project to my VPS and redirected my my_api_url domain to this VPS. In that way I've put a small layer of abstraction to my server IP, Also, I wouldn't need to type my IP everywhere instead I could use my own domain with fancy subdomains like api.omergulen.com.

In this step, I first tried to deploy it without certification on HTTP.

[blocked] The page at 'https://my_web_app'  was loaded over HTTPS but ran insecure content from 'http://my_api_url': this content should also be loaded over HTTPS.

Yet, my webserver was being server on Firebase Hosting and it was served as https, sending a request from HTTPS to HTTP is called Mixed Content. And it is not allowed to.

So, I just put s at the beginning of the URL :))

https://my_api_url as you can guess, it didn't work either.

GET https://my_api_url net::ERR_SSL_PROTOCOL_ERROR

Then, after doing focused research I've realized that I needed to create a certificate with a Certificate Authority. Many Certificate Authorities were paid but not Let's Encrypt.

Let’s Encrypt is a free, automated, and open Certificate Authority.

If you have shell access to your server, it suggests you use certbot.

In the certbot website, I chose:

My HTTP website is running None of the above on Ubuntu 16.04 (xenial) which was fitting to my case.

Before starting they want you to be sure to have:

  • comfort with the command line
  • and an HTTP website (API Server in my case)
  • which is online
  • and serving on HTTP port (80)
  • which is hosted on a server
  • which you can access via SSH
  • with the ability to sudo

Then just apply the steps on the below:

1. SSH into the server

SSH into the server running your HTTP website as a user with sudo privileges.

2. Add Certbot PPA

You'll need to add the Certbot PPA to your list of repositories. To do so, run the following commands on the command line on the machine:

sudo apt-get update &&
sudo apt-get install software-properties-common &&
sudo add-apt-repository universe &&
sudo add-apt-repository ppa:certbot/certbot &&
sudo apt-get update

3. Install Certbot

Run this command on the command line on the machine to install Certbot.

sudo apt-get install certbot

4. Choose how you'd like to run Certbot

Are you ok with temporarily stopping your website?

Yes, my web server is not currently running on this machine.

Stop your web server, then run this command to get a certificate. Certbot will temporarily spin up a webserver on your machine.

sudo certbot certonly --standalone

No, I need to keep my webserver running.

If you have a web server that's already using port 80 and don't want to stop it while Certbot runs, run this command and follow the instructions in the terminal.

sudo certbot certonly --webroot

In this step, you need to insert your domain into the terminal such as dev.to. After that it will check your web server and look for specific files which it will create and in case of success it should print out like that:

Performing the following challenges:
http-01 challenge for my_api_url
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/my_api_url/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/my_api_url/privkey.pem
   Your cert will expire on 2020-04-01. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

Important Note:
To use the webroot plugin, your server must be configured to serve files from hidden directories. If /.well-known is treated specially by your webserver configuration, you might need to modify the configuration to ensure that files inside /.well-known/acme-challenge are served by the webserver.

4. Installing signed certificate to Express API Server

You'll need to install your new certificate in the configuration file for your API Server.

First, you need to install and import some modules:

yarn add https
// import packages
const https = require('https');
const fs = require('fs');

// serve the API with signed certificate on 443 (SSL/HTTPS) port
const httpsServer = https.createServer({
  key: fs.readFileSync('/etc/letsencrypt/live/my_api_url/privkey.pem'),
  cert: fs.readFileSync('/etc/letsencrypt/live/my_api_url/fullchain.pem'),
}, app);

httpsServer.listen(443, () => {
    console.log('HTTPS Server running on port 443');
});

If you also want to maintain HTTP requests among the HTTPS requests, you can add the following lines, too:

const http = require('http');

// serve the API on 80 (HTTP) port
const httpServer = http.createServer(app);

httpServer.listen(80, () => {
    console.log('HTTP Server running on port 80');
});

In the end your final API Server code will be something like that:

// import required packages
const express = require('express');
const cors = require('cors');

const https = require('https');
const http = require('http');

const fs = require('fs');


const app = express();
app.use(cors());

// create new express app and save it as "app"
const app = express();
app.use(cors());

// create a route for the app
app.get('/', (req, res) => {
  res.send('Hello dev.to!');
});

// another route
app.get('/omergulen', (req, res) => {
  res.send('Hello Omer! Welcome to dev.to!');
});

// Listen both http & https ports
const httpServer = http.createServer(app);
const httpsServer = https.createServer({
  key: fs.readFileSync('/etc/letsencrypt/live/my_api_url/privkey.pem'),
  cert: fs.readFileSync('/etc/letsencrypt/live/my_api_url/fullchain.pem'),
}, app);

httpServer.listen(80, () => {
    console.log('HTTP Server running on port 80');
});

httpsServer.listen(443, () => {
    console.log('HTTPS Server running on port 443');
});

5. Automatic Renewal and Test of the Certificate

The Certbot packages on your system come with a cron job or systemd timer that will renew your certificates automatically before they expire. You will not need to run Certbot again, unless you change your configuration. You can test automatic renewal for your certificates by running this command:

sudo certbot renew --dry-run

The command to renew certbot is installed in one of the following locations:

/etc/crontab/
/etc/cron.*/*
systemctl list-timers

If you needed to stop your webserver to run Certbot, you'll want to edit the built-in command to add the --pre-hook and --post-hook flags to stop and start your web server automatically. For example, if your webserver is HAProxy, add the following to the certbot renew command:

--pre-hook "service haproxy stop" --post-hook "service haproxy start"

More information is available in the Certbot documentation on renewing certificates.

Confirm that Certbot worked

To confirm that your site is set up properly, visit https://yourwebsite.com/ in your browser and look for the lock icon in the URL bar. If you want to check that you have the top-of-the-line installation, you can head to https://www.ssllabs.com/ssltest/.

Well done! You have come to the end of this long tutorial.

After applying these steps you can finally go to your API Server URL and you should be seeing Hello dev.to!.

Thanks for reading

I hope this tutorial was helpful enough.

You can check my last article here:

Feel free to reach out to me at omrglen@gmail.com.

I’m opened to suggestions & requests for future articles, cya 😃

Happy New Year! 🥳🥳🥳

Posted on by:

Discussion

markdown guide
 

It is best practice to put Node behind Nginx or Apache for several reasons:

  1. Offloading encryption
  2. Offloading Node when serving static content
  3. Handling errors when Node crashed or is restarting
  4. Load balancing
  5. Cache control ...
 
 

Why not to wrap components into containers (docker), start them as a stack and add one more component - proxy server based on smth like Caddy, that will give you pretty simple and smooth https with built in certificates from let's encrypt?

 

Maybe it's simpler or not. I am not experienced on that, please do write a tutorial about it so I can use it in the future, too. Thanks!

 

Well in my experience, it is much better to run Node server behind the defense line of Apache or Nginx server.

Both provides easy way of handling multiple web apps of different kinds with a most trusted security around the world ;)...

 

It's probably a better way to do, yet tutorial is about serving directly on Node with a certificate. Thanks :)

 

Followed every single step. Great article.

 
 

or is there any advantage of this over nginx ?

 

Personally I would avoid handling certs in Node, if possible.. that stuff should be handled by the webserver forwarding traffic to your Node application.

 

Can you give further details about your reasons, please? I would like to learn more about the reason behind it.

 

Hi, sorry for my late reply.
So there are a lot of reasons for taking out certificate handling from your application.

I could list many of the reasons here, but it's easier for me to refer to this article which explains lots of the issues: medium.com/intrinsic/why-should-i-...

tldr; :

  • use a reverse proxy to send requests to your Node application
  • let the proxy handle certificates
  • this scales much better (think multiple servers)
  • performance is better (check benchmarks, it's remarkable)
  • enables easier http->https handling

Thanks for your further explanation, I will check them out!

 

Hi Omer, very great article and guide - very generous of you. My only little issue with this is the oversight from your part to suggest, as pointed out in the comments, that there is another evidently better method of handling ssl certificates in a node.js setup.

I was about to delve in and start searching for the location of my private keys and certificate in my vps (for it already has an installed and paid for certificate) till I decided to check the comments here and realized that indeed there is a better way than exposing my private keys in my app and its many third party packages.

Please suggest to future readers, preferably at the top of the article, that there is another method of implementing this and it is recommended for performance, security and scalability.

Thank you for your effort and great article.

 

I'm having problem accessing my app with https, I'm getting ERR_SSL_PROTOCOL_ERROR.
could you please help me fix it

 

Thanks! Though, it looks like you've added this bit twice causing an error:

const app = express();
app.use(cors());