Home > Software design >  How to secure an endpoint for selected users with Flask-JWT-Extended?
How to secure an endpoint for selected users with Flask-JWT-Extended?

Time:05-13

Say I want to protect a route called /protected, I will do the following:

@app.route('/protected')
@jwt_required
def protected():
    return "Protected", 200

Doing so will mean that only authenticated users can access /protected. But what if I want to protect a route called /protected/123 such that only User123 is allowed to access the route?

To motivate this question, I am trying to implement an edit-profile feature. When User123 accesses /users/edit/123, the server will respond with existing user data. Of course, I want to make sure that the server will only respond when the request comes from User123.

CodePudding user response:

Flask-JWT-Extended does not offer any decorators that let you limit a view by userid.

You can check the user in the view and abort if the userid doesn't match the id in the route. E.g. if you are using the automatic user loading feature, check if the user id matches via the current_user object:

from flask import abort
from flask_jwt_extended import current_user

@app.route('/users/<userid:int>/edit')
@jwt_required
def users_edit(userid):
    if userid != current_user.id:
        abort(403)

    # ... handle view for matching user

Note: I changed the URL to put the userid right after /users/, so you'd get consistent URLs with other user-related routes.

If you are not using automic user loading but instead rely on the JWT identity claim (the sub claim usually), use get_jwt_identity() and check that value:

    # assuming that the sub claim is an integer value
    if userid != get_jwt_identity()

You can always create your own decorator that does the check before calling the decorated function:

from functools import wraps
from flask_jwt_extended import current_user, jwt_protected

def userid_must_match(f):
    """Abort with a 403 Forbidden if the userid doesn't match the jwt token

    This decorator adds the @protected decorator

    Checks for a `userid` parameter to the function and aborts with 
    status code 403 if this doesn't match the user identified by the
    token.
    
    """

    @wraps(f)
    @jwt_protected
    def wrapper(*args, userid=None, **kwargs):
        if userid is not None and userid != current_user.id:
            abort(403)
        return f(*args, **kwargs)

    return wrapper

The decorator assumes no check needs to be made if there is no userid parameter in the route.

Use like this:

@app.route('/users/<userid:int>/edit')
@userid_must_match
def users_edit():
    # ... handle view for matching user

From a design point of view, I'd make it so that you can leave out the userid; that way you can visit /users/edit and edit your own settings:

from flask import abort
from flask_jwt_extended import current_user

@app.route('/users/edit')
@app.route('/users/<userid:int>/edit')
@userid_must_match
def users_edit():
    # ... handle view for matching user via current_user

You can then consider checking for an administrator or superuser account if you want to grant access to editing any user to such accounts.

I'd also look into other Flask plugins such as Flask-Principal to handle roles and permissions, or even Flask-Security if you need more advanced user management options.

  • Related