Heroku NodeJS http to https ssl forced redirect
Asked Answered
D

13

111

I have an application up and running on Heroku with Express.js on Node.js with https. How do I identify the protocol to force a redirect to https with Node.js on Heroku?

My app is just a simple http-server, it doesn't (yet) realize Heroku is sending it https-requests:

// Heroku provides the port they want you on in this environment variable (hint: it's not 80)
app.listen(process.env.PORT || 3000);
Demibastion answered 25/8, 2011 at 4:15 Comment(4)
Heroku support answered my above question, and I didn't find it posted here already, so I thought I'd post it in public and share the knowledge. They pass a lot of info about the original request with it's request headers prefixed with an 'x-'. Here's the code I'm using now (at the top of my route definitions): app.get('*',function(req,res,next){ if(req.headers['x-forwarded-proto']!='https') res.redirect('https://mypreferreddomain.com'+req.url) else next() })Demibastion
ok so i get that you check for https like this and redirect if needed. But is there a way to do reroute at dns level with your domain name provider. So before browser resolve DNS it's already at https. Because with this approach is that, i Think given my knowledge of redirects, that once request is made over http and then again over https. So if sensitive data was sent then it was sent over http once. then over https. Which kinda defeats the purpose. Please let me know if i'm wrong.Mcferren
@MuhammadUmer, your reasoning seems on poin here, did you ever discover more?Morman
i simply used cloudflare as nameserver which works as nginx, and lets me redirect to ssl version just by clicking toggle button. also you could do this: developer.mozilla.org/en-US/docs/Web/HTTP/Headers/… Furthermore, usually nobody sends data right away they usually land on form and then submit. so on server side code, dns server, http header, javascript you can check and redirect to https developer.mozilla.org/en-US/docs/Web/HTTP/RedirectionsMcferren
D
102

The answer is to use the header of 'x-forwarded-proto' that Heroku passes forward as it does it's proxy thingamabob. (side note: They pass several other x- variables too that may be handy, check them out).

My code:

/* At the top, with other redirect methods before other routes */
app.get('*',function(req,res,next){
  if(req.headers['x-forwarded-proto']!='https')
    res.redirect('https://mypreferreddomain.com'+req.url)
  else
    next() /* Continue to other routes if we're not redirecting */
})

Thanks Brandon, was just waiting for that 6 hour delay thing that wouldn't let me answer my own question.

Demibastion answered 31/8, 2011 at 18:46 Comment(6)
wouldn't this let other methods than GET through?Cockfight
@Aaron: Well, you'd potentially lose information if you transparently redirected a POST-request. I think you should return a 400 on other requests than GET for http.Abbate
You might throw a && process.env.NODE_ENV === "production" into your conditional if you only want it to work in your production environment.Gisser
307 (redirect with same method) is probably better than a 400 error.Flanker
There are multiple issues with this answer, see the next answer below (https://mcmap.net/q/65200/-heroku-nodejs-http-to-https-ssl-forced-redirect) and rate this one down.Cochlea
This works for the server, but not for the client. How do we redirect http to https on the client, e.g. a Next.js React app?Zymo
X
113

As of today, 10th October 2014, using Heroku Cedar stack, and ExpressJS ~3.4.4, here is a working set of code.

The main thing to remember here is that we ARE deploying to Heroku. SSL termination happens at the load balancer, before encrypted traffic reaches your node app. It is possible to test whether https was used to make the request with req.headers['x-forwarded-proto'] === 'https'.

We don't need to concern ourselves with having local SSL certificates inside the app etc as you might if hosting in other environments. However, you should get a SSL Add-On applied via Heroku Add-ons first if using your own certificate, sub-domains etc.

Then just add the following to do the redirect from anything other than HTTPS to HTTPS. This is very close to the accepted answer above, but:

  1. Ensures you use "app.use" (for all actions, not just get)
  2. Explicitly externalises the forceSsl logic into a declared function
  3. Does not use '*' with "app.use" - this actually failed when I tested it.
  4. Here, I only want SSL in production. (Change as suits your needs)

Code:

 var express = require('express'),
   env = process.env.NODE_ENV || 'development';

 var forceSsl = function (req, res, next) {
    if (req.headers['x-forwarded-proto'] !== 'https') {
        return res.redirect(['https://', req.get('Host'), req.url].join(''));
    }
    return next();
 };

 app.configure(function () {      
    if (env === 'production') {
        app.use(forceSsl);
    }

    // other configurations etc for express go here...
 });

Note for SailsJS (0.10.x) users. You can simply create a policy (enforceSsl.js) inside api/policies:

module.exports = function (req, res, next) {
  'use strict';
  if ((req.headers['x-forwarded-proto'] !== 'https') && (process.env.NODE_ENV === 'production')) {
    return res.redirect([
      'https://',
      req.get('Host'),
      req.url
    ].join(''));
  } else {
    next();
  }
};

Then reference from config/policies.js along with any other policies, e.g:

'*': ['authenticated', 'enforceSsl']

Xray answered 27/5, 2014 at 16:35 Comment(4)
A note about using a sails policy: As stated in sailsjs.org/#/documentation/concepts/Policies: "Default policy mappings do not "cascade" or "trickle down." Specified mappings for the controller's actions will override the default mapping." This means that as soon as you have other policies for specific controller/action, you will have to make sure to add 'enforceSsl' on those controller/action.Hus
"The following table lists other small but important changes in Express 4: ... The app.configure() function has been removed. Use the process.env.NODE_ENV or app.get('env') function to detect the environment and configure the app accordingly. "Boesch
Also, note that res.redirect this defaults to a 302 redirect (at least in express 4.x). For SEO and caching reasons, you probably want a 301 redirect instead. Replace the corresponding line with return res.redirect(301, ['https://', req.get('Host'), req.url].join(''));Boesch
Note: In Express 4.x, remove the app.configure line and just use the inner potion. app.configure is legacy code and no longer included in express.Judicature
D
102

The answer is to use the header of 'x-forwarded-proto' that Heroku passes forward as it does it's proxy thingamabob. (side note: They pass several other x- variables too that may be handy, check them out).

My code:

/* At the top, with other redirect methods before other routes */
app.get('*',function(req,res,next){
  if(req.headers['x-forwarded-proto']!='https')
    res.redirect('https://mypreferreddomain.com'+req.url)
  else
    next() /* Continue to other routes if we're not redirecting */
})

Thanks Brandon, was just waiting for that 6 hour delay thing that wouldn't let me answer my own question.

Demibastion answered 31/8, 2011 at 18:46 Comment(6)
wouldn't this let other methods than GET through?Cockfight
@Aaron: Well, you'd potentially lose information if you transparently redirected a POST-request. I think you should return a 400 on other requests than GET for http.Abbate
You might throw a && process.env.NODE_ENV === "production" into your conditional if you only want it to work in your production environment.Gisser
307 (redirect with same method) is probably better than a 400 error.Flanker
There are multiple issues with this answer, see the next answer below (https://mcmap.net/q/65200/-heroku-nodejs-http-to-https-ssl-forced-redirect) and rate this one down.Cochlea
This works for the server, but not for the client. How do we redirect http to https on the client, e.g. a Next.js React app?Zymo
P
23

The accepted answer has a hardcoded domain in it, which isn't too good if you have the same code on several domains (eg: dev-yourapp.com, test-yourapp.com, yourapp.com).

Use this instead:

/* Redirect http to https */
app.get("*", function (req, res, next) {

    if ("https" !== req.headers["x-forwarded-proto"] && "production" === process.env.NODE_ENV) {
        res.redirect("https://" + req.hostname + req.url);
    } else {
        // Continue to other routes if we're not redirecting
        next();
    }

});

https://blog.mako.ai/2016/03/30/redirect-http-to-https-on-heroku-and-node-generally/

Phenylketonuria answered 30/3, 2016 at 18:21 Comment(1)
Works nice. I duno why i just had to replace req.hostname with req.headers.host maybe express version i'm in 4.2Goglet
M
17

I've written a small node module that enforces SSL on express projects. It works both in standard situations and in case of reverse proxies (Heroku, nodejitsu, etc.)

https://github.com/florianheinemann/express-sslify

Manyplies answered 23/3, 2014 at 1:5 Comment(0)
C
6

If you want to test out the x-forwarded-proto header on your localhost, you can use nginx to setup a vhost file that proxies all of the requests to your node app. Your nginx vhost config file might look like this

NginX

server {
  listen 80;
  listen 443;

  server_name dummy.com;

  ssl on;
  ssl_certificate     /absolute/path/to/public.pem;
  ssl_certificate_key /absolute/path/to/private.pem;

  access_log /var/log/nginx/dummy-access.log;
  error_log /var/log/nginx/dummy-error.log debug;

  # node
  location / {
    proxy_pass http://127.0.0.1:3000/;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

The important bits here are that you are proxying all requests to localhost port 3000 (this is where your node app is running) and you are setting up a bunch of headers including X-Forwarded-Proto

Then in your app detect that header as usual

Express

var app = express()
  .use(function (req, res, next) {
    if (req.header('x-forwarded-proto') == 'http') {
      res.redirect(301, 'https://' + 'dummy.com' + req.url)
      return
    }
    next()
  })

Koa

var app = koa()
app.use(function* (next) {
  if (this.request.headers['x-forwarded-proto'] == 'http') {
    this.response.redirect('https://' + 'dummy.com' + this.request.url)
    return
  }
  yield next
})

Hosts

Finally you have to add this line to your hosts file

127.0.0.1 dummy.com
Corbet answered 15/3, 2015 at 9:29 Comment(0)
O
6

You should take a look at heroku-ssl-redirect. It works like a charm!

var sslRedirect = require('heroku-ssl-redirect');
var express = require('express');
var app = express();

// enable ssl redirect
app.use(sslRedirect());

app.get('/', function(req, res){
  res.send('hello world');
});

app.listen(3000);
Onitaonlooker answered 3/10, 2017 at 1:32 Comment(0)
A
4

If you are using cloudflare.com as CDN in combination with heroku, you can enable automatic ssl redirect within cloudflare easily like this:

  1. Login and go to your dashboard

  2. Select Page Rules

    Select Page Rules

  3. Add your domain, e.g. www.example.com and switch always use https to on Switch always use https to on
Anthropology answered 18/11, 2014 at 1:31 Comment(0)
G
3

Loopback users can use a slightly adapted version of arcseldon answer as middleware:

server/middleware/forcessl.js

module.exports = function() {  
  return function forceSSL(req, res, next) {
    var FORCE_HTTPS = process.env.FORCE_HTTPS || false;
      if (req.headers['x-forwarded-proto'] !== 'https' && FORCE_HTTPS) {
        return res.redirect(['https://', req.get('Host'), req.url].join(''));
      }
      next();
    };
 };

server/server.js

var forceSSL = require('./middleware/forcessl.js');
app.use(forceSSL());
Gastroenterology answered 13/8, 2015 at 15:13 Comment(0)
G
2

This is a more Express specific way to do this.

app.enable('trust proxy');
app.use('*', (req, res, next) => {
  if (req.secure) {
    return next();
  }
  res.redirect(`https://${req.hostname}${req.url}`);
});
Greatest answered 3/8, 2017 at 5:5 Comment(0)
E
2

I am using Vue, Heroku and had same problem :

I updated my server.js as below, and i am not touching it anymore because it is working :) :

const serveStatic = require('serve-static')
const sts = require('strict-transport-security');
const path = require('path')

var express = require("express");

require("dotenv").config();
var history = require("connect-history-api-fallback");

const app = express()
const globalSTS = sts.getSTS({'max-age':{'days': 365}});
app.use(globalSTS);

app.use(
  history({
    verbose: true
  })
);

app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https') {
    res.redirect(`https://${req.header('host')}${req.url}`)
  } else {
    next();
  }
});

app.use('/', serveStatic(path.join(__dirname, '/dist')));
app.get(/.*/, function (req, res) {
res.sendFile(path.join(__dirname, '/dist/index.html'))
})

const port = process.env.PORT || 8080
app.listen(port)
console.log(`app is listening on port: ${port}`)
Exceptionable answered 19/3, 2021 at 19:38 Comment(0)
N
0
app.all('*',function(req,res,next){
  if(req.headers['x-forwarded-proto']!='https') {
    res.redirect(`https://${req.get('host')}`+req.url);
  } else {
    next(); /* Continue to other routes if we're not redirecting */
  }
});
Natal answered 1/8, 2017 at 17:4 Comment(0)
P
0

With app.use and dynamic url. Works both localy and on Heroku for me

app.use(function (req, res, next) {
  if (req.header('x-forwarded-proto') === 'http') {
    res.redirect(301, 'https://' + req.hostname + req.url);
    return
  }
  next()
});
Pascia answered 15/10, 2019 at 19:7 Comment(0)
I
-1

Checking the protocol in the X-Forwarded-Proto header works fine on Heroku, just like Derek has pointed out. For what it's worth, here is a gist of the Express middleware that I use and its corresponding test.

Intine answered 20/2, 2014 at 19:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.