Skip to content

Commit

Permalink
Update security docs (#1764)
Browse files Browse the repository at this point in the history
Contributes to #1531
  • Loading branch information
RobbeSneyders committed Oct 31, 2023
1 parent 17aa31a commit cbeac6f
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 95 deletions.
5 changes: 5 additions & 0 deletions connexion/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
'sphinx_copybutton',
'sphinx_design',
'sphinx.ext.autosectionlabel',
'sphinxemoji.sphinxemoji',
]
autosectionlabel_prefix_document = True

Expand Down
3 changes: 2 additions & 1 deletion docs/routing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <request:Automatic parameter handling>` to your
python function.

Individual paths
----------------
Expand Down
264 changes: 177 additions & 87 deletions docs/security.rst
Original file line number Diff line number Diff line change
@@ -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 <Routing:Explicit routing>`.

The recommended approach is to return a dict which complies with
`RFC 7662 <rfc7662_>`_. 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 <rfc7662_>`_:

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 <rfc6749_>`_.
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 <rfc6750_>`_ 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 <context:context.context>`, which you can also have passed in as an
:ref:`argument <request:Context>`.

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 <rfc6750_>`_ 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
-------------------------------
Expand All @@ -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.
6 changes: 3 additions & 3 deletions examples/oauth2/mock_tokeninfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__":
Expand Down
8 changes: 4 additions & 4 deletions examples/oauth2_local_tokeninfo/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down

0 comments on commit cbeac6f

Please sign in to comment.