I'm trying to create a two-tiered authentication system with basic auth, using Flask-HTTPAuth. My application has two routes, a base route at /
accessible to any logged in user, and an admin route at /admin
accessible only to users who are (as you might expect) logged in as admins.
So I decided to implement this by chaining decorators, with the relevant part of the code as follows (where dbops is just a namespace that handles talking to the database):
@auth.verify_password
def verify_pw(lastname, password):
ln = lastname.lower()
if ln in dbops.list_users():
hashed_pw = dbops.find_hashed_password(ln)
return bcrypt.checkpw(password.encode('utf8'), hashed_pw.encode('utf8'))
return False
def must_be_admin(f):
@wraps(f)
def wrapper(*args, **kwargs):
if dbops.is_admin(auth.username()):
return f(*args, **kwargs)
return "Not authorized."
return wrapper
@core.route("/")
@auth.login_required
def dataentry():
return render_template("dataentry.html")
@core.route("/admin")
@must_be_admin
@auth.login_required
def admin():
return render_template("admin.html")
This works fine so long as anyone trying to log in as an admin user first visits the /
route: it prompts for a username and password, and then the admin user can go to /admin
and carry out logged-in admin tasks.
However, if an admin user first visits /admin
it doesn't give a login prompt. It just throws, and after poking around in the debugger I've determined that auth.username()
is returning an empty string. So, my guess is that for some reason, the inner decorator isn't being applied, hence the lack of a login prompt.
Does anyone know what might be going on here?
My first hypothesis was that this was an easy bug, because the inner function on the admin decorator wasn't being called until after the is_admin
check. So I tried to fix that my calling the function---and thus presumably making auth.username()
available--- before the check, as follows:
def must_be_admin(f):
@wraps(f)
def wrapper(*args, **kwargs):
dummy_to_get_username = f(*args, **kwargs)
if dbops.is_admin(auth.username()):
return dummy_to_get_username
return "Not authorized."
return wrapper
But that just caused the same behavior.
I see from this prior SO that the recommended way to do this from the library author is to just create two separate Flask-HTTPAuth objects. Which I can do, no problem. But clearly my mental model about how decorators are working are failing, so I'd like to solve this problem independent of getting the functionality I want working...