I have set up a Python flask website with Azure AD authentication. The site works well and correctly authenticates me as an AD user signing onto the site. However, after a period of time, the AD authentication expires, and instead of what I'd expect is the standard behavior to refresh the token or redirect to sign in, the token expires and the site produces a 500 internal server error. Here is my flask code:
from flask import render_template
from flask import Flask, redirect, url_for
from flask_dance.contrib.azure import make_azure_blueprint, azure
class ReverseProxied(object):
def __init__(self, application):
self.application = application
def __call__(self, environ, start_response):
scheme = environ.get('HTTP_X_FORWARDED_PROTO')
if scheme:
environ['wsgi.url_scheme'] = scheme
return self.application(environ, start_response)
application = Flask(__name__)
application.wsgi_app = ReverseProxied(application.wsgi_app)
application.secret_key = "v6ZARqT7aE64WE+NClxEBw=="
blueprint = make_azure_blueprint(
client_id="",
client_secret="",
tenant="",
scope=["User.ReadBasic.All","openid","email","User.Read", "profile"]
)
application.register_blueprint(blueprint, url_prefix="/login")
@application.route('/')
def index():
if not azure.authorized:
return redirect(url_for("azure.login"))
resp = azure.get("/v1.0/me")
assert resp.ok
return render_template('index.html', user=resp.json()["userPrincipalName"])
@application.route('/protected')
def test():
if not azure.authorized:
return redirect(url_for("azure.login"))
resp = azure.get("/v1.0/me")
assert resp.ok
return render_template('test.html', user=resp.json()["userPrincipalName"])
@application.route('/user')
def userinfo():
if not azure.authorized:
return redirect(url_for("azure.login"))
resp = azure.get("/v1.0/me")
assert resp.ok
return render_template('userinfo.html', data=resp.json())
if __name__ == '__main__':
application.run(debug=True)
Here is the error from the Elastic Beanstalk / Apache logs when the token expires:
[Mon Sep 30 01:16:56.209088 2019] [:error] [pid 2778] [remote 172.31.0.71:136] [2019-09-30 01:16:56,207] ERROR in app: Exception on / [GET]
[Mon Sep 30 01:16:56.209131 2019] [:error] [pid 2778] [remote 172.31.0.71:136] Traceback (most recent call last):
[Mon Sep 30 01:16:56.209135 2019] [:error] [pid 2778] [remote 172.31.0.71:136] File "/opt/python/run/venv/local/lib/python3.6/site-packages/flask/app.py", line 2446, in wsgi_app
[Mon Sep 30 01:16:56.209138 2019] [:error] [pid 2778] [remote 172.31.0.71:136] response = self.full_dispatch_request()
[Mon Sep 30 01:16:56.209142 2019] [:error] [pid 2778] [remote 172.31.0.71:136] File "/opt/python/run/venv/local/lib/python3.6/site-packages/flask/app.py", line 1951, in full_dispatch_request
[Mon Sep 30 01:16:56.209145 2019] [:error] [pid 2778] [remote 172.31.0.71:136] rv = self.handle_user_exception(e)
[Mon Sep 30 01:16:56.209148 2019] [:error] [pid 2778] [remote 172.31.0.71:136] File "/opt/python/run/venv/local/lib/python3.6/site-packages/flask/app.py", line 1820, in handle_user_exception
[Mon Sep 30 01:16:56.209152 2019] [:error] [pid 2778] [remote 172.31.0.71:136] reraise(exc_type, exc_value, tb)
[Mon Sep 30 01:16:56.209155 2019] [:error] [pid 2778] [remote 172.31.0.71:136] File "/opt/python/run/venv/local/lib/python3.6/site-packages/flask/_compat.py", line 39, in reraise
[Mon Sep 30 01:16:56.209165 2019] [:error] [pid 2778] [remote 172.31.0.71:136] raise value
[Mon Sep 30 01:16:56.209168 2019] [:error] [pid 2778] [remote 172.31.0.71:136] File "/opt/python/run/venv/local/lib/python3.6/site-packages/flask/app.py", line 1949, in full_dispatch_request
[Mon Sep 30 01:16:56.209171 2019] [:error] [pid 2778] [remote 172.31.0.71:136] rv = self.dispatch_request()
[Mon Sep 30 01:16:56.209174 2019] [:error] [pid 2778] [remote 172.31.0.71:136] File "/opt/python/run/venv/local/lib/python3.6/site-packages/flask/app.py", line 1935, in dispatch_request
[Mon Sep 30 01:16:56.209177 2019] [:error] [pid 2778] [remote 172.31.0.71:136] return self.view_functions[rule.endpoint](**req.view_args)
[Mon Sep 30 01:16:56.209180 2019] [:error] [pid 2778] [remote 172.31.0.71:136] File "/opt/python/current/app/application.py", line 32, in index
[Mon Sep 30 01:16:56.209183 2019] [:error] [pid 2778] [remote 172.31.0.71:136] resp = azure.get("/v1.0/me")
[Mon Sep 30 01:16:56.209186 2019] [:error] [pid 2778] [remote 172.31.0.71:136] File "/opt/python/run/venv/local/lib/python3.6/site-packages/requests/sessions.py", line 546, in get
[Mon Sep 30 01:16:56.209188 2019] [:error] [pid 2778] [remote 172.31.0.71:136] return self.request('GET', url, **kwargs)
[Mon Sep 30 01:16:56.209191 2019] [:error] [pid 2778] [remote 172.31.0.71:136] File "/opt/python/run/venv/local/lib/python3.6/site-packages/flask_dance/consumer/requests.py", line 201, in request
[Mon Sep 30 01:16:56.209194 2019] [:error] [pid 2778] [remote 172.31.0.71:136] **kwargs
[Mon Sep 30 01:16:56.209197 2019] [:error] [pid 2778] [remote 172.31.0.71:136] File "/opt/python/run/venv/local/lib/python3.6/site-packages/requests_oauthlib/oauth2_session.py", line 395, in request
[Mon Sep 30 01:16:56.209200 2019] [:error] [pid 2778] [remote 172.31.0.71:136] http_method=method, body=data, headers=headers)
[Mon Sep 30 01:16:56.209203 2019] [:error] [pid 2778] [remote 172.31.0.71:136] File "/opt/python/run/venv/local/lib/python3.6/site-packages/oauthlib/oauth2/rfc6749/clients/base.py", line 198, in add_token
[Mon Sep 30 01:16:56.209206 2019] [:error] [pid 2778] [remote 172.31.0.71:136] raise TokenExpiredError()
[Mon Sep 30 01:16:56.209210 2019] [:error] [pid 2778] [remote 172.31.0.71:136] oauthlib.oauth2.rfc6749.errors.TokenExpiredError: (token_expired)
Now I'm aware there is a workaround floating around that says to try/catch the expired error and redirect the user to the login page again. But surely there is a way to configure or code this better than having to catch exceptions? I have done Azure AD authentication using ASP.NET websites and never had to do that kind of thing, the expiration would be naturally handed and the user redirected to login in again.