diff --git a/connexion/security.py b/connexion/security.py index 692d34d91..ddfd14b16 100644 --- a/connexion/security.py +++ b/connexion/security.py @@ -490,6 +490,11 @@ def parse_security_scheme( security_handler = self.security_handlers["apiKey"] return security_handler().get_fn(security_scheme, required_scopes) + # Custom security handler + elif (scheme := security_scheme["scheme"].lower()) in self.security_handlers: + security_handler = self.security_handlers[scheme] + return security_handler().get_fn(security_scheme, required_scopes) + else: logger.warning( "... Unsupported security scheme type %s", diff --git a/docs/conf.py b/docs/conf.py index c446d93ef..9f1a92dab 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,6 +32,7 @@ 'sphinx_copybutton', 'sphinx_design', 'sphinx.ext.autosectionlabel', + 'sphinxemoji.sphinxemoji', ] autosectionlabel_prefix_document = True diff --git a/docs/routing.rst b/docs/routing.rst index e12611700..018ebd08c 100644 --- a/docs/routing.rst +++ b/docs/routing.rst @@ -390,7 +390,8 @@ Convertors are used by defining them as the ``format`` in the parameter specific Specify a route parameter's type as ``integer`` or ``number`` or its type as ``string`` and its format as ``path`` to use these converters. -Path parameters are passed as arguments to your python function, see :doc:`parameters`. +Path parameters are passed as :ref:`arguments ` to your +python function. Individual paths ---------------- diff --git a/docs/security.rst b/docs/security.rst index 06fb288c4..fac723a1d 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -1,87 +1,152 @@ Security ======== -OAuth 2 Authentication and Authorization ----------------------------------------- +Connexion implements a pluggable security validation mechanism and provides built-in support for +some of the most popular security schemes. + +.. csv-table:: + :widths: 30, 70 + :header-rows: 1 + + **Swagger 2**, **Connexion support** + Basic Authentication, |:white_check_mark:| + API key, |:white_check_mark:| + Oauth2, |:white_check_mark:| + **OpenAPI**, + HTTP Basic, |:white_check_mark:| + HTTP Bearer, |:white_check_mark:| + Other HTTP schemes (RFC 7253), "No built-in support, use a `custom security handler <#custom-security-handlers>`_" + API key, |:white_check_mark:| + Oauth2, |:white_check_mark:| + OpenID, "No built-in support, use a `custom security handler <#custom-security-handlers>`_" + +General authentication flow +--------------------------- -Connexion supports one of the three OAuth 2 handling methods. -With Connexion, the API security definition **must** include a -``x-tokenInfoFunc`` or set ``TOKENINFO_FUNC`` env var. - -``x-tokenInfoFunc`` must contain a reference to a function -used to obtain the token info. This reference should be a string using -the same syntax that is used to connect an ``operationId`` to a Python -function when routing. For example, an ``x-tokenInfoFunc`` with a value of -``auth.verifyToken`` would pass the user's token string to the function -``verifyToken`` in the module ``auth.py``. The referenced function accepts -a token string as argument and should return a dict containing a ``scope`` -field that is either a space-separated list or an array of scopes belonging to -the supplied token. This list of scopes will be validated against the scopes -required by the API security definition to determine if the user is authorized. -You can supply a custom scope validation func with ``x-scopeValidateFunc`` -or set ``SCOPEVALIDATE_FUNC`` env var, otherwise default scope validation function -``connexion.security.security_handler_factory.validate_scope`` will be used automatically. +For each supported authentication type, Connexion lets you register a validation function to +validate the incoming credentials, and return information about the authenticated user. +The validation function must either be defined in the API security definition +as ``x-{type}InfoFunc``, or in the environment variables as ``{TYPE}INFO_FUNC``. The function +should be referenced as a string using the same syntax that is used to connect an ``operationId`` +to a Python function when :ref:`routing `. -The recommended approach is to return a dict which complies with -`RFC 7662 `_. Note that you have to validate the ``active`` -or ``exp`` fields etc. yourself. +While the validation functions should accept different arguments based on the authentication type +(as documented below), they should all return a dict which complies with `RFC 7662 `_: -The Token Info response will be passed in the ``token_info`` argument to the handler -function. The ``sub`` property of the Token Info response will be passed in the ``user`` -argument to the handler function. +.. code-block:: json -Deprecated features, retained for backward compatibility: + { + "active": true, + "client_id": "l238j323ds-23ij4", + "username": "jdoe", + "scope": "read write dolphin", + "sub": "Z5O3upPC88QrAjx00dis", + "aud": "https://protected.example.net/resource", + "iss": "https://server.example.com/", + "exp": 1419356238, + "iat": 1419350238, + "extension_field": "twenty-seven" + } -- As alternative to ``x-tokenInfoFunc``, you can set ``x-tokenInfoUrl`` or - ``TOKENINFO_URL`` env var. It must contain a URL to validate and get the token - information which complies with `RFC 6749 `_. - When both ``x-tokenInfoUrl`` and ``x-tokenInfoFunc`` are used, Connexion - will prioritize the function method. Connexion expects the authorization - server to receive the OAuth token in the ``Authorization`` header field in the - format described in `RFC 6750 `_ section 2.1. This aspect represents - a significant difference from the usual OAuth flow. -- ``scope`` field can also be named ``scopes``. -- ``sub`` field can also be named ``uid``. +The token information is made available to your endpoint view functions via the +:ref:`context `, which you can also have passed in as an +:ref:`argument `. -You can find a `minimal OAuth example application`_ showing the use of -``x-tokenInfoUrl``, and `another OAuth example`_ showing the use of -``x-tokenInfoFunc`` in Connexion's "examples" folder. +.. note:: -.. _minimal OAuth example application: https://github.com/spec-first/connexion/tree/main/examples/oauth2 -.. _another OAuth example: https://github.com/spec-first/connexion/tree/main/examples/oauth2_local_tokeninfo + Note that you are responsible to validate any fields other than the scopes yourself. + +.. _rfc7662: https://tools.ietf.org/html/rfc7662 Basic Authentication -------------------- -With Connexion, the API security definition **must** include a -``x-basicInfoFunc`` or set ``BASICINFO_FUNC`` env var. It uses the same -semantics as for ``x-tokenInfoFunc``, but the function accepts three -parameters: username, password and required_scopes. +For Basic authentication, the API security definition must include an +``x-basicInfoFunc`` definition or set the ``BASICINFO_FUNC`` environment variable. + +The function should accept the following arguments: + +- username +- password +- required_scopes (optional) You can find a `minimal Basic Auth example application`_ in Connexion's "examples" folder. -.. _oauth scope: https://oauth.net/2/scope/ .. _minimal Basic Auth example application: https://github.com/spec-first/connexion/tree/main/examples/basicauth +Bearer Authentication (JWT) +--------------------------- + +For Bearer authentication (JWT), the API security definition must include an +``x-bearerInfoFunc`` definition or set the ``BEARERINFO_FUNC`` environment variable. + +The function should accept the following arguments: + +- token +- required_scopes (optional) + +You can find a `minimal Bearer example application`_ in Connexion's "examples" folder. + +.. _minimal Bearer example application: https://github.com/spec-first/connexion/tree/main/examples/jwt + ApiKey Authentication --------------------- -With Connexion, the API security definition **must** include a -``x-apikeyInfoFunc`` or set ``APIKEYINFO_FUNC`` env var. It uses the same -semantics as for ``x-basicInfoFunc``, but the function accepts two -parameters: apikey and required_scopes. +For API key authentication, the API security definition must include an +``x-apikeyInfoFunc`` definition or set the ``APIKEYINFO_FUNC`` environment variable. + +The function should accept the following arguments: + +- apikey +- required_scopes (optional) You can find a `minimal API Key example application`_ in Connexion's "examples" folder. -Bearer Authentication (JWT) ---------------------------- +.. _minimal API Key example application: https://github.com/spec-first/connexion/tree/main/examples/apikey + +OAuth 2 Authentication and Authorization +---------------------------------------- -With Connexion, the API security definition **must** include a -``x-bearerInfoFunc`` or set ``BEARERINFO_FUNC`` env var. It uses the same -semantics as for ``x-tokenInfoFunc``, but the function accepts one parameter: token. +For OAuth authentication, the API security definition must include an +``x-tokenInfoFunc`` definition or set the ``TOKENINFO_FUNC`` environment variable. -You can find a `minimal JWT example application`_ in Connexion's "examples" folder. +The function should accept the following arguments: + +- token +- required_scopes (optional) + +As alternative to an ``x-tokenInfoFunc`` definition, you can set an ``x-tokenInfoUrl`` definition or +``TOKENINFO_URL`` environment variable, and connexion will call the url instead of a local +function instead. Connexion expects the authorization server to receive the OAuth token in the +``Authorization`` header field in the format described in `RFC 6750 `_ section 2.1 and +return the token information in the same format as a validation function. When both +``x-tokenInfoUrl`` and ``x-tokenInfoFunc`` are used, Connexion will prioritize the function. + +The list of scopes returned in the token information will be validated against the scopes +required by the API security definition to determine if the user is authorized. +You can supply a custom scope validation func by defining ``x-scopeValidateFunc`` +or setting a ``SCOPEVALIDATE_FUNC`` environment variable. + +The function should accept the following arguments: + +- required_scopes +- token_scopes + +and return a boolean indicating if the validation was successful. + +Deprecated features, retained for backward compatibility: + +- ``scope`` field can also be named ``scopes``. +- ``sub`` field can also be named ``uid``. + +You can find a `minimal OAuth example application`_ showing the use of +``x-tokenInfoUrl``, and `another OAuth example`_ showing the use of +``x-tokenInfoFunc`` in Connexion's "examples" folder. + +.. _minimal OAuth example application: https://github.com/spec-first/connexion/tree/main/examples/oauth2 +.. _another OAuth example: https://github.com/spec-first/connexion/tree/main/examples/oauth2_local_tokeninfo +.. _rfc6750: https://tools.ietf.org/html/rfc6750 Multiple Authentication Schemes ------------------------------- @@ -96,45 +161,70 @@ Multiple OAuth2 security schemes in AND fashion are not supported. .. _OpenAPI specification: https://swagger.io/docs/specification/authentication/#multiple -Deploying Authentication +Custom security handlers ------------------------ -Some production hosting environments, such as Apache with modwsgi, do not by default pass -authentication headers to WSGI applications. Therefore, to allow connexion to handle -authentication, you will need to enable passthrough. +You can implement your own security handlers for schemes that are not supported yet in Connexion +by subclassing the ``connexion.security.AbstractSecurityHandler`` class and passing it in a custom +``security_map`` to your application or API: -Instructions for `enabling authentication passthrough in modwsgi`_ are available as -part of the `modwsgi documentation`_. +.. code-block:: python + :caption: **app.py** -HTTPS Support -------------- + from connexion.security import AbstractSecurityHandler -When specifying HTTPS as the scheme in the API YAML file, all the URIs -in the served Swagger UI are HTTPS endpoints. The problem: The default -server that runs is a "normal" HTTP server. This means that the -Swagger UI cannot be used to play with the API. What is the correct -way to start a HTTPS server when using Connexion? -One way, `described by Flask`_, looks like this: + class MyCustomSecurityHandler(AbstractSecurityHandler): -.. code-block:: python + security_definition_key = "x-{type}InfoFunc" + environ_key = "{TYPE}INFO_FUNC" - from OpenSSL import SSL - context = SSL.Context(SSL.SSLv23_METHOD) - context.use_privatekey_file('yourserver.key') - context.use_certificate_file('yourserver.crt') + def _get_verify_func(self, {type}_info_func): + ... - app.run(host='127.0.0.1', port='12344', - debug=False/True, ssl_context=context) + security_map = { + "{type}": MyCustomSecurityHandler, + } -However, Connexion doesn't provide an ssl_context parameter. This is -because Flask doesn't, either--but it uses ``**kwargs`` to send the -parameters to the underlying `werkzeug`_ server. +.. tab-set:: -.. _rfc6750: https://tools.ietf.org/html/rfc6750 -.. _rfc6749: https://tools.ietf.org/html/rfc6749 -.. _rfc7662: https://tools.ietf.org/html/rfc7662 -.. _minimal API Key example application: https://github.com/spec-first/connexion/tree/main/examples/apikey -.. _minimal JWT example application: https://github.com/spec-first/connexion/tree/main/examples/jwt -.. _enabling authentication passthrough in modwsgi: https://modwsgi.readthedocs.io/en/develop/configuration-directives/WSGIPassAuthorization.html -.. _modwsgi documentation: https://modwsgi.readthedocs.io/en/develop/index.html + .. tab-item:: AsyncApp + :sync: AsyncApp + + .. code-block:: python + :caption: **app.py** + + from connexion import AsyncApp + + app = AsyncApp(__name__, security_map=security_map) + app.add_api("openapi.yaml", security_map=security_map) + + + .. tab-item:: FlaskApp + :sync: FlaskApp + + .. code-block:: python + :caption: **app.py** + + from connexion import FlaskApp + + app = FlaskApp(__name__, security_map=security_map) + app.add_api("openapi.yaml", security_map=security_map) + + .. tab-item:: ConnexionMiddleware + :sync: ConnexionMiddleware + + .. code-block:: python + :caption: **app.py** + + from asgi_framework import App + from connexion import ConnexionMiddleware + + app = App(__name__) + app = ConnexionMiddleware(app, security_map=security_map) + app.add_api("openapi.yaml", security_map=security_map) + +.. note:: + + If you implement a custom security handler, and think it would be valuable for other users, we + would appreciate it as a contribution. diff --git a/examples/oauth2/mock_tokeninfo.py b/examples/oauth2/mock_tokeninfo.py index 8182aa554..0064006ef 100755 --- a/examples/oauth2/mock_tokeninfo.py +++ b/examples/oauth2/mock_tokeninfo.py @@ -16,12 +16,12 @@ def get_tokeninfo() -> dict: except Exception: access_token = "" - uid = TOKENS.get(access_token) + sub = TOKENS.get(access_token) - if not uid: + if not sub: return "No such token", 401 - return {"uid": uid, "scope": ["uid"]} + return {"sub": sub, "scope": ["uid"]} if __name__ == "__main__": diff --git a/examples/oauth2_local_tokeninfo/app.py b/examples/oauth2_local_tokeninfo/app.py index f12a29b89..b98c310a8 100755 --- a/examples/oauth2_local_tokeninfo/app.py +++ b/examples/oauth2_local_tokeninfo/app.py @@ -13,11 +13,11 @@ def get_secret(user) -> str: return f"You are: {user}" -def token_info(access_token) -> dict: - uid = TOKENS.get(access_token) - if not uid: +def token_info(token) -> dict: + sub = TOKENS.get(token) + if not sub: return None - return {"uid": uid, "scope": ["uid"]} + return {"sub": sub, "scope": ["uid"]} app = connexion.FlaskApp(__name__, specification_dir="spec") diff --git a/pyproject.toml b/pyproject.toml index 7c12fbc9e..a3e1ddc89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ sphinx = "5.3.0" sphinx_copybutton = "0.5.2" sphinx_design = "0.4.1" sphinx-rtd-theme = "1.2.0" +sphinxemoji = "0.2.0" [build-system] requires = ["poetry-core>=1.2.0"]