The Setup:
The system should be as follows:
- There are two AWS loadbalancers, each of which runs a Docker container. One is a private loadbalancer, which contains an arbitrary service that ought to be protected from unauthenticated access; the other is a public-facing loadbalancer, which contains the authentication portal.
- Inside the Docker container (on the public loadbalancer) is an NginX server listening on port 80, and a Flask app serving on port 8000.
- NginX uses the
auth_request
directive to make a subrequest to the Flask app (passing along credentials as headers on the request), expecting either 200 or 401 in response.- The Flask app pulls the credentials from the request and checks if that username is already part of
flask.session
. If so, it immediately returns a 200 response; otherwise, it attempts to authenticate against an external source of truth. If it's successful, that user is added toflask.session
and the app returns a 200 response; otherwise, it will return a 401 response. - There also exists a check for expiration; if a user's been logged in for more than an hour, for example, the app should remove them from
flask.session
and return a 401 response. The user can then make a new login request.
- The Flask app pulls the credentials from the request and checks if that username is already part of
- If the response is 200, traffic is routed to the private loadbalancer.
The user's browser should cache the credentials that they've submitted, and so each new request should be able to immediately see that the user is part of flask.session
and avoid making a new authentication attempt.
The Issue:
Seemingly at random, while refreshing or navigating the protected resource (after successful authentication), sometimes the authentication popup appears and the user will be required to authenticate again. They can submit, and a single resource will be loaded before being prompted again for reauthentication.
Example:
The protected resource is a static website consisting of an index page, a CSS file, and three images. After initial authentication, the user refreshes the page several times. One of these times, the authentication prompt will be triggered. They will enter their credentials again, and the index page will load. They'll be prompted again for the CSS file, and then again for each image.
The Code
I'm not sure how many things I'll need to link here in order to root out the issue, so I'll begin with the nginx file responsible for making the auth_request
subrequest and the subsequent routing, and the two python files responsible for making the auth requests and handling sessions.
nginx.default
server {
listen 80;
server_name _;
location / {
auth_request /auth;
proxy_pass {{resource_endpoint}};
}
location /auth {
proxy_pass {{auth_backend}};
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
location /logout {
proxy_pass {{auth_backend}};
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
}
}
app.py
import flask
import auth0
import os
app = flask.Flask(__name__)
app.secret_key = os.getenv("SECRET_KEY", 'sooper seekrit')
@app.route('/auth', methods=['GET'])
@auth0.requires_auth
def login():
print("logging in")
resp_text = (
"Authentication successful."
)
return flask.Response(
response=resp_text,
status=200,
)
@app.route('/logout', methods=['GET'])
def logout():
# To successfully invalidate a user, we must both clear the Flask session
# as well as direct them to a '401: Unauthorized' route.
# http://stackoverflow.com/questions/233507/how-to-log-out-user-from-web-site-using-basic-authentication
print("logging out")
flask.session.clear()
return flask.Response(
response='Logout',
status=401,
)
if __name__ == "__main__":
app.debug = True
from gevent.wsgi import WSGIServer
http_server = WSGIServer(('', 8000), app)
http_server.serve_forever()
auth0.py
import json
import requests
import flask
import datetime
import os
from functools import wraps
def check_auth(username, password):
if 'username' in flask.session:
import pdb; pdb.set_trace()
if 'logged_in' in flask.session:
now = datetime.datetime.now()
expiry_window = datetime.timedelta(
minutes=int(os.getenv('AUTH0_EXPIRY'))
)
if flask.session['logged_in'] >= now - expiry_window:
return True
else:
flask.session.pop('username', None)
data = {
'client_id': os.getenv("AUTH0_CLIENT_ID"),
'username': username,
'password': password,
'id_token': '',
'connection': os.getenv("AUTH0_CONNECTION"),
'grant_type': 'password',
'scope': 'openid',
'device': ''
}
resp = requests.post(
url="https://" + os.getenv('AUTH0_DOMAIN') + "/oauth/ro",
data=json.dumps(data),
headers={"Content-type": "application/json"}
)
if 'error' in json.loads(resp.text):
return False
else:
flask.session['username'] = username
flask.session['logged_in'] = datetime.datetime.now()
return True
def authenticate():
return flask.Response(
'Could not verify your access level for that URL.\n'
'You have to login with proper credentials', 401,
{'WWW-Authenticate': 'Basic realm="Login Required"'},
)
def requires_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth = flask.request.authorization
if not auth or not check_auth(auth.username, auth.password):
return authenticate()
return f(*args, **kwargs)
return decorated