From 2b8fb9c1c9962359ca0b069b89713b07aee24b32 Mon Sep 17 00:00:00 2001 From: Isaac Jessup Date: Fri, 2 Jun 2023 10:42:39 -0400 Subject: [PATCH 1/6] httpx_auth integration Using httpx_auth in the glue files so that the httpx authentication parameter is used as much as possible. The changes were designed to handle combination authentication methods. It brings support for OAuth2, AWSSigV4, and more. Any authentication method in the httpx_auth library can be used directly and dynamically. In order to ensure Security Schemes Objects continue to follow the OpenAPI specifications, the type must be set to "http" and the scheme can be set to any of the httpx_auth classes that extend the httpx.Auth class. Example Spec: spec = {...} spec["components"]["securitySchemes"]["sigv4"] = {"type": "http", "scheme": "aws4auth"} api = OpenAPI("fict.iv", document=spec) api.authenticate( apiKey="asdf1234", sigv4={ "access_id": "my-access-id", "secret_key": "my-secret-key", "service": "execute-api", "region": "us-east-1", }, ) --- aiopenapi3/v20/glue.py | 8 ++-- aiopenapi3/v30/glue.py | 83 +++++++++++++++++++++++++++++++++--------- requirements.txt | 5 ++- 3 files changed, 73 insertions(+), 23 deletions(-) diff --git a/aiopenapi3/v20/glue.py b/aiopenapi3/v20/glue.py index 9e6c3e71..a8e0f066 100644 --- a/aiopenapi3/v20/glue.py +++ b/aiopenapi3/v20/glue.py @@ -3,6 +3,8 @@ import urllib.parse import httpx +import httpx_auth +import inspect import pydantic from ..base import SchemaBase, ParameterBase @@ -83,17 +85,17 @@ def _prepare_secschemes(self, scheme: str, value: Union[str, List[str]]): if ss.type == "basic": value = cast(List[str], value) - self.req.auth = httpx.BasicAuth(*value) + self.req.auth = httpx_auth.Basic(*value) value = cast(str, value) if ss.type == "apiKey": if ss.in_ == "query": # apiKey in query parameter - self.req.params[ss.name] = value + httpx_auth.QueryApiKey(value, getattr(ss, "name", None)) if ss.in_ == "header": # apiKey in query header data - self.req.headers[ss.name] = value + httpx_auth.HeaderApiKey(value, getattr(ss, "name", None)) def _prepare_parameters(self, provided): provided = provided or dict() diff --git a/aiopenapi3/v30/glue.py b/aiopenapi3/v30/glue.py index b5a0e57a..2442eafc 100644 --- a/aiopenapi3/v30/glue.py +++ b/aiopenapi3/v30/glue.py @@ -3,6 +3,8 @@ import urllib.parse import httpx +import httpx_auth +import inspect import pydantic import pydantic.json @@ -77,33 +79,78 @@ def _prepare_security(self): def _prepare_secschemes(self, scheme: str, value: Union[str, List[str]]): ss = self.root.components.securitySchemes[scheme] - - if ss.type == "http" and ss.scheme_ == "basic": - self.req.auth = httpx.BasicAuth(*value) - - if ss.type == "http" and ss.scheme_ == "digest": - self.req.auth = httpx.DigestAuth(*value) - + auth_methods = { + name.lower(): getattr(httpx_auth, name) + for name in httpx_auth.__all__ + if inspect.isclass((class_ := getattr(httpx_auth, name))) + if issubclass(class_, httpx.Auth) + } + add_auths = [] + + if ss.type == "oauth2": + # NOTE: refresh_url is not currently supported by httpx_auth + # REF: https://github.com/Colin-b/httpx_auth/issues/17 + if flow := getattr(ss.flows, "implicit", None): + add_auths.append(httpx_auth.OAuth2Implicit( + **value, + authorization_url=flow.authorizationUrl, + scopes=flow.scopes, + # refresh_url=getattr(flow, "refreshUrl", None), + )) + if flow := getattr(ss.flows, "password", None): + add_auths.append(httpx_auth.OAuth2ResourceOwnerPasswordCredentials( + **value, + token_url=flow.tokenUrl, + scopes=flow.scopes, + # refresh_url=getattr(flow, "refreshUrl", None), + )) + if flow := getattr(ss.flows, "clientCredentials", None): + add_auths.append(httpx_auth.OAuth2ClientCredentials( + **value, + token_url=flow.tokenUrl, + scopes=flow.scopes, + # refresh_url=getattr(flow, "refreshUrl", None), + )) + if flow := getattr(ss.flows, "authorizationCode", None): + add_auths.append(httpx_auth.OAuth2AuthorizationCode( + **value, + authorization_url=flow.authorizationUrl, + token_url=flow.tokenUrl, + scopes=flow.scopes, + # refresh_url=getattr(flow, "refreshUrl", None), + )) + + if ss.type == "http": + if auth := auth_methods.get(ss.scheme_, None): + if isinstance(value, tuple): + add_auths.append(auth(*value)) + if isinstance(value, dict): + add_auths.append(auth(**value)) + if ss.scheme_ == "bearer": + add_auths.append(auth_methods["headerapikey"]( + f"{ss.bearerFormat or 'Bearer'} {value}", + "Authorization" + )) + value = cast(str, value) - if ss.type == "http" and ss.scheme_ == "bearer": - header = ss.bearerFormat or "Bearer {}" - self.req.headers["Authorization"] = header.format(value) - + if ss.type == "mutualTLS": # TLS Client certificates (mutualTLS) self.req.cert = value if ss.type == "apiKey": - if ss.in_ == "query": - # apiKey in query parameter - self.req.params[ss.name] = value - - if ss.in_ == "header": - # apiKey in query header data - self.req.headers[ss.name] = value + if auth := auth_methods.get((ss.in_+ss.type).lower(), None): + add_auths.append(auth(value, getattr(ss, "name", None))) if ss.in_ == "cookie": self.req.cookies = {ss.name: value} + + for auth in add_auths: + if self.req.auth: + self.req.auth += auth + else: + self.req.auth = auth + def _prepare_parameters(self, provided): """ diff --git a/requirements.txt b/requirements.txt index 25c02e23..d0c2b46c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ fastapi~=0.95.0 -httpx~=0.23.3 +httpx~=0.24.0 +httpx-auth~=0.17.0 hypercorn~=0.14.3 pydantic~=1.10.7 pydantic[email] pytest~=7.2.2 PyYAML~=6.0 -uvloop~=0.17.0 +uvloop~=0.17.0; sys_platform != 'win32' yarl~=1.8.2 From 1f755a67f28513e1261a0120cb69f8a98f50b5b0 Mon Sep 17 00:00:00 2001 From: Isaac Jessup Date: Fri, 2 Jun 2023 11:04:51 -0400 Subject: [PATCH 2/6] minor cleanup --- aiopenapi3/v20/glue.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/aiopenapi3/v20/glue.py b/aiopenapi3/v20/glue.py index a8e0f066..a2cddd15 100644 --- a/aiopenapi3/v20/glue.py +++ b/aiopenapi3/v20/glue.py @@ -1,10 +1,7 @@ from typing import List, Union, cast import json -import urllib.parse -import httpx import httpx_auth -import inspect import pydantic from ..base import SchemaBase, ParameterBase From d5bd50c99070a93ab5549522803b4df3be57accb Mon Sep 17 00:00:00 2001 From: Isaac Jessup Date: Fri, 2 Jun 2023 11:05:35 -0400 Subject: [PATCH 3/6] httpx-auth Added httpx-auth to the install requirements --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 1485d0ff..5d1e34dd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,6 +44,7 @@ install_requires = pydantic[email] yarl httpx + httpx-auth more-itertools typing_extensions; python_version<"3.8" pathlib3x; python_version<"3.9" From ec4ba6a195fde6e1852511924393238415b89afc Mon Sep 17 00:00:00 2001 From: Isaac Jessup Date: Fri, 2 Jun 2023 13:03:51 -0400 Subject: [PATCH 4/6] missing self.req.auth setter --- aiopenapi3/v20/glue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiopenapi3/v20/glue.py b/aiopenapi3/v20/glue.py index a2cddd15..bc54e033 100644 --- a/aiopenapi3/v20/glue.py +++ b/aiopenapi3/v20/glue.py @@ -88,11 +88,11 @@ def _prepare_secschemes(self, scheme: str, value: Union[str, List[str]]): if ss.type == "apiKey": if ss.in_ == "query": # apiKey in query parameter - httpx_auth.QueryApiKey(value, getattr(ss, "name", None)) + self.req.auth = httpx_auth.QueryApiKey(value, getattr(ss, "name", None)) if ss.in_ == "header": # apiKey in query header data - httpx_auth.HeaderApiKey(value, getattr(ss, "name", None)) + self.req.auth = httpx_auth.HeaderApiKey(value, getattr(ss, "name", None)) def _prepare_parameters(self, provided): provided = provided or dict() From 5e720b7731856ab8b6b132ba03df5a86a0c678c5 Mon Sep 17 00:00:00 2001 From: Isaac Jessup Date: Fri, 2 Jun 2023 14:27:04 -0400 Subject: [PATCH 5/6] Ensure SupportMultiAuth --- aiopenapi3/v30/glue.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiopenapi3/v30/glue.py b/aiopenapi3/v30/glue.py index 2442eafc..9e5ac5bf 100644 --- a/aiopenapi3/v30/glue.py +++ b/aiopenapi3/v30/glue.py @@ -4,6 +4,7 @@ import httpx import httpx_auth +from httpx_auth.authentication import SupportMultiAuth import inspect import pydantic import pydantic.json @@ -146,7 +147,7 @@ def _prepare_secschemes(self, scheme: str, value: Union[str, List[str]]): self.req.cookies = {ss.name: value} for auth in add_auths: - if self.req.auth: + if self.req.auth and isinstance(self.req.auth, SupportMultiAuth): self.req.auth += auth else: self.req.auth = auth From 830f04cf7a3d7e1b0259b63cd4e6b00e42f1c8e6 Mon Sep 17 00:00:00 2001 From: Isaac Jessup Date: Fri, 2 Jun 2023 14:40:59 -0400 Subject: [PATCH 6/6] conditional httpx_auth Only uses httpx_auth if it's been installed separately. --- aiopenapi3/v20/glue.py | 19 ++++- aiopenapi3/v30/glue.py | 177 ++++++++++++++++++++++++----------------- requirements.txt | 1 - setup.cfg | 1 - 4 files changed, 119 insertions(+), 79 deletions(-) diff --git a/aiopenapi3/v20/glue.py b/aiopenapi3/v20/glue.py index bc54e033..d024ed19 100644 --- a/aiopenapi3/v20/glue.py +++ b/aiopenapi3/v20/glue.py @@ -1,7 +1,7 @@ from typing import List, Union, cast import json -import httpx_auth +import httpx import pydantic from ..base import SchemaBase, ParameterBase @@ -10,6 +10,11 @@ from .parameter import Parameter +try: + import httpx_auth +except: + httpx_auth = None + class Request(RequestBase): @property @@ -82,17 +87,23 @@ def _prepare_secschemes(self, scheme: str, value: Union[str, List[str]]): if ss.type == "basic": value = cast(List[str], value) - self.req.auth = httpx_auth.Basic(*value) + self.req.auth = httpx_auth.Basic(*value) if httpx_auth else httpx.BasicAuth(*value) value = cast(str, value) if ss.type == "apiKey": if ss.in_ == "query": # apiKey in query parameter - self.req.auth = httpx_auth.QueryApiKey(value, getattr(ss, "name", None)) + if httpx_auth: + self.req.auth = httpx_auth.QueryApiKey(value, getattr(ss, "name", None)) + else: + self.req.params[ss.name] = value if ss.in_ == "header": # apiKey in query header data - self.req.auth = httpx_auth.HeaderApiKey(value, getattr(ss, "name", None)) + if httpx_auth: + self.req.auth = httpx_auth.HeaderApiKey(value, getattr(ss, "name", None)) + else: + self.req.headers[ss.name] = value def _prepare_parameters(self, provided): provided = provided or dict() diff --git a/aiopenapi3/v30/glue.py b/aiopenapi3/v30/glue.py index 9e5ac5bf..f337111d 100644 --- a/aiopenapi3/v30/glue.py +++ b/aiopenapi3/v30/glue.py @@ -3,8 +3,11 @@ import urllib.parse import httpx -import httpx_auth -from httpx_auth.authentication import SupportMultiAuth +try: + import httpx_auth + from httpx_auth.authentication import SupportMultiAuth +except: + httpx_auth = None import inspect import pydantic import pydantic.json @@ -80,77 +83,105 @@ def _prepare_security(self): def _prepare_secschemes(self, scheme: str, value: Union[str, List[str]]): ss = self.root.components.securitySchemes[scheme] - auth_methods = { - name.lower(): getattr(httpx_auth, name) - for name in httpx_auth.__all__ - if inspect.isclass((class_ := getattr(httpx_auth, name))) - if issubclass(class_, httpx.Auth) - } - add_auths = [] - - if ss.type == "oauth2": - # NOTE: refresh_url is not currently supported by httpx_auth - # REF: https://github.com/Colin-b/httpx_auth/issues/17 - if flow := getattr(ss.flows, "implicit", None): - add_auths.append(httpx_auth.OAuth2Implicit( - **value, - authorization_url=flow.authorizationUrl, - scopes=flow.scopes, - # refresh_url=getattr(flow, "refreshUrl", None), - )) - if flow := getattr(ss.flows, "password", None): - add_auths.append(httpx_auth.OAuth2ResourceOwnerPasswordCredentials( - **value, - token_url=flow.tokenUrl, - scopes=flow.scopes, - # refresh_url=getattr(flow, "refreshUrl", None), - )) - if flow := getattr(ss.flows, "clientCredentials", None): - add_auths.append(httpx_auth.OAuth2ClientCredentials( - **value, - token_url=flow.tokenUrl, - scopes=flow.scopes, - # refresh_url=getattr(flow, "refreshUrl", None), - )) - if flow := getattr(ss.flows, "authorizationCode", None): - add_auths.append(httpx_auth.OAuth2AuthorizationCode( - **value, - authorization_url=flow.authorizationUrl, - token_url=flow.tokenUrl, - scopes=flow.scopes, - # refresh_url=getattr(flow, "refreshUrl", None), - )) - - if ss.type == "http": - if auth := auth_methods.get(ss.scheme_, None): - if isinstance(value, tuple): - add_auths.append(auth(*value)) - if isinstance(value, dict): - add_auths.append(auth(**value)) - if ss.scheme_ == "bearer": - add_auths.append(auth_methods["headerapikey"]( - f"{ss.bearerFormat or 'Bearer'} {value}", - "Authorization" - )) - - value = cast(str, value) - - if ss.type == "mutualTLS": - # TLS Client certificates (mutualTLS) - self.req.cert = value - - if ss.type == "apiKey": - if auth := auth_methods.get((ss.in_+ss.type).lower(), None): - add_auths.append(auth(value, getattr(ss, "name", None))) - - if ss.in_ == "cookie": - self.req.cookies = {ss.name: value} - - for auth in add_auths: - if self.req.auth and isinstance(self.req.auth, SupportMultiAuth): - self.req.auth += auth - else: - self.req.auth = auth + if httpx_auth: + auth_methods = { + name.lower(): getattr(httpx_auth, name) + for name in httpx_auth.__all__ + if inspect.isclass((class_ := getattr(httpx_auth, name))) + if issubclass(class_, httpx.Auth) + } + add_auths = [] + + if ss.type == "oauth2": + # NOTE: refresh_url is not currently supported by httpx_auth + # REF: https://github.com/Colin-b/httpx_auth/issues/17 + if flow := getattr(ss.flows, "implicit", None): + add_auths.append(httpx_auth.OAuth2Implicit( + **value, + authorization_url=flow.authorizationUrl, + scopes=flow.scopes, + # refresh_url=getattr(flow, "refreshUrl", None), + )) + if flow := getattr(ss.flows, "password", None): + add_auths.append(httpx_auth.OAuth2ResourceOwnerPasswordCredentials( + **value, + token_url=flow.tokenUrl, + scopes=flow.scopes, + # refresh_url=getattr(flow, "refreshUrl", None), + )) + if flow := getattr(ss.flows, "clientCredentials", None): + add_auths.append(httpx_auth.OAuth2ClientCredentials( + **value, + token_url=flow.tokenUrl, + scopes=flow.scopes, + # refresh_url=getattr(flow, "refreshUrl", None), + )) + if flow := getattr(ss.flows, "authorizationCode", None): + add_auths.append(httpx_auth.OAuth2AuthorizationCode( + **value, + authorization_url=flow.authorizationUrl, + token_url=flow.tokenUrl, + scopes=flow.scopes, + # refresh_url=getattr(flow, "refreshUrl", None), + )) + + if ss.type == "http": + if auth := auth_methods.get(ss.scheme_, None): + if isinstance(value, tuple): + add_auths.append(auth(*value)) + if isinstance(value, dict): + add_auths.append(auth(**value)) + if ss.scheme_ == "bearer": + add_auths.append(auth_methods["headerapikey"]( + f"{ss.bearerFormat or 'Bearer'} {value}", + "Authorization" + )) + + value = cast(str, value) + + if ss.type == "mutualTLS": + # TLS Client certificates (mutualTLS) + self.req.cert = value + + if ss.type == "apiKey": + if auth := auth_methods.get((ss.in_+ss.type).lower(), None): + add_auths.append(auth(value, getattr(ss, "name", None))) + + if ss.in_ == "cookie": + self.req.cookies = {ss.name: value} + + for auth in add_auths: + if self.req.auth and isinstance(self.req.auth, SupportMultiAuth): + self.req.auth += auth + else: + self.req.auth = auth + else: + if ss.type == "http" and ss.scheme_ == "basic": + self.req.auth = httpx.BasicAuth(*value) + + if ss.type == "http" and ss.scheme_ == "digest": + self.req.auth = httpx.DigestAuth(*value) + + value = cast(str, value) + if ss.type == "http" and ss.scheme_ == "bearer": + header = ss.bearerFormat or "Bearer {}" + self.req.headers["Authorization"] = header.format(value) + + if ss.type == "mutualTLS": + # TLS Client certificates (mutualTLS) + self.req.cert = value + + if ss.type == "apiKey": + if ss.in_ == "query": + # apiKey in query parameter + self.req.params[ss.name] = value + + if ss.in_ == "header": + # apiKey in query header data + self.req.headers[ss.name] = value + + if ss.in_ == "cookie": + self.req.cookies = {ss.name: value} def _prepare_parameters(self, provided): diff --git a/requirements.txt b/requirements.txt index d0c2b46c..af7437ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ fastapi~=0.95.0 httpx~=0.24.0 -httpx-auth~=0.17.0 hypercorn~=0.14.3 pydantic~=1.10.7 pydantic[email] diff --git a/setup.cfg b/setup.cfg index 5d1e34dd..1485d0ff 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,6 @@ install_requires = pydantic[email] yarl httpx - httpx-auth more-itertools typing_extensions; python_version<"3.8" pathlib3x; python_version<"3.9"