flaskjwtflask-jwt-extended

How to secure an endpoint for selected users with Flask-JWT-Extended?


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.


Solution

  • 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.