From 1d3bfb14a101c5a6c6b03d6209acb517f7f6d2b1 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Fri, 12 Jan 2024 10:50:30 +0000 Subject: [PATCH] Optionally use x-forwarded headers OIDC authentication redirect - Add OIDCConfig option to allow use of x-forwarded-proto, x-forwarded-host headers when constructing code redirect - Prevents issue when SSL terminated and request forwarded over http, but OIDC client-id is registered with patch matching regex that requires http --- CHANGELOG.md | 3 +++ fastapi_opa/auth/auth_oidc.py | 34 ++++++++++++++++++++++++++++++++-- pyproject.toml | 2 +- tests/test_oidc_auth.py | 22 ++++++++++++++++++++++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e3a533..1e44c18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Change Log +## [1.4.8] - 2024-01-12 +- Optionally use `x-forwarded-` cookies when reconstructing redirect path for OIDC + ## [1.4.7] - 2023-10-12 - Add option to define package name parameter in OPA Config diff --git a/fastapi_opa/auth/auth_oidc.py b/fastapi_opa/auth/auth_oidc.py index 8d6b5ea..4e7518d 100644 --- a/fastapi_opa/auth/auth_oidc.py +++ b/fastapi_opa/auth/auth_oidc.py @@ -26,10 +26,36 @@ @dataclass class OIDCConfig: + """ + Configuration for the OIDC flow. + + PARAMETERS + ---------- + app_uri: str + Unused + client_id: str + The OIDC client id of the service, to be passed with the + redirect to the OIDC provider + client_secret: str + The OIDC client secret, to be passed with the access_token + request from the middleware to the OIDC provider + scope: str, default="openid email profile" + Space seperated list of scopes to request from the OIDC provider + trust_x_headers: bool, default=False + Whether to trust incoming `x-forwarded-` headers when constructing + the redirect to pass to the OIDC provider. + The constructed redirect may have to match with a matcher regex + configured with the OIDC provider for the client-id. + However with a wildcard client-id this may open pathways for + malicious injection of headers as part of a cross-site attack, + and so defaults to false. + """ + app_uri: str client_id: str client_secret: str scope: str = field(default="openid email profile") + trust_x_headers: bool = field(default=False) # provide either well_known or all the other values well_known_endpoint: str = field(default="") @@ -81,8 +107,12 @@ async def authenticate( ) -> Union[RedirectResponse, Dict]: callback_uri = urlunparse( [ - request.url.scheme, - request.url.netloc, + request.headers.get("x-forwarded-proto", request.url.scheme) + if self.config.trust_x_headers + else request.url.scheme, + request.headers.get("x-forwarded-host", request.url.netloc) + if self.config.trust_x_headers + else request.url.netloc, request.url.path, "", "", diff --git a/pyproject.toml b/pyproject.toml index 197ec78..54535d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "fastapi-opa" -version = "1.4.7" +version = "1.4.8" description = "Fastapi OPA middleware incl. auth flow." authors = ["Matthias Osswald "] license = "GPL-3.0-or-later" diff --git a/tests/test_oidc_auth.py b/tests/test_oidc_auth.py index d88222b..ccc4fb4 100644 --- a/tests/test_oidc_auth.py +++ b/tests/test_oidc_auth.py @@ -10,6 +10,9 @@ from cryptography.hazmat.primitives._serialization import PublicFormat from freezegun import freeze_time from mock import Mock +from starlette.datastructures import URL +from starlette.datastructures import Headers +from starlette.requests import Request from fastapi_opa.auth.auth_oidc import OIDCAuthentication from fastapi_opa.auth.exceptions import OIDCException @@ -32,6 +35,25 @@ def test_auth_redirect_uri(mocker): assert expected_url == response +@pytest.mark.asyncio +async def test_auth_redirect_uri_from_headers(mocker): + call_uri = "http://fastapi-app.busykoala.ch/test/path" + headers = {"x-forwarded-proto": "https", "x-forwarded-host": "foo.bar.ch"} + with mocker.patch( + "fastapi_opa.auth.auth_oidc.requests.get", + return_value=oidc_well_known_response(), + ): + config = oidc_config() + config.trust_x_headers = True + oidc = OIDCAuthentication(config) + request: Request = Request({"type": "http", "query_string": ""}) + request._headers = Headers(headers) + request._url = URL(call_uri) + response = await oidc.authenticate(request) + expected_url = "http://keycloak.busykoala.ch/auth/realms/example-realm/protocol/openid-connect/auth?response_type=code&scope=openid%20email%20profile&client_id=example-client&redirect_uri=https%3A//foo.bar.ch/test/path" # noqa + assert expected_url == response.headers["location"] + + def test_get_auth_token(mocker): with mocker.patch( "fastapi_opa.auth.auth_oidc.requests.get",