73

I am using url_for to generate a redirect URL when a user has logged out:

return redirect(url_for('.index', _external=True))

However, when I changed the page to a https connection, the url_for still gives me http.

I would like to explicitly ask url_for to add https at the beginning of a URL.

Can you point me how to change it? I looked at Flask docs, without luck.

daaawx
  • 3,273
  • 2
  • 17
  • 16
Blaise
  • 7,230
  • 6
  • 43
  • 53
  • 1
    How is your flask app deployed. Because `https` is usually handled by the wsgi handler – Jakob Bowyer Feb 11 '13 at 11:00
  • @JakobBowyer I am using default testing deployment environment packed with Flask. Simply invoking `python index.py`. So that's Flask's wsgi handler. Check also @leon's answer. – Blaise Feb 11 '13 at 14:20
  • None of these solutions worked. So had to resort to adding the redirect url as a configuration entry. – Martlark Jun 18 '15 at 03:09

8 Answers8

75

With Flask 0.10, there will be a much better solution available than wrapping url_for. If you look at https://github.com/mitsuhiko/flask/commit/b5069d07a24a3c3a54fb056aa6f4076a0e7088c7, a _scheme parameter has been added. Which means you can do the following:

url_for('secure_thingy',
        _external=True,
        _scheme='https',
        viewarg1=1, ...)

_scheme sets the URL scheme, generating a URL like https://.. instead of http://. However, by default Flask only generates paths (without host or scheme), so you will need to include the _external=True to go from /secure_thingy to https://example.com/secure_thingy.


However, consider making your website HTTPS-only instead. It seems that you're trying to partially enforce HTTPS for only a few "secure" routes, but you can't ensure that your https-URL is not changed if the page linking to the secure page is not encrypted. This is similar to mixed content.

Markus Unterwaditzer
  • 7,992
  • 32
  • 60
58

If you want to affect the URL scheme for all server-generated URLs (url_for and redirect), rather than having to set _scheme on every call, it seems that the "correct" answer is to use WSGI middleware, as in this snippet: http://flask.pocoo.org/snippets/35/

(This Flask bug seems to confirm that that is the preferred way.)

Basically, if your WSGI environment has environ['wsgi.url_scheme'] = 'https', then url_for will generate https: URLs.

I was getting http:// URLs from url_for because my server was deployed behind an Elastic Beanstalk load balancer, which communicates with the server in regular HTTP. My solution (specific to Elastic Beanstalk) was like this (simplified from the snippet linked above):

class ReverseProxied(object):
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        scheme = environ.get('HTTP_X_FORWARDED_PROTO')
        if scheme:
            environ['wsgi.url_scheme'] = scheme
        return self.app(environ, start_response)

app = Flask(__name__)
app.wsgi_app = ReverseProxied(app.wsgi_app)

The Elastic Beanstalk-specific part of that is HTTP_X_FORWARDED_PROTO. Other environments would have other ways of determining whether the external URL included https. If you just want to always use HTTPS, you could unconditionally set environ['wsgi.url_scheme'] = 'https'.

PREFERRED_URL_SCHEME is not the way to do this. It's ignored whenever a request is in progress.

aldel
  • 6,489
  • 1
  • 27
  • 32
  • This solution worked perfectly!. apparently, this issue happened specifically with aws elastic bean stalk. The same code base had no redirection issues when hosted without elastic beanstalk. – Jinesh Jul 18 '17 at 08:43
  • Thanks a lot for this solution! When deploying a Flask app on Azure Web Service this does the trick (with gunicorn as a container at least). – reim May 20 '19 at 13:05
  • 1
    Thanks for this solution! I would like to add, for other flask noobs like myself, that you can "stack" or add multiple middlewares. So if your code already has a line like `app.wsgi_app = ProxyFix(app.wsgi_app)`, you should still be able to add aldel's solution without conflict. At least it worked for me. – N. Quest Jul 02 '19 at 23:46
  • This actually happens on many platforms, including Heroku. Thanks for this! – M. Volf May 25 '20 at 10:21
  • Implemented something similar for a Private AWS API Gateway accessed through a VPC endpoint. For some reason the header `HTTP_X_FORWARDED_PROTO` isn't sent to the server so I had to set `environ['wsgi.url_scheme'] = 'https'` regardless of what the server sent (I didn't want http in any case). – Tim Ludwinski Dec 14 '20 at 21:03
  • I am running a flask app in GCP compute engine and my app is loaded in iframe inside some CRM. This solution worked for my case. – Gray_Rhino Apr 28 '21 at 08:06
  • Flask's `ProxyFix` should already take care of this without having to define your own middleware. https://werkzeug.palletsprojects.com/en/2.2.x/middleware/proxy_fix/ – mochatiger Jan 24 '23 at 22:00
34

I tried the accepted answer with an url_for arg but I found it easier to use the PREFERRED_URL_SCHEME config variable and set it to https with:

app.config.update(dict(
  PREFERRED_URL_SCHEME = 'https'
))

since you don't have to add it to every url_for call.

Jim
  • 72,985
  • 14
  • 101
  • 108
dajobe
  • 4,938
  • 35
  • 41
32

If your are accessing your website through a reverse proxy like Nginx, then Flask correctly dectects the scheme being HTTP.

Browser -----HTTPS----> Reverse proxy -----HTTP----> Flask

The easiest solution is to configure your reverse proxy to set the X-Forwarded-Proto header. Flask will automatically detect this header and manage scheme accordingly. There is a more detailed explanation in the Flask documentation under the Proxy Setups section. For example, if you use Nginx, you will have to add the following line in your location block.

proxy_set_header   X-Forwarded-Proto    $scheme;

As other mentionned, if you can't change the configuration of your proxy, you can either use the werkzeug ProxyFix or build your own fix as described in the documentation: http://flask.pocoo.org/docs/0.12/deploying/wsgi-standalone/#proxy-setups

Timothée Jeannin
  • 9,652
  • 2
  • 56
  • 65
9

Setting _scheme on every url_for() call is extremely tedious, and PREFERRED_URL_SCHEME doesn't seem to work. However, mucking with what the request's supposed scheme is at the WSGI level seems to successfully convince Flask to always construct HTTPS URLs:

def _force_https(app):
    def wrapper(environ, start_response):
        environ['wsgi.url_scheme'] = 'https'
        return app(environ, start_response)
    return wrapper

app = Flask(...)

app = _force_https(app)
cvrebert
  • 9,075
  • 2
  • 38
  • 49
8

For anyone ending up here recently there is an official uwsgi fixer for this: https://stackoverflow.com/a/23504684/13777925

FWIW this still didn't work for me since the header wasn't being set correctly so I augmented the ReversedProxied middleware to prefer https if found thusly:

class ReverseProxied(object):
"""
Because we are reverse proxied from an aws load balancer
use environ/config to signal https
since flask ignores preferred_url_scheme in url_for calls
"""

    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        # if one of x_forwarded or preferred_url is https, prefer it.
        forwarded_scheme = environ.get("HTTP_X_FORWARDED_PROTO", None)
        preferred_scheme = app.config.get("PREFERRED_URL_SCHEME", None)
        if "https" in [forwarded_scheme, preferred_scheme]:
            environ["wsgi.url_scheme"] = "https"
        return self.app(environ, start_response)

Called as:

app = flask.Flask(__name__)
app.wsgi_app = ReverseProxied(app.wsgi_app)

This way if you've set the environment var "PREFERRED_URL_SCHEME" explicitly or if the nginx/etc/proxy sets the X_FORWARDED_PROTO, it does the right thing.

Jeff Bryner
  • 109
  • 1
  • 4
  • 1
    This shim was necessary to get Flask + OAuth2 working properly in a Google Cloud Run container running gunicorn. – postal May 26 '21 at 22:14
  • It is important to note that `app` in `preferred_scheme = app...` is not `self.app` (the WSGI app), but the Flask app. – moi Mar 19 '22 at 11:34
0

I personally could not fix this problem with any of the answers here, but found that simply adding --cert=adhoc to the end of the flask run command, which makes the flask app run with https, solved the issue.

flask run --host=0.0.0.0 --cert=adhoc

-1
ingress:
  web:
    enabled: true
    annotations:
      kubernetes.io/ingress.class: "nginx"
      nginx.ingress.kubernetes.io/rewrite-target: /
      nginx.ingress.kubernetes.io/use-regex: "true"
      nginx.ingress.kubernetes.io/configuration-snippet: |
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Port 443;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

https://github.com/apache/airflow/discussions/31805

seunggabi
  • 1,699
  • 12
  • 12