Automatic HTTPS connection/redirect with node.js/express
Asked Answered
U

23

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.

Undertrick answered 16/9, 2011 at 22:15 Comment(1)
Does this answer your question? Heroku NodeJS http to https ssl forced redirectAlternant
U
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.
  })
);
Undertrick answered 17/9, 2011 at 23:44 Comment(7)
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)?Monarda
How would I fix this resulting symptom? Error 310 (net::ERR_TOO_MANY_REDIRECTS): There were too many redirectsTushy
Actually, this seems better,... just wrap the redirect with if(!req.secure){}Tushy
I just want to point out the answer to @Costa's important question about security given in another post - #8606220Townsman
might want to wrap around a if(req.protocol==='http') statementCombustion
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.Rollandrollaway
This redirects to https all the time even if it's already https. Check check that the request is not secure before redirecting.Chart
K
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)
})
Kelliekellina answered 8/3, 2018 at 15:31 Comment(6)
for AWS ELB use app.get('X-Forwarded-Proto') != 'http' instead of req.secure aws.amazon.com/premiumsupport/knowledge-center/…Lunular
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) });Street
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.Langan
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.Sylvester
Without setup crt and key?Heymann
To make sure POST requests also redirect see this questionJudicative
M
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

Microwave answered 1/6, 2014 at 6:18 Comment(1)
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: https://mcmap.net/q/119970/-expressjs-how-to-redirect-a-post-request-with-parametersDynasty
L
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);  
}
Liminal answered 1/5, 2012 at 18:57 Comment(8)
I found req.headers["x-forwarded-proto"] === "https") to not be reliable, however req.secure works!Otti
I found the same, this appears very unreliable.Twaddle
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,httpTwaddle
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." expressjs.com/en/4x/api.html#app.setElodea
Don't treat urls as strings, use the url module instead.Highboy
@Elodea 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.Deletion
It doesn't redirect by itself, it simply makes the above checks work correctly in a proxy setting.Elodea
This redirects all my routes except for my root domain route (i.e. '/'). Any idea why?Clementineclementis
D
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.

Diazotize answered 16/9, 2011 at 23:8 Comment(6)
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?Undertrick
Your Node.js application can have multiple http(s) servers. I checked the Node.js issues (github.com/joyent/node/issues) and the mailing list (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.Diazotize
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)?Monarda
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.Diazotize
It's 2015, is this still the case?Nishanishi
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.Diazotize
M
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
Myles answered 20/10, 2017 at 15:24 Comment(1)
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)Waste
W
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);
Workmanlike answered 22/2, 2018 at 20:52 Comment(3)
Use this with caution because the package hasn't been updated in years. I ran into issues in AWS where encoding was bad.Sikora
sister package: npmjs.com/package/express-to-https - but I have no idea if it works/isbetter/etcBostic
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)Quintana
I
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();
  } 
});
Idolla answered 22/11, 2013 at 11:23 Comment(0)
J
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);
Josiejosler answered 25/4, 2014 at 2:29 Comment(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.Grill
J
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
Jointworm answered 9/10, 2016 at 10:8 Comment(0)
S
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)
Sephira answered 23/11, 2012 at 2:37 Comment(2)
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. :(Roam
This does not work as described. I can confirm the issue Joe saw.Savoirfaire
H
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.

Highboy answered 28/7, 2016 at 18:5 Comment(2)
Your answer would be perfect if you had given an example.Stubstad
Sounds legit but some references / links would be great.Auden
M
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!

Matrona answered 3/11, 2015 at 19:4 Comment(3)
Don't treat urls as strings, use the url module instead.Highboy
this wont cause infinite loop?Supraorbital
No, because it only listens on port 80 and sends to port 443. The only way an infinite loop would happen is if some listener on 443 redirects back to 80 which isn't in my code. I hope that makes sense. Thanks!Matrona
L
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();
    } 
});
Lordinwaiting answered 10/12, 2016 at 16:50 Comment(1)
This may be right, but you should elaborate on how this answers the asker's question.Emersion
P
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;
  }
})();
Phonometer answered 3/1, 2021 at 16:1 Comment(0)
M
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/

Macfadyn answered 28/2, 2012 at 23:1 Comment(0)
R
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);
Rouleau answered 18/9, 2020 at 18:29 Comment(0)
M
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.

Monroemonroy answered 11/11, 2020 at 16:33 Comment(1)
wouldn't it be app.all rather than app.get ?Recipient
Y
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

Yager answered 4/7, 2019 at 10:19 Comment(0)
C
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);
  }
});
Ceria answered 3/2, 2021 at 17:44 Comment(0)
L
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://mcmap.net/q/117577/-automatic-https-connection-redirect-with-node-js-express 
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);
});
});
Liquate answered 19/1, 2022 at 15:37 Comment(0)
S
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);
});
Stryker answered 6/2, 2023 at 12:25 Comment(0)
Y
-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>
Yogh answered 12/9, 2020 at 4:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.