How to force SSL / https in Express.js

58,371

Solution 1

Since I was working on nginx, I had access to the header's x-forwarded-proto property so I could write a tiny middleware to redirect all traffic as described here: http://elias.kg/post/14971446990/force-ssl-with-express-js-on-heroku-nginx

Edit: Updated the url

Solution 2

Just in case you're hosting on Heroku and just want to redirect to HTTPS regardless of port, here's the middleware solution we're using.

It doesn't bother to redirect if you're developing locally.

function requireHTTPS(req, res, next) {
  // The 'x-forwarded-proto' check is for Heroku
  if (!req.secure && req.get('x-forwarded-proto') !== 'https' && process.env.NODE_ENV !== "development") {
    return res.redirect('https://' + req.get('host') + req.url);
  }
  next();
}

You can use it with Express (2.x and 4.x) like so:

app.use(requireHTTPS);

Solution 3

Although the question looks a year old, I would like to answer as it might help others. Its actually really simple with the latest version of expressjs (2.x). First create the key and cert using this code

openssl genrsa -out ssl-key.pem 1024

$ openssl req -new -key ssl-key.pem -out certrequest.csr .. bunch of prompts

$ openssl x509 -req -in certrequest.csr -signkey ssl-key.pem -out ssl-cert.pem

Store the cert and key files in the folder containing app.js. Then edit the app.js file and write the following code before express.createServer()

var https = require('https');
var fs = require('fs');

var sslkey = fs.readFileSync('ssl-key.pem');
var sslcert = fs.readFileSync('ssl-cert.pem')

var options = {
    key: sslkey,
    cert: sslcert
};

Now pass the options object in the createServer() function

express.createServer(options);

Done!

Solution 4

First, let me see if I can clarify the problem. You are limited to one (1) node.js process, but that process can listen on two (2) network ports, both 80 and 443, right? (When you say one server it's not clear if you mean one process or only one network port).

Given that constraint, you problem seems to be, for reasons you don't provide, somehow your clients are connecting to the wrong port. This is a bizarre edge case because by default, clients will make HTTP requests to port 80 and HTTPS to port 443. And when I say "by default", I mean if no specific ports are included in the URLs. So unless you are explicitly using criss-crossed URLs like http://example.com:443 and https://example.com:80, you really shouldn't have any criss-crossed traffic hitting your site. But since you asked the question, I guess you must have it, although I bet you are using non-standard ports as opposed to the 80/443 defaults.

So, for background: YES some web servers handle this reasonably well. For example, if you do http://example.com:443 to nginx, it will respond with an HTTP 400 "Bad Request" response indicating "The plain HTTP request was sent to HTTPS port". YES, you can listen on both 80 and 443 from the same node.js process. You just need to create 2 separate instances of express.createServer(), so that's no problem. Here's a simple program to demonstrate handling both protocols.

var fs = require("fs");
var express = require("express");

var http = express.createServer();

var httpsOptions = {
  key: fs.readFileSync('key.pem'),
  cert: fs.readFileSync('cert.pem')
};

var https = express.createServer(httpsOptions);

http.all('*', function(req, res) {
  console.log("HTTP: " + req.url);
  return res.redirect("https://" + req.headers["host"] + req.url);
});

http.error(function(error, req, res, next) {
  return console.log("HTTP error " + error + ", " + req.url);
});

https.error(function(error, req, res, next) {
  return console.log("HTTPS error " + error + ", " + req.url);
});

https.all('*', function(req, res) {
  console.log("HTTPS: " + req.url);
  return res.send("Hello, World!");
});

http.listen(80);

And I can test this via cURL like this:

$ curl --include --silent http://localhost/foo
HTTP/1.1 302 Moved Temporarily
X-Powered-By: Express
Content-Type: text/html
Location: https://localhost/foo
Connection: keep-alive
Transfer-Encoding: chunked

<p>Moved Temporarily. Redirecting to <a href="https://localhost/foo">https://localhost/foo</a></p>

$ curl --include --silent --insecure https://localhost:443/foo
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 13
Connection: keep-alive

Hello, World!%

And showing the redirect from HTTP to HTTPS...

curl --include --silent --location --insecure 'http://localhost/foo?bar=bux'
HTTP/1.1 302 Moved Temporarily
X-Powered-By: Express
Content-Type: text/html
Location: https://localhost/foo?bar=bux
Connection: keep-alive
Transfer-Encoding: chunked

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 13
Connection: keep-alive

Hello, World!%

So that will work to serve both protocols for the regular case and redirect properly. However, criss-crosses don't work at all. I believe a criss-crossed request hitting an express server isn't going to get routed through the middleware stack because it will be treated as an error from the very beginning and won't even get the request URI parsed properly, which is necessary to send it through the route middleware chain. The express stack doesn't even get them I think because they are not valid requests, so they get ignored somewhere in the node TCP stack. It's probably possible to write a server to do this, and there may already be a module out there, but you'd have to write it at the TCP layer directly. And you'd have to detect a regular HTTP request in the first chunk of client data that hits the TCP port and wire that connection to an HTTP server instead of your normal TLS handshake.

When I do either of these, my express error handlers do NOT get called.

curl  --insecure https://localhost:80/foo
curl: (35) Unknown SSL protocol error in connection to localhost:80

curl http://localhost:443/foo
curl: (52) Empty reply from server

Solution 5

Based on Elias's answer but with inline code. This works if you have node behind nginx or a load balancer. Nginx or the load balancer will always hit node with plain old http, but it sets a header so you can distinguish.

app.use(function(req, res, next) {
  var schema = req.headers['x-forwarded-proto'];

  if (schema === 'https') {
    // Already https; don't do anything special.
    next();
  }
  else {
    // Redirect to https.
    res.redirect('https://' + req.headers.host + req.url);
  }
});
Share:
58,371
Elias
Author by

Elias

Updated on February 06, 2020

Comments

  • Elias
    Elias about 4 years

    I am trying to create a middleware for Express.js to redirect all non-secure (port 80) traffic to the secured SSL port (443). Unfortunately there is no information in an Express.js request that lets you determine if the request comes over http or https.

    One solution would be to redirect every request but this is not an option for me.

    Notes:

    1. There is no possibility to handle it with Apache or something else. It has to be done in node.

    2. Only one server can be fired up in the application.

    How would you solve that?

  • Gorm Casper
    Gorm Casper about 12 years
    This page is gone, so I'm downvoting. If it comes back up please let me know so I can vote it back up! This could have been avoided by reproducing some of the content here. Thanks.
  • Gorm Casper
    Gorm Casper about 12 years
    Excellent! Changed my vote. (In retrospect I probably should have just left a comment, and not downvoted it.)
  • Milan Babuškov
    Milan Babuškov almost 12 years
    I tried this and it does not work. I get: Error: Can't set headers after they are sent. and node process dies afterwards.
  • Costa Michailidis
    Costa Michailidis almost 12 years
    Security Question: suppose your application uses cookie authentication and someone who is logged in navigates to an http://... part of the site, is it possible an attacker could sniff their cookie before the redirect happens?
  • Peter Lyons
    Peter Lyons almost 12 years
    Yes, I believe that is possible, which is why you should set the "Secure" flag in your cookie header, which will instruct browsers not to transmit the cookie over HTTP, only HTTPS. en.wikipedia.org/wiki/HTTP_cookie#Secure_and_HttpOnly
  • Costa Michailidis
    Costa Michailidis almost 12 years
    Okay, great! Just gotta figure out how to get CouchDB to set a Secure Flag. Thanks!!
  • Rohit Chatterjee
    Rohit Chatterjee over 10 years
    If express tells you not to call createServer - stackoverflow.com/questions/11804202/…
  • Rohit Chatterjee
    Rohit Chatterjee over 10 years
    If express tells you not to call createServer - stackoverflow.com/questions/11804202/…
  • ItalyPaleAle
    ItalyPaleAle about 10 years
    Doing this is vulnerable to man in the middle attacks. See HSTS: en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
  • c24w
    c24w over 9 years
    Cached copy here. Alternatively, see Jonathan Tran's answer which is virtually a copy of the code from the post.
  • Iain Collins
    Iain Collins almost 9 years
    The answer from @jonathon-tran is correct and it's not any more vulnerable to man in the middle attacks than not doing it. HSTS is an additional protection against them for clients that you'd ideally want to use in conjunction with something like this (though it is still not supported in IE as of IE 11).
  • 000000000000000000000
    000000000000000000000 over 8 years
    I get this error from chrome: ERR_TOO_MANY_REDIRECTS
  • 000000000000000000000
    000000000000000000000 over 8 years
  • mikemaccana
    mikemaccana over 8 years
    Can you please make this JavaScript per Stack guidelines?
  • Bergur
    Bergur over 4 years
    Nice solution. Only problem I see with this is that it's forwarding all http methods, shouldn't it maybe return 403 on everything that's not get.
  • Raffael
    Raffael over 4 years
    For the beginners among us: Remember to put the app.use(requireHTTPS) call before any app.get calls (before anything that should be guarded by SSL).
  • Drakinite
    Drakinite over 3 years
    Worked for me perfectly, thanks! Re: @Bergur's comment, that can be done in another middleware statement. expressjs.com/en/4x/api.html