Flask restx Debug "Unauthorized" response

158 Views Asked by At

I have a Flask (2.2.3) app with Flask-RESTX (1.1.0) used as an API (without frontend). I'm using flask-azure-oauth library to authenticate users using Azure AD. The setup is:

from flask import Flask, current_app
from flask_azure_oauth import FlaskAzureOauth
from flask_restx import Api

app = Flask(__name__)
api = Api(app, <...>)
CORS(app)
auth = FlaskAzureOauth()
auth.init_app(app)

# App routes
@api.route("/foo")
class FooCollection(Resource):
    @auth('my_role')
    def get(self):
        return [<...>]

This was working fine, but since a few days I started to receive Unauthorized responses when passing valid token. Unfortunately I am not able to track the reason - tokens seem fine (examined manually or decoded using jwt.ms) and the only response I have from API is: 401 UNAUTHORIZED with response body { "message": null }.

I tried to add error logging and error handlers:

# Logging request/response
@app.before_request
def log_request_info():
    app.logger.debug(f"{request.method} {request.path} {request.data}")

@app.after_request
def log_response_info(response):
    app.logger.debug(f"{response.status}")
    return response

# Error handling
@app.errorhandler(Unauthorized)
def handle_error(error):
    current_app.logger.debug(f"Oops")
    <...>

@app.errorhandler
def handle_error(error):
    current_app.logger.debug(f"Noooo...!")
    <...>

With this, request and response are logged and non-HTTP exceptions are handled by handle_error. But HTTP errors like 404, 401, ... are just passing by, ignored by both generic error handler and a specific one (@app.errorhandler(Unauthorized)).

Here's some code used to validate that:

from werkzeug.exceptions import Unauthorized

def unauth():
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            raise Unauthorized("No-no")  # <<<
        return wrapper
    return decorator

@api.route("/dummy")
class Dummy(Resource):
    @unauth()
    def get(self):
        return jsonify(message="Hello there!")

@app.errorhandler
def handle_error(Exception):
    current_app.logger.debug("Intercepted")

Dummy route is protected by unauth decorator that denies all requests with 401 - UNAUTHORIZED and this is exactly what client receives. However @app.errorhandler(Exception), which is supposed to catch ALL exceptions, still misses it. Replace raise Unauthorized with something like 1 / 0 and the exception will normally be caught. HTTP errors then get some special treatment!

So how do I properly intercept and examine them? (with focus on: how do I find out why it denied token authorization)

1

There are 1 best solutions below

1
On

OAuth credentials have got a lifetime. It seems like your token's lifetime has expired and you are using the token without refreshing it.

I would recommend reading the scenario of Web App that signs in users from Microsoft.

If you look at the Microsoft Documentation for access tokens, you will see that each token has got a lifetime. When that lifetime expires, you have to request a new token using the refresh token that would have been provided to you when you first got the access token (the one you are currently providing for auth).

Good clients usually deal with refreshing tokens automatically (or the users can just sign in again once the token expires). Because you don't have a client (Frontend) yet, you would need to do this manually. There is some good documentation from Microsoft on how to get a new access token using the refresh token.

As for why your errors are not caught, it is because FlaskAzureOauth has its own wrapper around werkzeug.exceptions.HTTPException, which is raised in case of any errors from Azure. I would suggest you try and add an error handler for flask_azure_oauth.errors._HTTPException and see if it catches it. However, I'm not a 100% sure it will because the authentication and authorisation code is executed before the API code runs, and I am not sure if Flask's errorhandlers take that code into account.