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

Update security docs #1764

Merged
merged 2 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
258 changes: 171 additions & 87 deletions docs/security.rst
Original file line number Diff line number Diff line change
@@ -1,87 +1,146 @@
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
Ruwann marked this conversation as resolved.
Show resolved Hide resolved
---------------------------

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These don't get rendered properly:
image

I believe an extra empty line is needed before the first list item

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

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.

You can find a `minimal JWT example application`_ in Connexion's "examples" folder.
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 +155,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)
Ruwann marked this conversation as resolved.
Show resolved Hide resolved


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