How to configure a single flask application to handle multiple domains? - python

Currently, my flask application (that uses sessions) does the following to handle ONE domain:
app.config.from_object(settings)
and in the settings object:
SESSION_COOKIE_DOMAIN = ".first.com"
What I'd like to do now is to dynamically set the session cookie domain to handle, for example, requests from www.first.com and www.second.com. Please note that I'm talking about domains but not subdomains. Thank you.

Grepping SESSION_COOKIE_DOMAIN through Flask's Github repo one can see that it is used like this:
def get_cookie_domain(self, app):
"""Helpful helper method that returns the cookie domain that should
be used for the session cookie if session cookies are used.
"""
if app.config['SESSION_COOKIE_DOMAIN'] is not None:
return app.config['SESSION_COOKIE_DOMAIN']
if app.config['SERVER_NAME'] is not None:
# chop of the port which is usually not supported by browsers
rv = '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0]
# Google chrome does not like cookies set to .localhost, so
# we just go with no domain then. Flask documents anyways that
# cross domain cookies need a fully qualified domain name
if rv == '.localhost':
rv = None
# If we infer the cookie domain from the server name we need
# to check if we are in a subpath. In that case we can't
# set a cross domain cookie.
if rv is not None:
path = self.get_cookie_path(app)
if path != '/':
rv = rv.lstrip('.')
return rv
Doing the same thing with get_cookie_domain( you'll see:
def save_session(self, app, session, response):
domain = self.get_cookie_domain(app)
path = self.get_cookie_path(app)
...
OK. Now we only need to find out what domain name to use. Digging through docs or code you'll see that save_session() is called in request context. So you just need to import the request object from flask module:
from flask import request
and use it inside save_session() to determine domain name for the cookies (e.g. from the Host header) like this:
def save_session(self, app, session, response):
domain = '.' + request.headers['Host']
path = self.get_cookie_path(app)
# the rest of the method is intact
The only time you need to specify cookies domain is when you send them back with response object.
Also bear in mind that Host header might be absent.
To wire up the whole thing you'll need to specify your version (subclass) of SecureCookieSessionInterface:
app = Flask(__name__)
app.session_interface = MySessionInterface()
More doc links:
Response Object
Session Interface

Related

How are PREFERRED_URL_SCHEME, SERVER_NAME, APPLICATION_ROOT and BASE_URL related in Flask?

Reading about the url_for function I have been quite confused about what is the meaning of the configuration values mentioned in the question. My assumption is as follows:
BASE_URL = PREFERRED_URL_SCHEME + SERVER_NAME + APPLICATION_ROOT
Now, I have the following questions:
Is the assumption conceptually valid?
If yes to the previous question, is BASE_URL used by Flask in that way?
Edit: since someone voted to close this question due to lack of clarity, I will explain a bit more about the nature of the question:
The configuration values mentioned in the question are in multiple parts of the Flask documentation, however, so far I have not been able to find a clear definition of these and with examples, other than vague mentions in forums. Now, resolving the assumption I raised above might give more clarity on the representation of those values, an assumption that was born out of this piece of documentation:
base_url – Base URL where the app is being served, which path is relative to. If not given, built from PREFERRED_URL_SCHEME, subdomain, SERVER_NAME, and APPLICATION_ROOT.
Now, I know that the existence of all those configuration values is because some users might prefer to specify some and let Flask infer the others, so it would be very useful to have an example that gives clarity on how to assign those values (especially if there is a need to use all of them). For example, if we have the following:
PREFERRED_URL_SCHEME = https
SERVER_NAME = www.mywebsite.com
APPLICATION_ROOT = /united-states
A user will know that he has to set the following configuration value (in case there is a need to use it):
BASE_URL = https://www.mywebsite.com/united-states
I'm not a Flask expert, I have only built two or three non-trivial apps. But there are still no answers yet, let me try to address some of your questions.
AFAIK, the only important setting is the APPLICATION_ROOT. It must match the http server's config or else the app will not work properly. In regular deployment the Flask will receive incoming requests containing the URL scheme and the server name, you don't have to put these into the app config.
The docs confirms that. "PREFERRED_URL_SCHEME Use this scheme for generating external URLs when not in a request context." But except in testing, the flask functions are in a request context.
The situation with the SERVER_NAME is little bit more complicated, it is related to subdomain matching, I'm not really familiar with. There is some information (with many upvotes!) here: Flask app that routes based on subdomain
The linked docs for test_request_context also says: "This is mostly useful during testing, where you may want to run a function that uses request data without dispatching a full request." It seems logical that in case of non-existing real request the missing values must be given by other means.
Finally, the code (testing.py file) answers your question how the base_url is being built from the pieces:
def __init__(
self,
app: "Flask",
path: str = "/",
base_url: t.Optional[str] = None,
subdomain: t.Optional[str] = None,
url_scheme: t.Optional[str] = None,
*args: t.Any,
**kwargs: t.Any,
) -> None:
assert not (base_url or subdomain or url_scheme) or (
base_url is not None
) != bool(
subdomain or url_scheme
), 'Cannot pass "subdomain" or "url_scheme" with "base_url".'
if base_url is None:
http_host = app.config.get("SERVER_NAME") or "localhost"
app_root = app.config["APPLICATION_ROOT"]
if subdomain:
http_host = f"{subdomain}.{http_host}"
if url_scheme is None:
url_scheme = app.config["PREFERRED_URL_SCHEME"]
url = url_parse(path)
base_url = (
f"{url.scheme or url_scheme}://{url.netloc or http_host}"
f"/{app_root.lstrip('/')}"
)

Failed to use auth code flow with python MSAL

I simply can't get acquire_token_by_auth_code_flow() from the MSAL package to work outside a flask app using the basic example giving in the MSAL documentation.
I think the problem comes from using the wrong authentication response which must be a "dict of the query string received from auth server" according to the documentation. In a flask app, I can simply use request.args which I'm not quite sure how to use outside of flask.
I've already tried using requests.request as well as urlsplit. The device flow is working fine as well as using the MSAL package in Java and connecting via R. So the app seems to be set up correctly.
The basic example code from the MSAL app below produces the error:
state mismatch: XXXXXXXXXXXX vs None
(so auth_response is wrong).
Any thoughts?
import requests
import msal
CLIENT_ID = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" # Application (client) ID of app registration
CLIENT_SECRET = "XX-XXXXXXXX-XXXXXXXX.XX~XXXXX~XXXX" # Placeholder - for use ONLY during testing.
AUTHORITY = "https://login.microsoftonline.com/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXX"
REDIRECT_PATH = "/getAToken" # Used for forming an absolute URL to your redirect URI.
# The absolute URL must match the redirect URI you set
# in the app's registration in the Azure portal.
ENDPOINT = 'https://graph.microsoft.com/v1.0/me'
SCOPE = ["https://graph.microsoft.com/.default"]
# Cache
cache = msal.SerializableTokenCache()
# Build msal app
app = msal.ConfidentialClientApplication(
CLIENT_ID, authority=AUTHORITY,
client_credential=CLIENT_SECRET, token_cache=cache)
# Initiate auth code flow
session = requests.Session()
session.flow = app.initiate_auth_code_flow(scopes=SCOPE, redirect_uri=REDIRECT_PATH)
# Aquire token
result = app.acquire_token_by_auth_code_flow(auth_code_flow=session.flow, auth_response = dict(parse.parse_qsl(parse.urlsplit(REDIRECT_PATH).query)))
The equivalent code for the last bit from the flask app looks like this with REDIRECT_PATH = "/getAToken":
#app.route(app_config.REDIRECT_PATH) # Its absolute URL must match your app's redirect_uri set in AAD
def authorized():
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(
session.get("flow", {}), request.args)
return redirect(url_for("index"))
Getting a token requires few requests according to documentation. To make it possible you need to create flow and store it inside session before navigating to microsoft login page.
session["flow"] = _build_auth_code_flow(authority=app_config.AUTHORITY, scopes=app_config.SCOPE)
After navigation back to your application you should use this flow object as you did in your example
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(
session.get("flow", {}), request.args)
Make sure that you didn't create it twice. In this case error will be similar, but state mismatch: XXXXXXXXXXXX vs XXXXXXXXXXXX. It may happened if you route called twice.
auth_response must be a dictionary built from the current HTTP request query params.
If this is a desktop application you must switch to PublicClientApplication. You can find a sample here.

Using OAuth to authenticate Office 365/Graph users with Django

We are creating an application for use in our organization, but we only want people in our organization to be able to use the app. We had the idea of using Microsoft's OAuth endpoint in order to authenticate whether a user is part of our org or not. The idea is to bring up a sign in screen where the user can enter their Office 365 username and password, which will then allow them to use our app upon submission of their credentials.
Our app is running on Django, and I've only found a solution to this problem using Flask and Microsoft's Graph API connect sample for Python (See code snippet below). This sample uses a similar idea to the one above to log in to the app. Are there any similar methods of authentication for Django?
import requests
from flask import Flask, redirect, url_for, session, request, render_template
from flask_oauthlib.client import OAuth
# read private credentials from text file
client_id, client_secret, *_ = open('_PRIVATE.txt').read().split('\n')
if (client_id.startswith('*') and client_id.endswith('*')) or \
(client_secret.startswith('*') and client_secret.endswith('*')):
print('MISSING CONFIGURATION: the _PRIVATE.txt file needs to be edited ' + \
'to add client ID and secret.')
sys.exit(1)
app = Flask(__name__)
app.debug = True
app.secret_key = 'development'
oauth = OAuth(app)
# since this sample runs locally without HTTPS, disable InsecureRequestWarning
requests.packages.urllib3.disable_warnings()
msgraphapi = oauth.remote_app( \
'microsoft',
consumer_key=client_id,
consumer_secret=client_secret,
request_token_params={'scope': 'User.Read Mail.Send'},
base_url='https://graph.microsoft.com/v1.0/',
request_token_url=None,
access_token_method='POST',
access_token_url='https://login.microsoftonline.com/common/oauth2/v2.0/token',
authorize_url='https://login.microsoftonline.com/common/oauth2/v2.0/authorize'
)
#app.route('/login')
def login():
"""Handler for login route."""
guid = uuid.uuid4() # guid used to only accept initiated logins
session['state'] = guid
return msgraphapi.authorize(callback=url_for('authorized', _external=True), state=guid)
#app.route('/login/authorized')
def authorized():
"""Handler for login/authorized route."""
response = msgraphapi.authorized_response()
if response is None:
return "Access Denied: Reason={0}\nError={1}".format( \
request.args['error'], request.args['error_description'])
# Check response for state
if str(session['state']) != str(request.args['state']):
raise Exception('State has been messed with, end authentication')
session['state'] = '' # reset session state to prevent re-use
# Okay to store this in a local variable, encrypt if it's going to client
# machine or database. Treat as a password.
session['microsoft_token'] = (response['access_token'], '')
# Store the token in another session variable for easy access
session['access_token'] = response['access_token']
me_response = msgraphapi.get('me')
me_data = json.loads(json.dumps(me_response.data))
username = me_data['displayName']
email_address = me_data['userPrincipalName']
session['alias'] = username
session['userEmailAddress'] = email_address
return redirect('main')
You should be able to use just about any OAUTH 2.0 library for Python. I've not worked with Django but I know there are several out there for Python.
I came across django-azure-ad-auth which seems to be exactly what you're looking for.
I also found a general OAUTH library called django-allauth which seems to have a lot of activity. It doesn't have a built-in provider but the model they use for providers seems simple enough that you may be able to extend it without too much trouble.

Find Host and Port in a Django Application

I'm connecting to the Twitter Streaming API and am setting up the OAuth handshake. I need to request a token and send a callback_url as a params dictionary along with post request.
I've hardcoded in the url for development (http://localhost:8000/oauth) but when I deploy this will change. I want to set up something that will find the host and port and set a reference it. Ideally, looking like "http://%s/oauth" % (domain_name)
I've tried using both the os and socket modules, and code is below:
class OAuth:
def request_oauthtoken(self):
name = socket.gethostname()
ip = socket.gethostbyname(name)
domain = socket.gethostbyaddr(ip) # Sequence attempts to find the current domain name. This will add expandability to the calling the API, as opposed to hard coding it in. It's not returning what I'm expecting
payload = { 'oauth_callback': 'http://localhost:8000/oauth' }
print(domain)
return payload
domain returns ('justins-mbp-2.local.tld', ['145.15.168.192.in-addr.arpa'], ['192.168.15.145'])
name returns the first element of the tuple above and ip returns the last item of the tuple unwrapped from the collection.
I'm looking for a return value of localhost or localhost:8000. I can work with either one.
call request.build_absolute_uri(), then extract the domain.
docs:
HttpRequest.build_absolute_uri(location) Returns the absolute URI form
of location. If no location is provided, the location will be set to
request.get_full_path().
If the location is already an absolute URI, it will not be altered.
Otherwise the absolute URI is built using the server variables
available in this request.
Example: "http://example.com/music/bands/the_beatles/?print=true"

Proxying to another web service with Flask

I want to proxy requests made to my Flask app to another web service running locally on the machine. I'd rather use Flask for this than our higher-level nginx instance so that we can reuse our existing authentication system built into our app. The more we can keep this "single sign on" the better.
Is there an existing module or other code to do this? Trying to bridge the Flask app through to something like httplib or urllib is proving to be a pain.
I spent a good deal of time working on this same thing and eventually found a solution using the requests library that seems to work well. It even handles setting multiple cookies in one response, which took a bit of investigation to figure out. Here's the flask view function:
from dotenv import load_dotenv # pip package python-dotenv
import os
#
from flask import request, Response
import requests # pip package requests
load_dotenv()
API_HOST = os.environ.get('API_HOST'); assert API_HOST, 'Envvar API_HOST is required'
#api.route('/', defaults={'path': ''}) # ref. https://medium.com/#zwork101/making-a-flask-proxy-server-online-in-10-lines-of-code-44b8721bca6
#api.route('/<path>')
def redirect_to_API_HOST(path): #NOTE var :path will be unused as all path we need will be read from :request ie from flask import request
res = requests.request( # ref. https://stackoverflow.com/a/36601467/248616
method = request.method,
url = request.url.replace(request.host_url, f'{API_HOST}/'),
headers = {k:v for k,v in request.headers if k.lower() == 'host'},
data = request.get_data(),
cookies = request.cookies,
allow_redirects = False,
)
#region exlcude some keys in :res response
excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] #NOTE we here exclude all "hop-by-hop headers" defined by RFC 2616 section 13.5.1 ref. https://www.rfc-editor.org/rfc/rfc2616#section-13.5.1
headers = [
(k,v) for k,v in res.raw.headers.items()
if k.lower() not in excluded_headers
]
#endregion exlcude some keys in :res response
response = Response(res.content, res.status_code, headers)
return response
Update April 2021: excluded_headers should probably include all "hop-by-hop headers" defined by RFC 2616 section 13.5.1.
I have an implementation of a proxy using httplib in a Werkzeug-based app (as in your case, I needed to use the webapp's authentication and authorization).
Although the Flask docs don't state how to access the HTTP headers, you can use request.headers (see Werkzeug documentation). If you don't need to modify the response, and the headers used by the proxied app are predictable, proxying is staightforward.
Note that if you don't need to modify the response, you should use the werkzeug.wsgi.wrap_file to wrap httplib's response stream. That allows passing of the open OS-level file descriptor to the HTTP server for optimal performance.
My original plan was for the public-facing URL to be something like http://www.example.com/admin/myapp proxying to http://myapp.internal.example.com/. Down that path leads madness.
Most webapps, particularly self-hosted ones, assume that they're going to be running at the root of a HTTP server and do things like reference other files by absolute path. To work around this, you have to rewrite URLs all over the place: Location headers and HTML, JavaScript, and CSS files.
I did write a Flask proxy blueprint which did this, and while it worked well enough for the one webapp I really wanted to proxy, it was not sustainable. It was a big mess of regular expressions.
In the end, I set up a new virtual host in nginx and used its own proxying. Since both were at the root of the host, URL rewriting was mostly unnecessary. (And what little was necessary, nginx's proxy module handled.) The webapp being proxied to does its own authentication which is good enough for now.

Categories