229

I've been trying to get HTTPS set up with a node.js project I'm working on. I've essentially followed the node.js documentation for this example:

// curl -k https://localhost:8000/
var https = require('https');
var fs = require('fs');

var options = {
  key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
  cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
};

https.createServer(options, function (req, res) {
  res.writeHead(200);
  res.end("hello world\n");
}).listen(8000);

Now, when I do

curl -k https://localhost:8000/

I get

hello world

as expected. But if I do

curl -k http://localhost:8000/

I get

curl: (52) Empty reply from server

In retrospect this seems obvious that it would work this way, but at the same time, people who eventually visit my project aren't going to type in https://yadayada, and I want all traffic to be https from the moment they hit the site.

How can I get node (and Express as that is the framework I'm using) to hand off all incoming traffic to https, regardless of whether or not it was specified? I haven't been able to find any documentation that has addressed this. Or is it just assumed that in a production environment, node has something that sits in front of it (e.g. nginx) that handles this kind of redirection?

This is my first foray into web development, so please forgive my ignorance if this is something obvious.

Jake
  • 5,379
  • 6
  • 19
  • 19
  • Does this answer your question? [Heroku NodeJS http to https ssl forced redirect](https://stackoverflow.com/questions/7185074/heroku-nodejs-http-to-https-ssl-forced-redirect) – TylerH Jan 11 '22 at 15:16

23 Answers23

222

Ryan, thanks for pointing me in the right direction. I fleshed out your answer (2nd paragraph) a little bit with some code and it works. In this scenario these code snippets are put in my express app:

// set up plain http server
var http = express();

// set up a route to redirect http to https
http.get('*', function(req, res) {  
    res.redirect('https://' + req.headers.host + req.url);

    // Or, if you don't want to automatically detect the domain name from the request header, you can hard code it:
    // res.redirect('https://example.com' + req.url);
})

// have it listen on 8080
http.listen(8080);

The https express server listens ATM on 3000. I set up these iptables rules so that node doesn't have to run as root:

iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 3000

All together, this works exactly as I wanted it to.

To prevent theft of cookies over HTTP, see this answer (from the comments) or use this code:

const session = require('cookie-session');
app.use(
  session({
    secret: "some secret",
    httpOnly: true,  // Don't let browser javascript access cookies.
    secure: true, // Only use cookies over https.
  })
);
miguelmorin
  • 5,025
  • 4
  • 29
  • 64
Jake
  • 5,379
  • 6
  • 19
  • 19
  • 19
    Really important question (regarding security). Before that redirect actually happens, is it possible for an attacker to sniff out and steal a cookie (session ID)? – Costa Michailidis Jun 23 '12 at 16:56
  • 5
    How would I fix this resulting symptom? `Error 310 (net::ERR_TOO_MANY_REDIRECTS): There were too many redirects` – bodine Mar 27 '13 at 20:56
  • 18
    Actually, [this seems better](http://stackoverflow.com/questions/10697660/force-ssl-with-expressjs-3),... just wrap the redirect with `if(!req.secure){}` – bodine Mar 27 '13 at 22:03
  • 6
    I just want to point out the answer to @Costa's important question about security given in another post - http://stackoverflow.com/questions/8605720/how-to-force-ssl-https-in-express-js#comment-14832113 – ThisClark Jan 15 '16 at 04:10
  • 3
    might want to wrap around a `if(req.protocol==='http')` statement – Max Aug 21 '16 at 05:53
  • This is not working for me. https requests are alone working. I am running a webapp on node server/Express Mongo Stack in aws ec2. Alternatives are welcome for the problem. – Vignesh Nov 04 '16 at 18:41
  • 1
    This redirects to `https` all the time even if it's already `https`. Check check that the request is not secure before redirecting. – David Aug 22 '18 at 14:50
185

Thanks to this guy: https://www.tonyerwin.com/2014/09/redirecting-http-to-https-with-nodejs.html

If secure, requests via https, otherwise redirects to https

app.enable('trust proxy')
app.use((req, res, next) => {
    req.secure ? next() : res.redirect('https://' + req.headers.host + req.url)
})
Esqarrouth
  • 38,543
  • 21
  • 161
  • 168
Jeremias Binder
  • 1,899
  • 1
  • 8
  • 6
  • 4
    for AWS ELB use `app.get('X-Forwarded-Proto') != 'http'` instead of `req.secure` https://aws.amazon.com/premiumsupport/knowledge-center/redirect-http-https-elb/ – shak Oct 14 '19 at 21:13
  • Good answer! Just an improvement using ternary expression `app.use (function (req, res, next) { req.secure ? next() : res.redirect('https://' + req.headers.host + req.url) });` – Shi Wei May 02 '20 at 05:39
  • I couldn't make this solution work, even setting the IP tables route `sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8443`. The accepted answer worked; remember to set the `secure` flag for cookies. – miguelmorin Sep 25 '20 at 08:44
  • 1
    This answer only relates to the Express framework and does not help if you are looking for the solution in pure node.js, which is what the OP's code is, and what brings this page up in a search. The answer by basarat below is still the best 6 years later. – Chris Robinson Jan 22 '21 at 13:54
  • Without setup crt and key? – Iago Coutinho Campos Jan 28 '21 at 18:47
  • To make sure POST requests also redirect see [this question](https://stackoverflow.com/questions/17612695/expressjs-how-to-redirect-a-post-request-with-parameters/17612942#17612942) – Josh May 07 '21 at 20:34
137

If you follow conventional ports since HTTP tries port 80 by default and HTTPS tries port 443 by default you can simply have two server's on the same machine: Here's the code:

var https = require('https');

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

https.createServer(options, function (req, res) {
    res.end('secure!');
}).listen(443);

// Redirect from http port 80 to https
var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(301, { "Location": "https://" + req.headers['host'] + req.url });
    res.end();
}).listen(80);

Test with https:

$ curl https://127.0.0.1 -k
secure!

With http:

$ curl http://127.0.0.1 -i
HTTP/1.1 301 Moved Permanently
Location: https://127.0.0.1/
Date: Sun, 01 Jun 2014 06:15:16 GMT
Connection: keep-alive
Transfer-Encoding: chunked

More details : Nodejs HTTP and HTTPS over same port

Community
  • 1
  • 1
basarat
  • 261,912
  • 58
  • 460
  • 511
  • 7
    `res.writeHead(301, etc.)` is only going to work correctly for GET calls, since `301` is not telling the client to use the same method. If you want to keep the method used (and all the other parameters) you have to use `res.writeHead(307, etc.)`. And if it still doesn't work, you could have to do some proxying. Source: [http://stackoverflow.com/a/17612942/1876359](http://stackoverflow.com/a/17612942/1876359) – ocramot Mar 29 '17 at 13:02
28

With Nginx you can take advantage of the "x-forwarded-proto" header:

function ensureSec(req, res, next){
    if (req.headers["x-forwarded-proto"] === "https"){
       return next();
    }
    res.redirect("https://" + req.headers.host + req.url);  
}
Adam Rackis
  • 82,527
  • 56
  • 270
  • 393
ccyrille
  • 873
  • 7
  • 18
  • 6
    I found req.headers["x-forwarded-proto"] === "https") to not be reliable, however req.secure works! – Aaron Silverman Jan 31 '13 at 18:04
  • I found the same, this appears very unreliable. – dacopenhagen May 21 '13 at 15:51
  • 2
    req.secure is the proper way, however this is buggy if you are behind a proxy because req.secure is equivalent to proto == "https", however behind a proxy express may say your proto is https,http – dacopenhagen May 22 '13 at 16:04
  • 8
    You need to `app.enable('trust proxy');`: "Indicates the app is behind a front-facing proxy, and to use the X-Forwarded-* headers to determine the connection and the IP address of the client." http://expressjs.com/en/4x/api.html#app.set – dskrvk Jun 10 '16 at 18:06
  • 1
    Don't treat urls as strings, use the url module instead. – arboreal84 Jul 28 '16 at 18:07
  • @dskrvk it doesn't seem like `app.enable('trust proxy')` actually redirects http to https, does it? From the docs it sounds like its main purpose is determining the client IP address. – Andy Jul 11 '18 at 19:02
  • It doesn't redirect by itself, it simply makes the above checks work correctly in a proxy setting. – dskrvk Jan 18 '19 at 20:19
  • This redirects all my routes except for my root domain route (i.e. '/'). Any idea why? – Matt Mar 30 '19 at 03:31
13

As of 0.4.12 we have no real clean way of listening for HTTP & HTTPS on the same port using Node's HTTP/HTTPS servers.

Some people have solved this issue by having having Node's HTTPS server (this works with Express.js as well) listen to 443 (or some other port) and also have a small http server bind to 80 and redirect users to the secure port.

If you absolutely have to be able to handle both protocols on a single port then you need to put nginx, lighttpd, apache, or some other web server on that port and have act as a reverse proxy for Node.

Ryan Olds
  • 4,847
  • 28
  • 24
  • Thanks Ryan. By "...have a small http server bind to 80 and redirect..." I assume you mean another **node** http server. I think I have an idea of how this might work but can you point to any example code? Also, do you know whether a "cleaner" solution to this issue is anywhere in the node roadmap? – Jake Sep 17 '11 at 04:01
  • Your Node.js application can have multiple http(s) servers. I checked the Node.js issues (http://github.com/joyent/node/issues) and the mailing list (http://groups.google.com/group/nodejs) and didn't see any issues reported, but did see couple posts about the problem on the mailing list. As far as I can tell this isn't in the pipe. I would recommend reporting it on github and seeing what feedback you get. – Ryan Olds Sep 17 '11 at 04:06
  • Really important question (regarding security). Before that redirect actually happens, is it possible for an "attacker" to sniff out and steal a cookie(session ID)? – Costa Michailidis Jun 25 '12 at 16:00
  • Yes, a 3xx status code and possibly a Location header are sent back to the agent, at which point the agent requests the URL specified by the Location header. – Ryan Olds Jun 25 '12 at 20:29
  • It's 2015, is this still the case? – TheEnvironmentalist Mar 19 '15 at 04:35
  • Yup. I don't see this changing. As we move to HTTP/2+, non-SSL/TLS connections will go away and this will no longer be an issue, except when supporting legacy code/browsers. – Ryan Olds Mar 19 '15 at 18:03
12

I use the solution proposed by Basarat but I also need to overwrite the port because I used to have 2 different ports for HTTP and HTTPS protocols.

res.writeHead(301, { "Location": "https://" + req.headers['host'].replace(http_port,https_port) + req.url });

I prefer also to use not standard port so to start nodejs without root privileges. I like 8080 and 8443 because I came from lots of years of programming on tomcat.

My complete file become

var fs = require('fs');
var http = require('http');
var http_port    =   process.env.PORT || 8080; 
var app = require('express')();

// HTTPS definitions
var https = require('https');
var https_port    =   process.env.PORT_HTTPS || 8443; 
var options = {
   key  : fs.readFileSync('server.key'),
   cert : fs.readFileSync('server.crt')
};

app.get('/', function (req, res) {
   res.send('Hello World!');
});

https.createServer(options, app).listen(https_port, function () {
   console.log('Magic happens on port ' + https_port); 
});

// Redirect from http port to https
http.createServer(function (req, res) {
    res.writeHead(301, { "Location": "https://" + req.headers['host'].replace(http_port,https_port) + req.url });
    console.log("http request, will go to >> ");
    console.log("https://" + req.headers['host'].replace(http_port,https_port) + req.url );
    res.end();
}).listen(http_port);

Then I use iptable for forwording 80 and 443 traffic on my HTTP and HTTPS ports.

sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 8080
sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-port 8443
Lorenzo Eccher
  • 301
  • 2
  • 4
  • Thanks Lorenzo, this worked for me. Instead of using options, I used letsencrypt. I deleted var options. I added the letsencrypt parameters (privateKey, certificate, ca, and credentials), and i changed options to credentials: https.createServer(credentials, app) – Nhon Ha Dec 06 '19 at 06:00
10

You can use the express-force-https module:

npm install --save express-force-https

var express = require('express');
var secure = require('express-force-https');

var app = express();
app.use(secure);
Mateus Ferreira
  • 519
  • 5
  • 9
  • 3
    Use this with caution because the package hasn't been updated in years. I ran into issues in AWS where encoding was bad. – Martavis P. Apr 25 '18 at 02:33
  • 1
    sister package: https://www.npmjs.com/package/express-to-https - but I have no idea if it works/isbetter/etc – quetzalcoatl Jun 26 '18 at 11:58
  • Note that if you're using middleware to serve static files (including single-page apps), any redirect middleware must be attached to `app` first. (see [this answer](https://stackoverflow.com/a/48299772/3748574)) – Matthew R. Feb 28 '20 at 03:51
8

I find req.protocol works when I am using express (have not tested without but I suspect it works). using current node 0.10.22 with express 3.4.3

app.use(function(req,res,next) {
  if (!/https/.test(req.protocol)){
     res.redirect("https://" + req.headers.host + req.url);
  } else {
     return next();
  } 
});
Catalyst
  • 3,143
  • 16
  • 20
8

This answer needs to be updated to work with Express 4.0. Here is how I got the separate http server to work:

var express = require('express');
var http = require('http');
var https = require('https');

// Primary https app
var app = express()
var port = process.env.PORT || 3000;
app.set('env', 'development');
app.set('port', port);
var router = express.Router();
app.use('/', router);
// ... other routes here
var certOpts = {
    key: '/path/to/key.pem',
    cert: '/path/to/cert.pem'
};
var server = https.createServer(certOpts, app);
server.listen(port, function(){
    console.log('Express server listening to port '+port);
});


// Secondary http app
var httpApp = express();
var httpRouter = express.Router();
httpApp.use('*', httpRouter);
httpRouter.get('*', function(req, res){
    var host = req.get('Host');
    // replace the port in the host
    host = host.replace(/:\d+$/, ":"+app.get('port'));
    // determine the redirect destination
    var destination = ['https://', host, req.url].join('');
    return res.redirect(destination);
});
var httpServer = http.createServer(httpApp);
httpServer.listen(8080);
NoBrainer
  • 5,853
  • 1
  • 27
  • 27
  • 1
    I got this to work by changing httpApp.use('*', httpRouter); to httpApp.use('/', httpRouter); and moving it to the line before you create the hhtp server. – KungWaz May 25 '14 at 09:59
8

If your app is behind a trusted proxy (e.g. an AWS ELB or a correctly configured nginx), this code should work:

app.enable('trust proxy');
app.use(function(req, res, next) {
    if (req.secure){
        return next();
    }
    res.redirect("https://" + req.headers.host + req.url);
});

Notes:

  • This assumes that you're hosting your site on 80 and 443, if not, you'll need to change the port when you redirect
  • This also assumes that you're terminating the SSL on the proxy. If you're doing SSL end to end use the answer from @basarat above. End to end SSL is the better solution.
  • app.enable('trust proxy') allows express to check the X-Forwarded-Proto header
Jamie McCrindle
  • 9,114
  • 6
  • 43
  • 48
6

you can use "net" module to listening for HTTP & HTTPS on the same port

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

var net=require('net');
var handle=net.createServer().listen(8000)

var options = {
  key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
  cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
};

https.createServer(options, function (req, res) {
  res.writeHead(200);
  res.end("hello world\n");
}).listen(handle);

http.createServer(function(req,res){
  res.writeHead(200);
  res.end("hello world\n");
}).listen(handle)
Davit Yavryan
  • 180
  • 1
  • 10
luofei614
  • 85
  • 1
  • 1
  • 5
    When I run this on Node 0.8, only the last server that calls .listen seems to answer. In this case, HTTP works, but not HTTPS. If I reverse the order of .createServer, then HTTP works but not HTTPS. :( – Joe Dec 18 '12 at 15:44
  • 1
    This does not work as described. I can confirm the issue Joe saw. – Stepan Mazurov Jun 12 '13 at 17:55
5

Most answers here suggest to use the req.headers.host header.

The Host header is required by HTTP 1.1, but it is actually optional since the header might not be actually sent by a HTTP client, and node/express will accept this request.

You might ask: which HTTP client (e.g: browser) can send a request missing that header? The HTTP protocol is very trivial. You can craft a HTTP request in few lines of code, to not send a host header, and if each time you receive a malformed request you throw an exception, and depending on how you handle such exceptions, this can take your server down.

So always validate all input. This is not paranoia, I have received requests lacking the host header in my service.

Also, never treat URLs as strings. Use the node url module to modify specific parts of a string. Treating URLs as strings can be exploited in many many many ways. Don't do it.

arboreal84
  • 2,086
  • 18
  • 21
3
var express = require('express');
var app = express();

app.get('*',function (req, res) {
    res.redirect('https://<domain>' + req.url);
});

app.listen(80);

This is what we use and it works great!

Nick Kotenberg
  • 914
  • 9
  • 8
2

This worked for me:

app.use(function(req,res,next) {
    if(req.headers["x-forwarded-proto"] == "http") {
        res.redirect("https://[your url goes here]" + req.url, next);
    } else {
        return next();
    } 
});
Burak
  • 5,706
  • 20
  • 70
  • 110
Shummy1991
  • 77
  • 2
2

This script while loading the page saves the URL page and checks if the address is https or http. If it is http, the script automatically redirects you to the same https page

(function(){
  var link = window.location.href;
  if(link[4] != "s"){
    var clink = "";
    for (let i = 4; i < link.length; i++) {
      clink += link[i];
    }
    window.location.href = "https" + clink;
  }
})();
Blackjack
  • 1,322
  • 1
  • 16
  • 21
1

Updated code for jake's answer. Run this alongside your https server.

// set up plain http server
var express = require('express');
var app = express();
var http = require('http');

var server = http.createServer(app);

// set up a route to redirect http to https
app.get('*', function(req, res) {
  res.redirect('https://' + req.headers.host + req.url);
})

// have it listen on 80
server.listen(80);
Storm
  • 35
  • 4
1

This works with express for me:

app.get("*",(req,res,next) => {
    if (req.headers["x-forwarded-proto"]) {
        res.redirect("https://" + req.headers.host + req.url)
    }
    if (!res.headersSent) {
        next()
    }
})

Put this before all HTTP handlers.

Elletlar
  • 3,136
  • 7
  • 32
  • 38
Kudos
  • 346
  • 3
  • 10
1

You can instantiate 2 Node.js servers - one for HTTP and HTTPS

You can also define a setup function that both servers will execute, so that you don't have to write much duplicated code.

Here's the way I did it: (using restify.js, but should work for express.js, or node itself too)

http://qugstart.com/blog/node-js/node-js-restify-server-with-both-http-and-https/

awaage
  • 2,669
  • 1
  • 17
  • 15
0

This worked for me:

/* Headers */
require('./security/Headers/HeadersOptions').Headers(app);

/* Server */
const ssl = {
    key: fs.readFileSync('security/ssl/cert.key'),
    cert: fs.readFileSync('security/ssl/cert.pem')
};
//https server
https.createServer(ssl, app).listen(443, '192.168.1.2' && 443, '127.0.0.1');
//http server
app.listen(80, '192.168.1.2' && 80, '127.0.0.1');
app.use(function(req, res, next) {
    if(req.secure){
        next();
    }else{
        res.redirect('https://' + req.headers.host + req.url);
    }
});

Recommend add the headers before redirect to https

Now, when you do:

curl http://127.0.0.1 --include

You get:

HTTP/1.1 302 Found
//
Location: https://127.0.0.1/
Vary: Accept
Content-Type: text/plain; charset=utf-8
Content-Length: 40
Date: Thu, 04 Jul 2019 09:57:34 GMT
Connection: keep-alive

Found. Redirecting to https://127.0.0.1/

I use express 4.17.1

0

The idea is to check if the incoming request is made with https, if so simply don't redirect it again to https but continue as usual. Else, if it is http, redirect it with appending https.

app.use (function (req, res, next) {
  if (req.secure) {
          next();
  } else {
          res.redirect('https://' + req.headers.host + req.url);
  }
});
rexloxrix
  • 31
  • 1
  • 4
0

From the long years of reseaching for a perfect redirection from http to https, I've found the perfect solution here.

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

const { parse } = require("url");
const next = require("next");
const fs = require("fs");

const ports = {
http: 3000,
https: 3001
}

const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();

const httpsOptions = {
key: fs.readFileSync("resources/certificates/localhost-key.pem"),
cert: fs.readFileSync("resources/certificates/localhost.pem")
};

// Automatic HTTPS connection/redirect with node.js/express
// source: https://stackoverflow.com/questions/7450940/automatic-https- 
connection-redirect-with-node-js-express
app.prepare().then(() => {

// Redirect from http port to https
http.createServer(function (req, res) {
    res.writeHead(301, { "Location": "https://" + req.headers['host'].replace(ports.http, ports.https) + req.url });
    console.log("http request, will go to >> ");
    console.log("https://" + req.headers['host'].replace(ports.http, ports.https) + req.url);
    res.end();
}).listen(ports.http, (err) => {
    if (err) throw err;
    console.log("ready - started server on url: http://localhost:" + ports.http);
});

https.createServer(httpsOptions, (req, res) => {
    const parsedUrl = parse(req.url, true);
    handle(req, res, parsedUrl);
}).listen(ports.https, (err) => {
    if (err) throw err;
    console.log("ready - started server on url: https://localhost:" + ports.https);
});
});
Prince Owusu
  • 31
  • 1
  • 4
0

My case i must change also the port and listen both ports:

appr.get("/", (req, res) => {
  res.redirect('https://' + req.headers['host'].replace(PORT, PORTS) + req.url);
});
Tyler2P
  • 2,324
  • 26
  • 22
  • 31
-1

if your node application install on IIS you can do like this in web.config

<configuration>
    <system.webServer>

        <!-- indicates that the hello.js file is a node.js application 
    to be handled by the iisnode module -->

        <handlers>
            <add name="iisnode" path="src/index.js" verb="*" modules="iisnode" />
        </handlers>

        <!-- use URL rewriting to redirect the entire branch of the URL namespace
    to hello.js node.js application; for example, the following URLs will 
    all be handled by hello.js:
    
        http://localhost/node/express/myapp/foo
        http://localhost/node/express/myapp/bar
        -->
        <rewrite>
            <rules>
                <rule name="HTTPS force" enabled="true" stopProcessing="true">
                    <match url="(.*)" />
                    <conditions>
                        <add input="{HTTPS}" pattern="^OFF$" />
                    </conditions>
                    <action type="Redirect" url="https://{HTTP_HOST}{REQUEST_URI}" redirectType="Permanent" />
                </rule>
                <rule name="sendToNode">
                    <match url="/*" />
                    <action type="Rewrite" url="src/index.js" />
                </rule>
            </rules>
        </rewrite>

        <security>
            <requestFiltering>
                <hiddenSegments>
                    <add segment="node_modules" />
                </hiddenSegments>
            </requestFiltering>
        </security>

    </system.webServer>
</configuration>
VnDevil
  • 1,321
  • 15
  • 13