Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authorization to header? #36

Closed
dels07 opened this issue Jan 23, 2019 · 22 comments
Closed

Add authorization to header? #36

dels07 opened this issue Jan 23, 2019 · 22 comments
Labels

Comments

@dels07
Copy link

dels07 commented Jan 23, 2019

Currently I use flask_jwt_extended for API security, how can I define the need of passing Authorization header?

I tried using this:
@blp.doc(parameters={'Authorization': {'name': 'Authorization', 'in': 'header', 'description': 'Authorization: Bearer <access_token>', 'required': 'true'}})

but it throw an error

builtins.KeyError
KeyError: 'in

@lafrech
Copy link
Member

lafrech commented Jan 23, 2019

I didn't try, but I think this syntax should avoid the error.

    @blp.doc(parameters=[{'name': 'Authorization', 'in': 'header', 'description': 'Authorization: Bearer <access_token>', 'required': 'true'}])

However I think this is wrong, because this list of parameters will replace the list that is generated automatically rather than extend it.

I think the proper way to do that would be to create your own decorator, extending @jwt_required, that would append your parameter to func._apidoc. Check how the arguments does that (https://github.com/Nobatek/flask-rest-api/blob/master/flask_rest_api/arguments.py#L55).

It shouldn't be complicated. Tell me how it goes, and feel free to ask if this is unclear.

@dels07
Copy link
Author

dels07 commented Jan 28, 2019

Thanks for your suggestion @lafrech

I'm still new to python, this is the code I tried

def doc_jwt_required(arg):
    @wraps(func)
    def wrapper(func):
        @jwt_required
        def inner(*args, **kwargs):
            parameters = [{
                'name': 'Authorization',
                'in': 'header',
                'description': 'Authorization: Bearer <access_token>',
                'required': 'true'
            }]

            func._apidoc = getattr(func, '_apidoc', {})
            func._apidoc.setdefault('parameters', []).append(parameters)
            func(*args, **kwargs)

        return inner
    return wrapper

However I checked that now all my parameters are gone for the route that implementing above decorator, could you help me what's wrong with it

@lafrech
Copy link
Member

lafrech commented Jan 28, 2019

Maybe something like this would work (I didn't test):

from functools import wraps

def doc_jwt_required():
    @wraps
    def wrapper(func):
        parameters = [{
            'name': 'Authorization',
            'in': 'header',
            'description': 'Authorization: Bearer <access_token>',
            'required': 'true'
        }]
        func._apidoc = getattr(func, '_apidoc', {})
        func._apidoc.setdefault('parameters', []).append(parameters)
        return jwt_required(func)
    return wrapper

Tell me if it works.

You can subclass Blueprint to add this decorator as a classmethod, so that you can call

    @blp.doc_jwt_required
    def view_function():

@sjmh
Copy link

sjmh commented Feb 5, 2019

Here's the decorator I created for use with flask_httpauth @dels07

def doc_login_required(func):
    # 'Decorate' the function with the real authentication decorator
    auth_required_func = auth.login_required(func)

    # Create the wrapped function.  This just calls the 'decorated' function
    @wraps(func)
    def wrapper(*args, **kwargs):
        return auth_required_func(*args, **kwargs)


    # Update the api docs on the wrapped function and return it to be
    # further decorated by other decorators
    parameters = {
        'name': 'Authorization',
        'in': 'header',
        'description': 'Authorization: Bearer <access_token>',
        'required': 'true'
    }

    wrapper._apidoc = getattr(func, '_apidoc', {})
    wrapper._apidoc.setdefault('parameters', []).append(parameters)

    return wrapper

We already had lots of @auth.login_required decorators throughout the code, so I actually just subclassed the HTTPTokenAuth from flask_httpauth with this method and overwrote their method.

class DocHTTPTokenAuth(HTTPTokenAuth):
    def login_required(self, func):
        # 'Decorate' the function with the real authentication decorator
        auth_required_func = super().login_required(func)
    
        # Create the wrapped function.  This just calls the 'decorated' function
        @wraps(func)
        def wrapper(*args, **kwargs):
            return auth_required_func(*args, **kwargs)
    
        # Update the api docs on the wrapped function and return it to be
        # further decorated by other decorators
        parameters = {
            'name': 'Authorization',
            'in': 'header',
            'description': 'Authorization: Bearer <access_token>',
            'required': 'true'
        }
    
        wrapper._apidoc = getattr(func, '_apidoc', {})
        wrapper._apidoc.setdefault('parameters', []).append(parameters)
    
        return wrapper

Then instead of auth = HTTPTokenAuth(), I do auth = DocHTTPTokenAuth() and can use @auth.login_required as normal.

Hope this helps.

@lafrech
Copy link
Member

lafrech commented Feb 6, 2019

Thanks @sjmh. That's the way to go.

(Hint: when pasting code, add py after the three backquotes (```py) to get syntax highlighting.)

@dels07, please try something along those lines and tell us how it goes.

@sjmh
Copy link

sjmh commented Feb 8, 2019

Also, just as a heads up, depending on the auth library ( in my case, flask_httpauth ), the @auth.login_required decorator has to go at the top of your decorators, otherwise it tries to pass it's Flask Response object to @blp.response, which then bombs.

So you want:

@auth.login_required
@blp.response(MySchema)
def do_something():
  return {}

@lafrech
Copy link
Member

lafrech commented Feb 8, 2019

There's been discussions about allowing response to swallow a Response object (see #22). I've been busy with other stuff and I'm not done with this yet.

As a general rule, I'd always place authentication/authorization logic on top to drop the request asap if the client is not authorized.

@lafrech
Copy link
Member

lafrech commented Feb 24, 2019

it tries to pass it's Flask Response object to @blp.response, which then bombs.

@sjmh I just sent a PR fixing this (#40). Feedback welcome.

@dels07 did you manage to adapt @sjmh's code to your need?

@lafrech
Copy link
Member

lafrech commented Mar 8, 2019

Closing this for now. @dels07, feel free to comment if you're still stuck with this.

@lafrech lafrech closed this as completed Mar 8, 2019
@DavidM42
Copy link

Could somebody please further explain how to achieve this like @sjmh did? I think a small section in the documentation would also be very helpful. I tried to do what @sjmh did and it only kind of worked. ReDoc displays the auth token as parameter but SwaggerUi errors with TypeError: "O is undefined" . I think that is because openapi (3) expects a defined security schema or somethings as described here.
I'd really like to do this in my api as I think authentification is an integral part of documenting your api.

@lafrech
Copy link
Member

lafrech commented Apr 10, 2019

Indeed, the decorator proposed above adds an auth parameter to the resource.

To add a security scheme, use the api.spec.components.security_scheme method (see apispec doc).

No time right now to elaborate. Hope this helps.

@georgesequeira
Copy link

georgesequeira commented Oct 18, 2019

Hoping this helps others in the future but @DavidM42, I created an API where the entire application required the bearer token. In order to do this, I set the API_SPEC_OPTIONS flask configuration variable before instantiating my flask-smorest API to the following:

    app.config['API_SPEC_OPTIONS'] = {
        'security':[{"bearerAuth": []}],
        'components':{
            "securitySchemes":
                {
                    "bearerAuth": {
                        "type":"http",
                        "scheme": "bearer",
                        "bearerFormat": "JWT"
                    }
                }
        }
    }

    api = flask_smorest.Api(app)

This gave me the ability to set the token at the application level in the swagger ui:

image

Since I wanted to lock down all paths I did this for the flask app

from flask_jwt_extended import verify_jwt_in_request

@app.before_request
def before_jwt():
  verify_jwt_in_request()

This would 401 if the jwt is not present and valid.

@lafrech
Copy link
Member

lafrech commented Oct 18, 2019

Thanks for sharing.

Note that unless you really need to configure this in app config, you could use the dedicated components method to pass the security scheme at init (see https://apispec.readthedocs.io/en/stable/special_topics.html#documenting-security-schemes):

api.spec.components.security_scheme(
    "bearerAuth", {"type":"http", "scheme": "bearer", "bearerFormat": "JWT"}
)

Not a huge benefit, I admit. Well, this abstracts OpenAPI version, but one probably doesn't need that.

@jul1u5
Copy link

jul1u5 commented Mar 11, 2020

Note that by using @lafrech suggestion, you still need to add:

api.spec.options["security"] = [{"bearerAuth": []}]

Otherwise SwaggerUI won't add Authorization header.

@lafrech
Copy link
Member

lafrech commented Jul 15, 2020

Here's a link to a project in which I overload flask-httpauth's login_required to create a decorator that does the auth and documents both the 401 header and the fact that a basic auth is performed.

https://github.com/sigopti/pyodhean-server/blob/master/pyodhean_server/api.py

I also add the basic auth schema to the components.

@cmabastar
Copy link

Hello, hoping this helps!

In case anyone would stumble on this. I found a neat solution just to mark certains routes with authorized swagger button. This pattern is base on some fastapi workaround in which I am also using. This also decouples your stuff from flask-jwt-extended.

In your config to enable the lock button as mentioned in the previous posts.

    API_SPEC_OPTIONS = {
        "components": {
            "securitySchemes": {
                "Bearer Auth": {
                    "type": "apiKey",
                    "in": "header",
                    "name": "Authorization",
                    "bearerFormat": "JWT",
                    "description": "Enter: **'Bearer &lt;JWT&gt;'**, where JWT is the access token",
                }
            }
        },
    }

And then in your app factory. You can add this fix for the spec.

def create_app(config_name: Optional[str] = None):
    app = Flask("webapp")
    if not config_name:
        config_name = os.getenv("FLASK_CONFIG", "default")
    app.config.from_object(config[config_name])
    register_extensions(app)
    register_blueprints(app)
    return app


def register_extensions(app):
    """Register Flask extensions."""

    jwt.init_app(app)
    api.init_app(app)
    return None


def register_blueprints(app):
    """Register Flask blueprints."""
    api.register_blueprint(users.views.bp, url_prefix="/api/user")

    for path, items in api.spec._paths.items():
        for method in items.keys():
            if api.spec._paths[path][method].get("authorize", False):
                api.spec._paths[path][method]["security"] = [{"Bearer Auth": []}]

Then configure your routes like this.

@bp.route("/whoami")
class WhoAmI(MethodView):
    @bp.doc(authorize=True)
    @jwt_required()
    def get(self):
        return {"test": "hi"}

This way you'll get selective lock button.

image

image

@manfred-kaiser
Copy link

Based on @cmabastar solution I have written an improved decorator.
I wanted a solution, which adds the correct authentication methods depending on the used decorator.
I also wanted to use a single decorator for restricting the url to a specific authentication and add the information to the docs.

Add the authentication schemes to your documentation:

This is the same definition as explained in previous posts.

    API_SPEC_OPTIONS = {
        "components": {
            "securitySchemes": {
                "Bearer Auth": {
                    "type": "apiKey",
                    "in": "header",
                    "name": "Authorization",
                    "bearerFormat": "JWT",
                    "description": "Enter: **'Bearer &lt;JWT&gt;'**, where JWT is the access token",
                }
            }
        },
    }

Create a decorator for JWT based authentication:

This decorator is a new version, which adds the authentication schema without postprocessing in the app.

from copy import deepcopy
from functools import wraps

def jwt_required(func):
    @wraps(func)
    def decorator(*args, **kwargs):
        # validate the request or call some other methods
        return func(*args, **kwargs)

    decorator._apidoc = deepcopy(func, '_apidoc', {})
    decorator._apidoc.setdefault('manual_doc', {})
    decorator._apidoc['manual_doc']['security'] = [{"Bearer Auth": []}]
    return decorator

Add the decorator to the view:

Only the created @jwt_required decorator is needed.

@bp.route("/whoami")
class WhoAmI(MethodView):
    @jwt_required()
    def get(self):
        return {"test": "hi"}

@ElDavoo
Copy link

ElDavoo commented Jan 22, 2023

Based on @cmabastar solution I have written an improved decorator.

I'm getting:

 line 100, in cookie_required
    decorator._apidoc = deepcopy(func, '_apidoc', {})
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Program Files\Python311\Lib\copy.py", line 138, in deepcopy
    y = memo.get(d, _nil)
        ^^^^^^^^
AttributeError: 'str' object has no attribute 'get'

@Vyko
Copy link

Vyko commented Feb 19, 2023

Replace decorator._apidoc = deepcopy(func, '_apidoc', {}) with
decorator._apidoc = deepcopy(getattr(func, "_apidoc", {}))

Also, to complete @manfred-kaiser answer, for those who want to use the @jwt_required decorator from flask_jwt_extended you can upgrade the snippet like this:

from copy import deepcopy
from functools import wraps

from flask_jwt_extended import jwt_required


def jwt_required_with_doc(*args, **kwargs):
    def decorator(func):
        @wraps(func)
        def wrapper(*f_args, **f_kwargs):
            return jwt_required(*args, **kwargs)(func)(*f_args, **f_kwargs)

        wrapper._apidoc = deepcopy(getattr(func, "_apidoc", {}))
        wrapper._apidoc.setdefault('manual_doc', {})
        wrapper._apidoc['manual_doc']['security'] = [{"Bearer Auth": []}]
        return wrapper
    return decorator

@zaynOm
Copy link

zaynOm commented Mar 6, 2024

The issue I found while using the decorator provided by @Vyko is that it does not add the BearerAuth token to the requests, but when I add it to all routes using api.spec.options["security"] = [{"bearerAuth": []}] it works.

@arorajasman
Copy link

arorajasman commented Mar 7, 2024

Hi if any one is using flask_smorest then this solution might work, since it worked for me

@auth_blueprint.route("/user")
class UserDetails(MethodView):
    """Service to get the details of the user based on the id of the user"""

    @auth_blueprint.response(200, UserDetailsSchema)
    @jwt_required()
    @auth_blueprint.doc(
        security=[{"bearerAuth": []}],
        components={
            "securitySchemes": {
                "bearerAuth": {
                    "type": "http",
                    "scheme": "bearer",
                    "bearerFormat": "JWT",
                }
            }
        },
    )
    def get(self):
        """Service to get the details of the user based on the id of the user"""  # noqa
        try:
            jwt = get_jwt()
            user_id = jwt["sub"]["id"]
            user_email = jwt["sub"]["email"]  # noqa
            user = User.query.filter(User.id == user_id).first()
            if not user:
                raise NotFound(404, "User not found")
            return user, 200
        except CustomError as e:
            abort(e.status_code, message=str(e.message))
        except SQLAlchemyError as e:
            abort(500, message=f"Database error: {e}")
        except Exception as e:
            abort(500, message=str(e))

in the code above I've added

@auth_blueprint.doc(
        security=[{"bearerAuth": []}],
        components={
            "securitySchemes": {
                "bearerAuth": {
                    "type": "http",
                    "scheme": "bearer",
                    "bearerFormat": "JWT",
                }
            }
        },
    )

to show the lock icon and to get the jwt token for the get service in swagger
and in the config I've added the below configuration

  app.config["API_SPEC_OPTIONS"] = {
        "components": {
            "securitySchemes": {
                "bearerAuth": {
                    "type": "http",
                    "scheme": "bearer",
                    "bearerFormat": "JWT",
                }
            }
        },
    }
Hope this might help. If there are any issues then please let me know  

@chimeziriobioha
Copy link

Solution by @georgesequeira simply worked without blinking!

Thanks @georgesequeira

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests