How to force SSL / https in Express.js
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);
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!
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
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);
}
});