66

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?

rafaelcosman
  • 2,569
  • 7
  • 23
  • 39
Elias
  • 3,300
  • 4
  • 34
  • 38
  • 1
    Possible duplicate http://stackoverflow.com/questions/7450940/automatic-https-connection-redirect-with-node-js-express – clyfe Dec 22 '11 at 15:07
  • The solution to this problem is redirecting every request. But like I wrote, that's no option. – Elias Dec 22 '11 at 15:09
  • It only redirects the requests that come over http. – clyfe Dec 22 '11 at 15:13
  • That's right, had to add a second note. Only one server allowed. – Elias Dec 22 '11 at 15:20
  • I don't think you could be speaking 2 protocols on the same socket. I might be wrong. – clyfe Dec 22 '11 at 18:42
  • @clyfe: it's unusual, but you can: http://serverfault.com/a/342378/47187 – Bruno Dec 26 '11 at 18:23
  • There _is_ a possibility to find out whether there is a TLS connection established. http://expressjs.com/en/api.html#req.secure . I don't know when it was added though. – MauganRa Jun 17 '16 at 17:02

8 Answers8

89

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);
zwl
  • 57
  • 3
  • 5
Lavamantis
  • 3,766
  • 2
  • 25
  • 22
  • 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. – Bergur Aug 13 '19 at 13:41
  • 9
    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). – Raffael Aug 18 '19 at 10:30
  • Worked for me perfectly, thanks! Re: @Bergur's comment, that can be done in another middleware statement. https://expressjs.com/en/4x/api.html – Drakinite Oct 01 '20 at 22:37
28

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!

MeetM
  • 1,410
  • 17
  • 25
  • 1
    If express tells you not to call `createServer` - http://stackoverflow.com/questions/11804202/how-to-setup-a-ssl-cert-in-express-js-server-now – Rohit Chatterjee Dec 09 '13 at 13:02
23

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
Peter Lyons
  • 142,938
  • 30
  • 279
  • 274
  • 2
    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? – Costa Michailidis Jun 30 '12 at 21:26
  • 10
    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. http://en.wikipedia.org/wiki/HTTP_cookie#Secure_and_HttpOnly – Peter Lyons Jun 30 '12 at 22:32
  • Okay, great! Just gotta figure out how to get CouchDB to set a Secure Flag. Thanks!! – Costa Michailidis Jul 01 '12 at 13:04
  • If express tells you not to call `createServer` - http://stackoverflow.com/questions/11804202/how-to-setup-a-ssl-cert-in-express-js-server-now – Rohit Chatterjee Dec 09 '13 at 13:03
  • Can you please make this JavaScript per Stack guidelines? – mikemaccana Oct 06 '15 at 14:50
11

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);
  }
});
Jonathan Tran
  • 15,214
  • 9
  • 60
  • 67
  • 1
    Doing this is vulnerable to man in the middle attacks. See HSTS: http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security – ItalyPaleAle Mar 04 '14 at 05:11
  • 1
    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). – Iain Collins May 14 '15 at 13:54
  • I get this error from chrome: ERR_TOO_MANY_REDIRECTS – 000000000000000000000 Sep 28 '15 at 11:25
  • 1
    I found this answer and it worked: http://stackoverflow.com/questions/10697660/force-ssl-with-expressjs-3 – 000000000000000000000 Sep 28 '15 at 11:28
3

http.createServer(app.handle).listen(80)

https.createServer(options, app.handle).listen(443)

for express 2x

2

Try this example :

var express = require('express');
        var app = express();
        // set up a route to redirect http to https
        app.use(function (req, res, next) {
        if (!/https/.test(req.protocol)) {
            res.redirect("https://" + req.headers.host + req.url);
        } else {
            return next();
        }
        });
        var webServer = app.listen(port, function () {
            console.log('Listening on port %d', webServer.address().port);
        });
Ashok Kumawat
  • 613
  • 7
  • 13
2

This code looks like it does what you need: https://gist.github.com/903596

robocat
  • 5,293
  • 48
  • 65
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

Elias
  • 3,300
  • 4
  • 34
  • 38
  • 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. – Benjamin Atkin Feb 05 '12 at 03:43
  • Excellent! Changed my vote. (In retrospect I probably should have just left a comment, and not downvoted it.) – Benjamin Atkin Mar 14 '12 at 20:02
  • 1
    Cached copy [here](http://web.archive.org/web/20120112113542/http://elias.kg/post/14971446990/force-ssl-with-express-js-on-heroku-nginx). Alternatively, see [Jonathan Tran's answer](/questions/8605720/how-to-force-ssl-https-in-express-js/20531481#20531481) which is virtually a copy of the code from the post. – c24w Oct 28 '14 at 14:29