Giter Club home page Giter Club logo

Comments (22)

georgesequeira avatar georgesequeira commented on July 19, 2024 12

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.

from flask-smorest.

jul1u5 avatar jul1u5 commented on July 19, 2024 8

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

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

Otherwise SwaggerUI won't add Authorization header.

from flask-smorest.

Vyko avatar Vyko commented on July 19, 2024 6

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

from flask-smorest.

lafrech avatar lafrech commented on July 19, 2024 3

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.

from flask-smorest.

sjmh avatar sjmh commented on July 19, 2024 2

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.

from flask-smorest.

manfred-kaiser avatar manfred-kaiser commented on July 19, 2024 2

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"}

from flask-smorest.

ElDavoo avatar ElDavoo commented on July 19, 2024 1

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'

from flask-smorest.

lafrech avatar lafrech commented on July 19, 2024

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.

from flask-smorest.

dels07 avatar dels07 commented on July 19, 2024

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

from flask-smorest.

lafrech avatar lafrech commented on July 19, 2024

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():

from flask-smorest.

lafrech avatar lafrech commented on July 19, 2024

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.

from flask-smorest.

sjmh avatar sjmh commented on July 19, 2024

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 {}

from flask-smorest.

lafrech avatar lafrech commented on July 19, 2024

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.

from flask-smorest.

lafrech avatar lafrech commented on July 19, 2024

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?

from flask-smorest.

lafrech avatar lafrech commented on July 19, 2024

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

from flask-smorest.

DavidM42 avatar DavidM42 commented on July 19, 2024

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.

from flask-smorest.

lafrech avatar lafrech commented on July 19, 2024

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.

from flask-smorest.

lafrech avatar lafrech commented on July 19, 2024

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.

from flask-smorest.

cmabastar avatar cmabastar commented on July 19, 2024

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

from flask-smorest.

zaynOm avatar zaynOm commented on July 19, 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.

from flask-smorest.

arorajasman avatar arorajasman commented on July 19, 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  

from flask-smorest.

traderstechie avatar traderstechie commented on July 19, 2024

Solution by @georgesequeira simply worked without blinking!

Thanks @georgesequeira

from flask-smorest.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.