From 26422dc5f658be3e6074a2ee391f22a374db717a Mon Sep 17 00:00:00 2001 From: niebl Date: Thu, 5 Feb 2026 17:48:04 +0100 Subject: [PATCH 01/40] add jwt conformance to DummyBackend --- openeo/rest/_testing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openeo/rest/_testing.py b/openeo/rest/_testing.py index 998874551..fe8fbb89b 100644 --- a/openeo/rest/_testing.py +++ b/openeo/rest/_testing.py @@ -474,6 +474,8 @@ def build_capabilities( ] ) + conformance = ["https://api.openeo.org/1.3.0/authentication/jwt"] + capabilities = { "api_version": api_version, "stac_version": stac_version, @@ -481,6 +483,7 @@ def build_capabilities( "title": "Dummy openEO back-end", "description": "Dummy openeEO back-end", "endpoints": endpoints, + "conformsTo": conformance, "links": [], } return capabilities From cde00d63134a98714ec46b4c3c78e9fab3bbebe0 Mon Sep 17 00:00:00 2001 From: niebl Date: Fri, 6 Feb 2026 11:49:16 +0100 Subject: [PATCH 02/40] add further conformance --- openeo/rest/_testing.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openeo/rest/_testing.py b/openeo/rest/_testing.py index fe8fbb89b..83a1ae9eb 100644 --- a/openeo/rest/_testing.py +++ b/openeo/rest/_testing.py @@ -470,11 +470,17 @@ def build_capabilities( endpoints.extend( [ {"path": "/process_graphs", "methods": ["GET"]}, - {"path": "/process_graphs/{process_graph_id", "methods": ["GET", "PUT", "DELETE"]}, + {"path": "/process_graphs/{process_graph_id}", "methods": ["GET", "PUT", "DELETE"]}, ] ) - conformance = ["https://api.openeo.org/1.3.0/authentication/jwt"] + conformance = [ + "https://api.openeo.org/{api_version}", + "https://api.stacspec.org/v{stac_version}/core", + "https://api.stacspec.org/v{stac_version}/collections" + ] + if api_version == "1.3.0": #might need a way to compare version numbers via greater than + conformance.append("https://api.openeo.org/1.3.0/authentication/jwt") capabilities = { "api_version": api_version, From b0a66a510694f01254810b8d89ef7fcf7927d9b6 Mon Sep 17 00:00:00 2001 From: niebl Date: Fri, 6 Feb 2026 12:50:07 +0100 Subject: [PATCH 03/40] add conformance checking to basic auth --- openeo/rest/_testing.py | 35 ++++++++++++++++++++++++++++------- openeo/rest/auth/auth.py | 16 ++++++++++++---- openeo/rest/capabilities.py | 10 ++++++++++ openeo/rest/connection.py | 6 +++++- 4 files changed, 55 insertions(+), 12 deletions(-) diff --git a/openeo/rest/_testing.py b/openeo/rest/_testing.py index 83a1ae9eb..bc920ab5e 100644 --- a/openeo/rest/_testing.py +++ b/openeo/rest/_testing.py @@ -189,6 +189,14 @@ def setup_file_format(self, name: str, type: str = "output", gis_data_types: Ite } self._requests_mock.get(self.connection.build_url("/file_formats"), json=self.file_formats) return self + + def _get_conformance(self, request, context): + return { + "conformsTo": build_conformance( + api_version="1.3.0", + stac_version="1.0.0" + ) + } def _handle_post_result(self, request, context): """handler of `POST /result` (synchronous execute)""" @@ -424,6 +432,20 @@ def get_status(job_id: str, current_status: str) -> str: self.job_status_updater = get_status +def build_conformance( + *, + api_version: str = "1.0.0", + stac_version: str = "0.9.0", +) -> list[str]: + conformance = [ + "https://api.openeo.org/{api_version}", + "https://api.stacspec.org/v{stac_version}/core", + "https://api.stacspec.org/v{stac_version}/collections" + ] + if api_version == "1.3.0": #TODO: use ComparableVersion + conformance.append("https://api.openeo.org/1.3.0/authentication/jwt") + return conformance + def build_capabilities( *, @@ -441,6 +463,8 @@ def build_capabilities( """Build a dummy capabilities document for testing purposes.""" endpoints = [] + if basic_auth: + endpoints.append({"path": "/conformance", "methods": ["GET"]}) if basic_auth: endpoints.append({"path": "/credentials/basic", "methods": ["GET"]}) if oidc_auth: @@ -474,13 +498,10 @@ def build_capabilities( ] ) - conformance = [ - "https://api.openeo.org/{api_version}", - "https://api.stacspec.org/v{stac_version}/core", - "https://api.stacspec.org/v{stac_version}/collections" - ] - if api_version == "1.3.0": #might need a way to compare version numbers via greater than - conformance.append("https://api.openeo.org/1.3.0/authentication/jwt") + conformance = build_conformance( + api_version=api_version, + stac_version=stac_version + ) capabilities = { "api_version": api_version, diff --git a/openeo/rest/auth/auth.py b/openeo/rest/auth/auth.py index 378fbdbc2..9d47edb7b 100644 --- a/openeo/rest/auth/auth.py +++ b/openeo/rest/auth/auth.py @@ -41,12 +41,20 @@ def __call__(self, req: Request) -> Request: class BasicBearerAuth(BearerAuth): """Bearer token for Basic Auth (openEO API 1.0.0 style)""" - def __init__(self, access_token: str): - super().__init__(bearer="basic//{t}".format(t=access_token)) + def __init__(self, access_token: str, jwt_conformance: bool = False): + if jwt_conformance: + bearer="{t}" + else: + bearer = "basic//{t}".format(t=access_token) + super().__init__(bearer=bearer) class OidcBearerAuth(BearerAuth): """Bearer token for OIDC Auth (openEO API 1.0.0 style)""" - def __init__(self, provider_id: str, access_token: str): - super().__init__(bearer="oidc/{p}/{t}".format(p=provider_id, t=access_token)) + def __init__(self, provider_id: str, access_token: str, jwt_conformance: bool = False): + if jwt_conformance: + bearer="{t}" + else: + bearer="oidc/{p}/{t}".format(p=provider_id, t=access_token) + super().__init__(bearer=bearer) diff --git a/openeo/rest/capabilities.py b/openeo/rest/capabilities.py index 768093f6f..cb672d79e 100644 --- a/openeo/rest/capabilities.py +++ b/openeo/rest/capabilities.py @@ -1,4 +1,5 @@ from typing import Dict, List, Optional, Union +from fnmatch import fnmatch from openeo.internal.jupyter import render_component from openeo.rest.models import federation_extension @@ -36,6 +37,15 @@ def api_version_check(self) -> ComparableVersion: if not api_version: raise ApiVersionException("No API version found") return ComparableVersion(api_version) + + def has_conformance(self, conformance: str) -> bool: + """Check if backend provides a given conformance string""" + if "conformsTo" in self.capabilities: + for url in conformsTo: + if fnmatch(url, conformance): + return True + return False + def supports_endpoint(self, path: str, method="GET") -> bool: """Check if backend supports given endpoint""" diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index d4d4d5995..b09a52196 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -277,8 +277,12 @@ def authenticate_basic(self, username: Optional[str] = None, password: Optional[ # /credentials/basic is the only endpoint that expects a Basic HTTP auth auth=HTTPBasicAuth(username, password) ).json() + + # check for JWT bearer token conformance + jwt_conformance = self.capabilities().has_conformance("https://api.openeo.org/*/authentication/jwt") + # Switch to bearer based authentication in further requests. - self.auth = BasicBearerAuth(access_token=resp["access_token"]) + self.auth = BasicBearerAuth(access_token=resp["access_token"], jwt_conformance = jwt_conformance) return self def _get_oidc_provider( From d4d5dad8334327fb60d60382ed2ccfd7156ad234 Mon Sep 17 00:00:00 2001 From: niebl Date: Fri, 6 Feb 2026 15:54:30 +0100 Subject: [PATCH 04/40] fix has_conformance --- openeo/rest/capabilities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openeo/rest/capabilities.py b/openeo/rest/capabilities.py index cb672d79e..bf59649de 100644 --- a/openeo/rest/capabilities.py +++ b/openeo/rest/capabilities.py @@ -41,7 +41,7 @@ def api_version_check(self) -> ComparableVersion: def has_conformance(self, conformance: str) -> bool: """Check if backend provides a given conformance string""" if "conformsTo" in self.capabilities: - for url in conformsTo: + for url in self.capabilities["conformsTo"]: if fnmatch(url, conformance): return True return False From 1e75abe05baec31e9eb8d3b9fd7c61930375dfff Mon Sep 17 00:00:00 2001 From: niebl Date: Fri, 6 Feb 2026 16:53:33 +0100 Subject: [PATCH 05/40] add jwt conformant bearer token support to oidc auth --- openeo/rest/connection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index b09a52196..199ca9735 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -420,7 +420,9 @@ def _authenticate_oidc( ) token = tokens.access_token - self.auth = OidcBearerAuth(provider_id=provider_id, access_token=token) + # check for JWT bearer token conformance + jwt_conformance = self.capabilities().has_conformance("https://api.openeo.org/*/authentication/jwt") + self.auth = OidcBearerAuth(provider_id=provider_id, access_token=token, jwt_conformance=jwt_conformance) self._oidc_auth_renewer = oidc_auth_renewer return self From 6566959ccc681ccacc35affe8d4076e9637813f2 Mon Sep 17 00:00:00 2001 From: niebl Date: Mon, 9 Feb 2026 11:33:23 +0100 Subject: [PATCH 06/40] Add tests for jwt conformance --- tests/rest/conftest.py | 6 ++++ tests/rest/test_testing.py | 69 ++++++++++++++++++++++++-------------- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/tests/rest/conftest.py b/tests/rest/conftest.py index 2255cca85..411c82e1e 100644 --- a/tests/rest/conftest.py +++ b/tests/rest/conftest.py @@ -99,6 +99,12 @@ def con120(requests_mock, api_capabilities): con = Connection(API_URL) return con +@pytest.fixture +def con130(requests_mock, api_capabilities): + requests_mock.get(API_URL, json=build_capabilities(api_version="1.3.0", **api_capabilities)) + con = Connection(API_URL) + return con + @pytest.fixture def dummy_backend(requests_mock, con120) -> DummyBackend: diff --git a/tests/rest/test_testing.py b/tests/rest/test_testing.py index 589dda3dc..7b11ecd22 100644 --- a/tests/rest/test_testing.py +++ b/tests/rest/test_testing.py @@ -7,9 +7,12 @@ @pytest.fixture -def dummy_backend(requests_mock, con120): +def dummy_backend120(requests_mock, con120): return DummyBackend(requests_mock=requests_mock, connection=con120) +@pytest.fixture +def dummy_backend130(requests_mock, con130): + return DummyBackend(requests_mock=requests_mock, connection=con130) DUMMY_PG_ADD35 = { "add35": {"process_id": "add", "arguments": {"x": 3, "y": 5}, "result": True}, @@ -17,10 +20,10 @@ def dummy_backend(requests_mock, con120): class TestDummyBackend: - def test_create_job(self, dummy_backend, con120): - assert dummy_backend.batch_jobs == {} + def test_create_job(self, dummy_backend120, con120): + assert dummy_backend120.batch_jobs == {} _ = con120.create_job(DUMMY_PG_ADD35) - assert dummy_backend.batch_jobs == { + assert dummy_backend120.batch_jobs == { "job-000": { "job_id": "job-000", "pg": {"add35": {"process_id": "add", "arguments": {"x": 3, "y": 5}, "result": True}}, @@ -28,33 +31,33 @@ def test_create_job(self, dummy_backend, con120): } } - def test_start_job(self, dummy_backend, con120): + def test_start_job(self, dummy_backend120, con120): job = con120.create_job(DUMMY_PG_ADD35) - assert dummy_backend.batch_jobs == { + assert dummy_backend120.batch_jobs == { "job-000": {"job_id": "job-000", "pg": DUMMY_PG_ADD35, "status": "created"}, } job.start() - assert dummy_backend.batch_jobs == { + assert dummy_backend120.batch_jobs == { "job-000": {"job_id": "job-000", "pg": DUMMY_PG_ADD35, "status": "finished"}, } - def test_job_status_updater_error(self, dummy_backend, con120): - dummy_backend.job_status_updater = lambda job_id, current_status: "error" + def test_job_status_updater_error(self, dummy_backend120, con120): + dummy_backend120.job_status_updater = lambda job_id, current_status: "error" job = con120.create_job(DUMMY_PG_ADD35) - assert dummy_backend.batch_jobs["job-000"]["status"] == "created" + assert dummy_backend120.batch_jobs["job-000"]["status"] == "created" job.start() - assert dummy_backend.batch_jobs["job-000"]["status"] == "error" + assert dummy_backend120.batch_jobs["job-000"]["status"] == "error" @pytest.mark.parametrize("final", ["finished", "error"]) - def test_setup_simple_job_status_flow(self, dummy_backend, con120, final): - dummy_backend.setup_simple_job_status_flow(queued=2, running=3, final=final) + def test_setup_simple_job_status_flow(self, dummy_backend120, con120, final): + dummy_backend120.setup_simple_job_status_flow(queued=2, running=3, final=final) job = con120.create_job(DUMMY_PG_ADD35) - assert dummy_backend.batch_jobs["job-000"]["status"] == "created" + assert dummy_backend120.batch_jobs["job-000"]["status"] == "created" # Note that first status update (to "queued" here) is triggered from `start()`, not `status()` like below job.start() - assert dummy_backend.batch_jobs["job-000"]["status"] == "queued" + assert dummy_backend120.batch_jobs["job-000"]["status"] == "queued" # Now go through rest of status flow, through `status()` calls assert job.status() == "queued" @@ -66,25 +69,25 @@ def test_setup_simple_job_status_flow(self, dummy_backend, con120, final): assert job.status() == final assert job.status() == final - def test_setup_simple_job_status_flow_final_per_job(self, dummy_backend, con120): + def test_setup_simple_job_status_flow_final_per_job(self, dummy_backend120, con120): """Test per-job specific final status""" - dummy_backend.setup_simple_job_status_flow( + dummy_backend120.setup_simple_job_status_flow( queued=2, running=3, final="finished", final_per_job={"job-001": "error"} ) job0 = con120.create_job(DUMMY_PG_ADD35) job1 = con120.create_job(DUMMY_PG_ADD35) job2 = con120.create_job(DUMMY_PG_ADD35) - assert dummy_backend.batch_jobs["job-000"]["status"] == "created" - assert dummy_backend.batch_jobs["job-001"]["status"] == "created" - assert dummy_backend.batch_jobs["job-002"]["status"] == "created" + assert dummy_backend120.batch_jobs["job-000"]["status"] == "created" + assert dummy_backend120.batch_jobs["job-001"]["status"] == "created" + assert dummy_backend120.batch_jobs["job-002"]["status"] == "created" # Note that first status update (to "queued" here) is triggered from `start()`, not `status()` like below job0.start() job1.start() job2.start() - assert dummy_backend.batch_jobs["job-000"]["status"] == "queued" - assert dummy_backend.batch_jobs["job-001"]["status"] == "queued" - assert dummy_backend.batch_jobs["job-002"]["status"] == "queued" + assert dummy_backend120.batch_jobs["job-000"]["status"] == "queued" + assert dummy_backend120.batch_jobs["job-001"]["status"] == "queued" + assert dummy_backend120.batch_jobs["job-002"]["status"] == "queued" # Now go through rest of status flow, through `status()` calls for expected_status in ["queued", "running", "running", "running"]: @@ -98,9 +101,23 @@ def test_setup_simple_job_status_flow_final_per_job(self, dummy_backend, con120) assert job1.status() == "error" assert job2.status() == "finished" - def test_setup_job_start_failure(self, dummy_backend): - job = dummy_backend.connection.create_job(process_graph={}) - dummy_backend.setup_job_start_failure() + def test_setup_job_start_failure(self, dummy_backend120): + job = dummy_backend120.connection.create_job(process_graph={}) + dummy_backend120.setup_job_start_failure() with pytest.raises(OpenEoApiError, match=re.escape("[500] Internal: No job starting for you, buddy")): job.start() assert job.status() == "error" + + def test_version(self, dummy_backend120, dummy_backend130): + capabilities120 = dummy_backend120.connection.capabilities() + capabilities130 = dummy_backend130.connection.capabilities() + + assert capabilities120.api_version() == "1.2.0" + assert capabilities130.api_version() == "1.3.0" + + def test_jwt_conformance(self, dummy_backend120, dummy_backend130): + capabilities120 = dummy_backend120.connection.capabilities() + capabilities130 = dummy_backend130.connection.capabilities() + + assert capabilities120.has_conformance("https://api.openeo.org/*/authentication/jwt") == False + assert capabilities130.has_conformance("https://api.openeo.org/*/authentication/jwt") == True \ No newline at end of file From 208b72996648bb9e08b98400f5572dd46df3ad2f Mon Sep 17 00:00:00 2001 From: niebl Date: Mon, 9 Feb 2026 14:01:03 +0100 Subject: [PATCH 07/40] add tests for basic authentication --- tests/rest/test_connection.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index 6da731f71..b3b01a89d 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -59,7 +59,9 @@ API_URL = "https://oeo.test/" # TODO: eliminate this and replace with `build_capabilities` usage -BASIC_ENDPOINTS = [{"path": "/credentials/basic", "methods": ["GET"]}] +BASIC_ENDPOINTS = [ + {"path": "/credentials/basic", "methods": ["GET"]} + ] GEOJSON_POINT_01 = {"type": "Point", "coordinates": [3, 52]} @@ -848,7 +850,6 @@ def test_authenticate_basic(requests_mock, api_version, basic_auth): assert isinstance(conn.auth, BearerAuth) assert conn.auth.bearer == "basic//6cc3570k3n" - def test_authenticate_basic_from_config(requests_mock, api_version, auth_config, basic_auth): requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": BASIC_ENDPOINTS}) auth_config.set_basic_auth(backend=API_URL, username=basic_auth.username, password=basic_auth.password) @@ -859,6 +860,17 @@ def test_authenticate_basic_from_config(requests_mock, api_version, auth_config, assert isinstance(conn.auth, BearerAuth) assert conn.auth.bearer == "basic//6cc3570k3n" +def test_authenticate_basic_jwt_bearer(requests_mock, basic_auth): + requests_mock.get(API_URL, json={"api_version": "1.3.0", "endpoints": BASIC_ENDPOINTS}) + + conn = Connection(API_URL) + assert isinstance(conn.auth, NullAuth) + conn.authenticate_basic(username=basic_auth.username, password=basic_auth.password) + capabilities = conn.capabilities() + assert isinstance(conn.auth, BearerAuth) + assert capabilities.api_version() == "1.3.0" + assert capabilities.has_conformance("https://api.openeo.org/*/authentication/jwt") == "1.3.0" + assert conn.auth.bearer == "6cc3570k3n" @pytest.mark.slow def test_authenticate_oidc_authorization_code_100_single_implicit(requests_mock, caplog): From 39958bf1ccb338324b3a3c53e89f15b2683f6de7 Mon Sep 17 00:00:00 2001 From: niebl Date: Mon, 9 Feb 2026 14:36:08 +0100 Subject: [PATCH 08/40] fix bearer token formatting --- openeo/rest/auth/auth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openeo/rest/auth/auth.py b/openeo/rest/auth/auth.py index 9d47edb7b..d0f0cb766 100644 --- a/openeo/rest/auth/auth.py +++ b/openeo/rest/auth/auth.py @@ -42,8 +42,9 @@ class BasicBearerAuth(BearerAuth): """Bearer token for Basic Auth (openEO API 1.0.0 style)""" def __init__(self, access_token: str, jwt_conformance: bool = False): + bearer = False if jwt_conformance: - bearer="{t}" + bearer= "{t}".format(t=access_token) else: bearer = "basic//{t}".format(t=access_token) super().__init__(bearer=bearer) From 65eff7732546262426948fc6e9a2415a99b4631c Mon Sep 17 00:00:00 2001 From: niebl Date: Mon, 9 Feb 2026 15:32:52 +0100 Subject: [PATCH 09/40] fix basic auth test --- tests/rest/test_connection.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index b3b01a89d..b3f430321 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -861,15 +861,21 @@ def test_authenticate_basic_from_config(requests_mock, api_version, auth_config, assert conn.auth.bearer == "basic//6cc3570k3n" def test_authenticate_basic_jwt_bearer(requests_mock, basic_auth): - requests_mock.get(API_URL, json={"api_version": "1.3.0", "endpoints": BASIC_ENDPOINTS}) + requests_mock.get(API_URL, json={ + "api_version": "1.3.0", + "endpoints": BASIC_ENDPOINTS, + "conformsTo": ["https://api.openeo.org/1.3.0/authentication/jwt"] + } + ) conn = Connection(API_URL) + assert isinstance(conn.auth, NullAuth) conn.authenticate_basic(username=basic_auth.username, password=basic_auth.password) capabilities = conn.capabilities() assert isinstance(conn.auth, BearerAuth) assert capabilities.api_version() == "1.3.0" - assert capabilities.has_conformance("https://api.openeo.org/*/authentication/jwt") == "1.3.0" + assert capabilities.has_conformance("https://api.openeo.org/*/authentication/jwt") == True assert conn.auth.bearer == "6cc3570k3n" @pytest.mark.slow From 4273d74ffc404d208d07db9b80af1b33c907dd95 Mon Sep 17 00:00:00 2001 From: niebl Date: Mon, 9 Feb 2026 15:43:52 +0100 Subject: [PATCH 10/40] refactor requests_mock --- tests/rest/test_connection.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index b3f430321..3acc8ecfb 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -861,12 +861,7 @@ def test_authenticate_basic_from_config(requests_mock, api_version, auth_config, assert conn.auth.bearer == "basic//6cc3570k3n" def test_authenticate_basic_jwt_bearer(requests_mock, basic_auth): - requests_mock.get(API_URL, json={ - "api_version": "1.3.0", - "endpoints": BASIC_ENDPOINTS, - "conformsTo": ["https://api.openeo.org/1.3.0/authentication/jwt"] - } - ) + requests_mock.get(API_URL, json=build_capabilities(api_version="1.3.0")) conn = Connection(API_URL) @@ -903,7 +898,6 @@ def test_authenticate_oidc_authorization_code_100_single_implicit(requests_mock, assert conn.auth.bearer == 'oidc/fauth/' + oidc_mock.state["access_token"] assert "No OIDC provider given, but only one available: 'fauth'. Using that one." in caplog.text - def test_authenticate_oidc_authorization_code_100_single_wrong_id(requests_mock): requests_mock.get(API_URL, json={"api_version": "1.0.0"}) client_id = "myclient" From 84a82abad483edd6be34808ce610fa53ed099d71 Mon Sep 17 00:00:00 2001 From: niebl Date: Mon, 9 Feb 2026 15:47:30 +0100 Subject: [PATCH 11/40] use comparableVersion for cofnormance determination --- openeo/rest/_testing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openeo/rest/_testing.py b/openeo/rest/_testing.py index bc920ab5e..70ffba406 100644 --- a/openeo/rest/_testing.py +++ b/openeo/rest/_testing.py @@ -14,6 +14,7 @@ Union, ) +from openeo.utils.version import ComparableVersion from openeo import Connection, DataCube from openeo.rest.vectorcube import VectorCube from openeo.utils.http import HTTP_201_CREATED, HTTP_202_ACCEPTED, HTTP_204_NO_CONTENT @@ -442,8 +443,8 @@ def build_conformance( "https://api.stacspec.org/v{stac_version}/core", "https://api.stacspec.org/v{stac_version}/collections" ] - if api_version == "1.3.0": #TODO: use ComparableVersion - conformance.append("https://api.openeo.org/1.3.0/authentication/jwt") + if ComparableVersion(api_version) >= ComparableVersion("1.3.0"): + conformance.append(f"https://api.openeo.org/{api_version}/authentication/jwt") return conformance From ce742e3ef63a4a1b9b89c69013506467b6b11f34 Mon Sep 17 00:00:00 2001 From: Caro Niebl Date: Tue, 10 Feb 2026 09:20:35 +0100 Subject: [PATCH 12/40] Update openeo/rest/auth/auth.py Co-authored-by: Matthias Mohr --- openeo/rest/auth/auth.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openeo/rest/auth/auth.py b/openeo/rest/auth/auth.py index d0f0cb766..4c37a69dc 100644 --- a/openeo/rest/auth/auth.py +++ b/openeo/rest/auth/auth.py @@ -42,12 +42,9 @@ class BasicBearerAuth(BearerAuth): """Bearer token for Basic Auth (openEO API 1.0.0 style)""" def __init__(self, access_token: str, jwt_conformance: bool = False): - bearer = False - if jwt_conformance: - bearer= "{t}".format(t=access_token) - else: - bearer = "basic//{t}".format(t=access_token) - super().__init__(bearer=bearer) + if not jwt_conformance: + access_token = "basic//{t}".format(t=access_token) + super().__init__(bearer=access_token) class OidcBearerAuth(BearerAuth): From d05271faa2c53ad24d088744b6fa78781c8042f4 Mon Sep 17 00:00:00 2001 From: Caro Niebl Date: Tue, 10 Feb 2026 10:31:22 +0100 Subject: [PATCH 13/40] Update openeo/rest/_testing.py Co-authored-by: Matthias Mohr --- openeo/rest/_testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openeo/rest/_testing.py b/openeo/rest/_testing.py index 70ffba406..fd9c7bf48 100644 --- a/openeo/rest/_testing.py +++ b/openeo/rest/_testing.py @@ -442,7 +442,7 @@ def build_conformance( "https://api.openeo.org/{api_version}", "https://api.stacspec.org/v{stac_version}/core", "https://api.stacspec.org/v{stac_version}/collections" - ] + ] if ComparableVersion(api_version) >= ComparableVersion("1.3.0"): conformance.append(f"https://api.openeo.org/{api_version}/authentication/jwt") return conformance From 5166edaf1804d20a994776331bbe4644e0c52d63 Mon Sep 17 00:00:00 2001 From: Caro Niebl Date: Tue, 10 Feb 2026 10:37:22 +0100 Subject: [PATCH 14/40] Update openeo/rest/_testing.py Co-authored-by: Matthias Mohr --- openeo/rest/_testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openeo/rest/_testing.py b/openeo/rest/_testing.py index fd9c7bf48..a40c810bd 100644 --- a/openeo/rest/_testing.py +++ b/openeo/rest/_testing.py @@ -502,7 +502,7 @@ def build_capabilities( conformance = build_conformance( api_version=api_version, stac_version=stac_version - ) + ) capabilities = { "api_version": api_version, From 9884bf8c4d4f9b9191c2e2a193af0fcbaacfba7e Mon Sep 17 00:00:00 2001 From: niebl Date: Tue, 10 Feb 2026 10:36:46 +0100 Subject: [PATCH 15/40] refactor to use get --- openeo/rest/capabilities.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openeo/rest/capabilities.py b/openeo/rest/capabilities.py index bf59649de..4dbc8cdf2 100644 --- a/openeo/rest/capabilities.py +++ b/openeo/rest/capabilities.py @@ -40,10 +40,9 @@ def api_version_check(self) -> ComparableVersion: def has_conformance(self, conformance: str) -> bool: """Check if backend provides a given conformance string""" - if "conformsTo" in self.capabilities: - for url in self.capabilities["conformsTo"]: - if fnmatch(url, conformance): - return True + for url in self.capabilities.get("conformsTo", []): + if fnmatch(url, conformance): + return True return False From 299a079a7cb9f76cc17be0fdf15ac6a148bc4a84 Mon Sep 17 00:00:00 2001 From: niebl Date: Tue, 10 Feb 2026 11:24:34 +0100 Subject: [PATCH 16/40] indentation --- tests/rest/test_connection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index 3acc8ecfb..e9269f51c 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -409,8 +409,8 @@ def test_connect_with_session(): ], "https://oeo.test/openeo/1.1.0/", "1.1.0", - ), - ( + ), + ( [ {"api_version": "0.4.1", "url": "https://oeo.test/openeo/0.4.1/"}, {"api_version": "1.0.0", "url": "https://oeo.test/openeo/1.0.0/"}, @@ -464,8 +464,8 @@ def test_connect_with_session(): ], "https://oeo.test/openeo/1.1.0/", "1.1.0", - ), - ( + ), + ( [ { "api_version": "0.1.0", From 71a4503c7f418475fa507d2630cf98b43697a7ba Mon Sep 17 00:00:00 2001 From: niebl Date: Tue, 10 Feb 2026 11:24:56 +0100 Subject: [PATCH 17/40] refactor conformance string --- openeo/rest/__init__.py | 2 ++ openeo/rest/connection.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openeo/rest/__init__.py b/openeo/rest/__init__.py index 37b3a8170..dac500c00 100644 --- a/openeo/rest/__init__.py +++ b/openeo/rest/__init__.py @@ -10,6 +10,8 @@ DEFAULT_JOB_STATUS_POLL_CONNECTION_RETRY_INTERVAL = 30 DEFAULT_JOB_STATUS_POLL_SOFT_ERROR_MAX = 10 +CONFORMANCE_JWT_BEARER = "https://api.openeo.org/*/authentication/jwt" + class OpenEoClientException(BaseOpenEoException): """Base class for OpenEO client exceptions""" pass diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index 199ca9735..768841c9b 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -47,6 +47,7 @@ from openeo.metadata import CollectionMetadata from openeo.rest import ( DEFAULT_DOWNLOAD_CHUNK_SIZE, + CONFORMANCE_JWT_BEARER, CapabilitiesException, OpenEoApiError, OpenEoClientException, @@ -279,7 +280,7 @@ def authenticate_basic(self, username: Optional[str] = None, password: Optional[ ).json() # check for JWT bearer token conformance - jwt_conformance = self.capabilities().has_conformance("https://api.openeo.org/*/authentication/jwt") + jwt_conformance = self.capabilities().has_conformance(CONFORMANCE_JWT_BEARER) # Switch to bearer based authentication in further requests. self.auth = BasicBearerAuth(access_token=resp["access_token"], jwt_conformance = jwt_conformance) @@ -421,7 +422,7 @@ def _authenticate_oidc( token = tokens.access_token # check for JWT bearer token conformance - jwt_conformance = self.capabilities().has_conformance("https://api.openeo.org/*/authentication/jwt") + jwt_conformance = self.capabilities().has_conformance(CONFORMANCE_JWT_BEARER) self.auth = OidcBearerAuth(provider_id=provider_id, access_token=token, jwt_conformance=jwt_conformance) self._oidc_auth_renewer = oidc_auth_renewer return self From d8dda4114ab193ed2c243091d36f7fc19ed82df9 Mon Sep 17 00:00:00 2001 From: niebl Date: Tue, 10 Feb 2026 12:37:22 +0100 Subject: [PATCH 18/40] fix: OidcBearerAuth --- openeo/rest/auth/auth.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openeo/rest/auth/auth.py b/openeo/rest/auth/auth.py index 4c37a69dc..7a4684d2e 100644 --- a/openeo/rest/auth/auth.py +++ b/openeo/rest/auth/auth.py @@ -51,8 +51,7 @@ class OidcBearerAuth(BearerAuth): """Bearer token for OIDC Auth (openEO API 1.0.0 style)""" def __init__(self, provider_id: str, access_token: str, jwt_conformance: bool = False): - if jwt_conformance: - bearer="{t}" - else: - bearer="oidc/{p}/{t}".format(p=provider_id, t=access_token) - super().__init__(bearer=bearer) + if not jwt_conformance: + access_token = "oidc/{p}/{t}".format(p=provider_id, t=access_token) + super().__init__(bearer=access_token) + From 7107497e61beaa431186638394846e6b67a0ee41 Mon Sep 17 00:00:00 2001 From: Caro Niebl Date: Tue, 10 Feb 2026 12:40:36 +0100 Subject: [PATCH 19/40] Update tests/rest/test_connection.py Co-authored-by: Matthias Mohr --- tests/rest/test_connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index e9269f51c..d62144e75 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -61,7 +61,7 @@ # TODO: eliminate this and replace with `build_capabilities` usage BASIC_ENDPOINTS = [ {"path": "/credentials/basic", "methods": ["GET"]} - ] +] GEOJSON_POINT_01 = {"type": "Point", "coordinates": [3, 52]} From d92509411c9011236c428cdcd8e86c83fbb07d2c Mon Sep 17 00:00:00 2001 From: niebl Date: Tue, 10 Feb 2026 13:03:40 +0100 Subject: [PATCH 20/40] use re in has_conformance --- openeo/rest/capabilities.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openeo/rest/capabilities.py b/openeo/rest/capabilities.py index 4dbc8cdf2..96062bd56 100644 --- a/openeo/rest/capabilities.py +++ b/openeo/rest/capabilities.py @@ -1,5 +1,5 @@ from typing import Dict, List, Optional, Union -from fnmatch import fnmatch +import re from openeo.internal.jupyter import render_component from openeo.rest.models import federation_extension @@ -38,10 +38,11 @@ def api_version_check(self) -> ComparableVersion: raise ApiVersionException("No API version found") return ComparableVersion(api_version) - def has_conformance(self, conformance: str) -> bool: + def has_conformance(self, uri: str) -> bool: """Check if backend provides a given conformance string""" - for url in self.capabilities.get("conformsTo", []): - if fnmatch(url, conformance): + uri = re.escape(uri).replace('\\*', '[^/]+') + for conformance_uri in self.capabilities.get("conformsTo", []): + if re.match(uri, conformance_uri): return True return False From 7a92e8fd83fc608afcea29a9297ecaa6c138b6bb Mon Sep 17 00:00:00 2001 From: niebl Date: Tue, 10 Feb 2026 13:37:34 +0100 Subject: [PATCH 21/40] add oidc tests for jwt bearer token --- tests/rest/test_connection.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index d62144e75..3e74a9eb0 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -1061,6 +1061,36 @@ def test_authenticate_oidc_auth_code_pkce_flow_client_from_config(requests_mock, assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] +@pytest.mark.slow +def test_authenticate_oidc_auth_code_pkce_flow_jwt_bearer(requests_mock, auth_config): + requests_mock.get(API_URL, json=build_capabilities(api_version="1.3.0")) + client_id = "myclient" + issuer = "https://oidc.test" + requests_mock.get(API_URL + 'credentials/oidc', json={ + "providers": [{"id": "oi", "issuer": issuer, "title": "example", "scopes": ["openid"]}] + }) + oidc_mock = OidcMock( + requests_mock=requests_mock, + expected_grant_type="authorization_code", + expected_client_id=client_id, + expected_fields={"scope": "openid"}, + oidc_issuer=issuer, + scopes_supported=["openid"], + ) + auth_config.set_oidc_client_config(backend=API_URL, provider_id="oi", client_id=client_id) + + # With all this set up, kick off the openid connect flow + refresh_token_store = mock.Mock() + conn = Connection(API_URL, refresh_token_store=refresh_token_store) + assert isinstance(conn.auth, NullAuth) + conn.authenticate_oidc_authorization_code(webbrowser_open=oidc_mock.webbrowser_open) + capabilities = conn.capabilities() + assert isinstance(conn.auth, BearerAuth) + assert capabilities.api_version() == "1.3.0" + assert capabilities.has_conformance("https://api.openeo.org/*/authentication/jwt") == True + assert conn.auth.bearer == oidc_mock.state["access_token"] + # TODO: check issuer ("iss") value in parsed jwt. this will require the example jwt to be formatted accordingly + assert refresh_token_store.mock_calls == [] def test_authenticate_oidc_client_credentials(requests_mock): requests_mock.get(API_URL, json={"api_version": "1.0.0"}) From 9317759329a8d430ad8c378916cc242f912293a9 Mon Sep 17 00:00:00 2001 From: Caro Niebl Date: Tue, 10 Feb 2026 14:56:29 +0100 Subject: [PATCH 22/40] Apply suggestion from @m-mohr Co-authored-by: Matthias Mohr --- openeo/rest/_testing.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openeo/rest/_testing.py b/openeo/rest/_testing.py index a40c810bd..f67179c81 100644 --- a/openeo/rest/_testing.py +++ b/openeo/rest/_testing.py @@ -464,8 +464,6 @@ def build_capabilities( """Build a dummy capabilities document for testing purposes.""" endpoints = [] - if basic_auth: - endpoints.append({"path": "/conformance", "methods": ["GET"]}) if basic_auth: endpoints.append({"path": "/credentials/basic", "methods": ["GET"]}) if oidc_auth: From dc899704ac1d38a86255c311adb3f0bf0dc92fbe Mon Sep 17 00:00:00 2001 From: niebl Date: Tue, 10 Feb 2026 15:04:41 +0100 Subject: [PATCH 23/40] line breaks --- tests/rest/test_connection.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index 3e74a9eb0..d000dafa4 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -850,6 +850,7 @@ def test_authenticate_basic(requests_mock, api_version, basic_auth): assert isinstance(conn.auth, BearerAuth) assert conn.auth.bearer == "basic//6cc3570k3n" + def test_authenticate_basic_from_config(requests_mock, api_version, auth_config, basic_auth): requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": BASIC_ENDPOINTS}) auth_config.set_basic_auth(backend=API_URL, username=basic_auth.username, password=basic_auth.password) @@ -860,6 +861,7 @@ def test_authenticate_basic_from_config(requests_mock, api_version, auth_config, assert isinstance(conn.auth, BearerAuth) assert conn.auth.bearer == "basic//6cc3570k3n" + def test_authenticate_basic_jwt_bearer(requests_mock, basic_auth): requests_mock.get(API_URL, json=build_capabilities(api_version="1.3.0")) @@ -898,6 +900,7 @@ def test_authenticate_oidc_authorization_code_100_single_implicit(requests_mock, assert conn.auth.bearer == 'oidc/fauth/' + oidc_mock.state["access_token"] assert "No OIDC provider given, but only one available: 'fauth'. Using that one." in caplog.text + def test_authenticate_oidc_authorization_code_100_single_wrong_id(requests_mock): requests_mock.get(API_URL, json={"api_version": "1.0.0"}) client_id = "myclient" From 6040a9c206d431788a8aae5609362df909adbd15 Mon Sep 17 00:00:00 2001 From: Caro Niebl Date: Wed, 18 Feb 2026 11:06:17 +0100 Subject: [PATCH 24/40] Update openeo/rest/_testing.py Co-authored-by: Stefaan Lippens --- openeo/rest/_testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openeo/rest/_testing.py b/openeo/rest/_testing.py index f67179c81..ca51ddf94 100644 --- a/openeo/rest/_testing.py +++ b/openeo/rest/_testing.py @@ -499,7 +499,7 @@ def build_capabilities( conformance = build_conformance( api_version=api_version, - stac_version=stac_version + stac_version=stac_version, ) capabilities = { From 8b72876d5138cb6230e470da2cf5b9d1bb18825c Mon Sep 17 00:00:00 2001 From: niebl Date: Wed, 18 Feb 2026 10:10:19 +0100 Subject: [PATCH 25/40] bump stack version in build conformance --- openeo/rest/_testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openeo/rest/_testing.py b/openeo/rest/_testing.py index ca51ddf94..95c8ace7e 100644 --- a/openeo/rest/_testing.py +++ b/openeo/rest/_testing.py @@ -436,7 +436,7 @@ def get_status(job_id: str, current_status: str) -> str: def build_conformance( *, api_version: str = "1.0.0", - stac_version: str = "0.9.0", + stac_version: str = "1.1.0", ) -> list[str]: conformance = [ "https://api.openeo.org/{api_version}", From 85ff1b0cae4fbb9c8fcc8dde356a141536ad8eac Mon Sep 17 00:00:00 2001 From: niebl Date: Wed, 18 Feb 2026 10:57:26 +0100 Subject: [PATCH 26/40] keep bearer auth simple --- openeo/rest/auth/auth.py | 12 ++++-------- openeo/rest/connection.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/openeo/rest/auth/auth.py b/openeo/rest/auth/auth.py index 7a4684d2e..cfa3329b4 100644 --- a/openeo/rest/auth/auth.py +++ b/openeo/rest/auth/auth.py @@ -41,17 +41,13 @@ def __call__(self, req: Request) -> Request: class BasicBearerAuth(BearerAuth): """Bearer token for Basic Auth (openEO API 1.0.0 style)""" - def __init__(self, access_token: str, jwt_conformance: bool = False): - if not jwt_conformance: - access_token = "basic//{t}".format(t=access_token) - super().__init__(bearer=access_token) + def __init__(self, access_token: str): + super().__init__(bearer="basic//{t}".format(t=access_token)) class OidcBearerAuth(BearerAuth): """Bearer token for OIDC Auth (openEO API 1.0.0 style)""" - def __init__(self, provider_id: str, access_token: str, jwt_conformance: bool = False): - if not jwt_conformance: - access_token = "oidc/{p}/{t}".format(p=provider_id, t=access_token) - super().__init__(bearer=access_token) + def __init__(self, provider_id: str, access_token: str): + super().__init__(bearer="oidc/{p}/{t}".format(p=provider_id, t=access_token)) diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index 768841c9b..38604ccd6 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -283,7 +283,10 @@ def authenticate_basic(self, username: Optional[str] = None, password: Optional[ jwt_conformance = self.capabilities().has_conformance(CONFORMANCE_JWT_BEARER) # Switch to bearer based authentication in further requests. - self.auth = BasicBearerAuth(access_token=resp["access_token"], jwt_conformance = jwt_conformance) + if jwt_conformance: + self.auth = BearerAuth(bearer=resp["access_token"]) + else: + self.auth = BasicBearerAuth(access_token=resp["access_token"]) return self def _get_oidc_provider( @@ -423,7 +426,10 @@ def _authenticate_oidc( token = tokens.access_token # check for JWT bearer token conformance jwt_conformance = self.capabilities().has_conformance(CONFORMANCE_JWT_BEARER) - self.auth = OidcBearerAuth(provider_id=provider_id, access_token=token, jwt_conformance=jwt_conformance) + if jwt_conformance: + self.auth = BearerAuth(bearer=token) + else: + self.auth = OidcBearerAuth(provider_id=provider_id, access_token=token) self._oidc_auth_renewer = oidc_auth_renewer return self From 240c18d565351aeaf387c3e3bc53779ee67491e1 Mon Sep 17 00:00:00 2001 From: niebl Date: Wed, 18 Feb 2026 11:04:55 +0100 Subject: [PATCH 27/40] formatting --- tests/rest/test_connection.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index d000dafa4..8f515e1a0 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -59,9 +59,7 @@ API_URL = "https://oeo.test/" # TODO: eliminate this and replace with `build_capabilities` usage -BASIC_ENDPOINTS = [ - {"path": "/credentials/basic", "methods": ["GET"]} -] +BASIC_ENDPOINTS = [{"path": "/credentials/basic", "methods": ["GET"]}] GEOJSON_POINT_01 = {"type": "Point", "coordinates": [3, 52]} @@ -1092,7 +1090,7 @@ def test_authenticate_oidc_auth_code_pkce_flow_jwt_bearer(requests_mock, auth_co assert capabilities.api_version() == "1.3.0" assert capabilities.has_conformance("https://api.openeo.org/*/authentication/jwt") == True assert conn.auth.bearer == oidc_mock.state["access_token"] - # TODO: check issuer ("iss") value in parsed jwt. this will require the example jwt to be formatted accordingly + # TODO: check issuer ("iss") value in parsed jwt. this will require the example jwt to be formatted accordingly assert refresh_token_store.mock_calls == [] def test_authenticate_oidc_client_credentials(requests_mock): From 94fe9142a2c690ab4244f393085ff8a5700f9953 Mon Sep 17 00:00:00 2001 From: niebl Date: Wed, 18 Feb 2026 11:16:21 +0100 Subject: [PATCH 28/40] change import location of jwt bearer uri template --- openeo/rest/__init__.py | 2 -- openeo/rest/capabilities.py | 6 +++--- openeo/rest/connection.py | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/openeo/rest/__init__.py b/openeo/rest/__init__.py index dac500c00..37b3a8170 100644 --- a/openeo/rest/__init__.py +++ b/openeo/rest/__init__.py @@ -10,8 +10,6 @@ DEFAULT_JOB_STATUS_POLL_CONNECTION_RETRY_INTERVAL = 30 DEFAULT_JOB_STATUS_POLL_SOFT_ERROR_MAX = 10 -CONFORMANCE_JWT_BEARER = "https://api.openeo.org/*/authentication/jwt" - class OpenEoClientException(BaseOpenEoException): """Base class for OpenEO client exceptions""" pass diff --git a/openeo/rest/capabilities.py b/openeo/rest/capabilities.py index 96062bd56..25e9b8f46 100644 --- a/openeo/rest/capabilities.py +++ b/openeo/rest/capabilities.py @@ -1,5 +1,5 @@ -from typing import Dict, List, Optional, Union import re +from typing import Dict, List, Optional, Union from openeo.internal.jupyter import render_component from openeo.rest.models import federation_extension @@ -8,6 +8,7 @@ __all__ = ["OpenEoCapabilities"] +CONFORMANCE_JWT_BEARER = "https://api.openeo.org/*/authentication/jwt" class OpenEoCapabilities: """Container of the openEO capabilities document of an openEO backend.""" @@ -37,7 +38,7 @@ def api_version_check(self) -> ComparableVersion: if not api_version: raise ApiVersionException("No API version found") return ComparableVersion(api_version) - + def has_conformance(self, uri: str) -> bool: """Check if backend provides a given conformance string""" uri = re.escape(uri).replace('\\*', '[^/]+') @@ -45,7 +46,6 @@ def has_conformance(self, uri: str) -> bool: if re.match(uri, conformance_uri): return True return False - def supports_endpoint(self, path: str, method="GET") -> bool: """Check if backend supports given endpoint""" diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index 38604ccd6..8a40c43a8 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -47,7 +47,6 @@ from openeo.metadata import CollectionMetadata from openeo.rest import ( DEFAULT_DOWNLOAD_CHUNK_SIZE, - CONFORMANCE_JWT_BEARER, CapabilitiesException, OpenEoApiError, OpenEoClientException, @@ -69,7 +68,7 @@ OidcRefreshTokenAuthenticator, OidcResourceOwnerPasswordAuthenticator, ) -from openeo.rest.capabilities import OpenEoCapabilities +from openeo.rest.capabilities import CONFORMANCE_JWT_BEARER, OpenEoCapabilities from openeo.rest.datacube import DataCube, InputDate from openeo.rest.graph_building import CollectionProperty from openeo.rest.job import BatchJob From e6230a314abc5bd1b842cf05820bd5a7fa2f63d6 Mon Sep 17 00:00:00 2001 From: niebl Date: Wed, 18 Feb 2026 11:27:18 +0100 Subject: [PATCH 29/40] revert test_testing.py to af55fd68312c6e0b1bf7c819d576d7d0ec32e959 --- tests/rest/test_testing.py | 69 ++++++++++++++------------------------ 1 file changed, 26 insertions(+), 43 deletions(-) diff --git a/tests/rest/test_testing.py b/tests/rest/test_testing.py index 7b11ecd22..589dda3dc 100644 --- a/tests/rest/test_testing.py +++ b/tests/rest/test_testing.py @@ -7,12 +7,9 @@ @pytest.fixture -def dummy_backend120(requests_mock, con120): +def dummy_backend(requests_mock, con120): return DummyBackend(requests_mock=requests_mock, connection=con120) -@pytest.fixture -def dummy_backend130(requests_mock, con130): - return DummyBackend(requests_mock=requests_mock, connection=con130) DUMMY_PG_ADD35 = { "add35": {"process_id": "add", "arguments": {"x": 3, "y": 5}, "result": True}, @@ -20,10 +17,10 @@ def dummy_backend130(requests_mock, con130): class TestDummyBackend: - def test_create_job(self, dummy_backend120, con120): - assert dummy_backend120.batch_jobs == {} + def test_create_job(self, dummy_backend, con120): + assert dummy_backend.batch_jobs == {} _ = con120.create_job(DUMMY_PG_ADD35) - assert dummy_backend120.batch_jobs == { + assert dummy_backend.batch_jobs == { "job-000": { "job_id": "job-000", "pg": {"add35": {"process_id": "add", "arguments": {"x": 3, "y": 5}, "result": True}}, @@ -31,33 +28,33 @@ def test_create_job(self, dummy_backend120, con120): } } - def test_start_job(self, dummy_backend120, con120): + def test_start_job(self, dummy_backend, con120): job = con120.create_job(DUMMY_PG_ADD35) - assert dummy_backend120.batch_jobs == { + assert dummy_backend.batch_jobs == { "job-000": {"job_id": "job-000", "pg": DUMMY_PG_ADD35, "status": "created"}, } job.start() - assert dummy_backend120.batch_jobs == { + assert dummy_backend.batch_jobs == { "job-000": {"job_id": "job-000", "pg": DUMMY_PG_ADD35, "status": "finished"}, } - def test_job_status_updater_error(self, dummy_backend120, con120): - dummy_backend120.job_status_updater = lambda job_id, current_status: "error" + def test_job_status_updater_error(self, dummy_backend, con120): + dummy_backend.job_status_updater = lambda job_id, current_status: "error" job = con120.create_job(DUMMY_PG_ADD35) - assert dummy_backend120.batch_jobs["job-000"]["status"] == "created" + assert dummy_backend.batch_jobs["job-000"]["status"] == "created" job.start() - assert dummy_backend120.batch_jobs["job-000"]["status"] == "error" + assert dummy_backend.batch_jobs["job-000"]["status"] == "error" @pytest.mark.parametrize("final", ["finished", "error"]) - def test_setup_simple_job_status_flow(self, dummy_backend120, con120, final): - dummy_backend120.setup_simple_job_status_flow(queued=2, running=3, final=final) + def test_setup_simple_job_status_flow(self, dummy_backend, con120, final): + dummy_backend.setup_simple_job_status_flow(queued=2, running=3, final=final) job = con120.create_job(DUMMY_PG_ADD35) - assert dummy_backend120.batch_jobs["job-000"]["status"] == "created" + assert dummy_backend.batch_jobs["job-000"]["status"] == "created" # Note that first status update (to "queued" here) is triggered from `start()`, not `status()` like below job.start() - assert dummy_backend120.batch_jobs["job-000"]["status"] == "queued" + assert dummy_backend.batch_jobs["job-000"]["status"] == "queued" # Now go through rest of status flow, through `status()` calls assert job.status() == "queued" @@ -69,25 +66,25 @@ def test_setup_simple_job_status_flow(self, dummy_backend120, con120, final): assert job.status() == final assert job.status() == final - def test_setup_simple_job_status_flow_final_per_job(self, dummy_backend120, con120): + def test_setup_simple_job_status_flow_final_per_job(self, dummy_backend, con120): """Test per-job specific final status""" - dummy_backend120.setup_simple_job_status_flow( + dummy_backend.setup_simple_job_status_flow( queued=2, running=3, final="finished", final_per_job={"job-001": "error"} ) job0 = con120.create_job(DUMMY_PG_ADD35) job1 = con120.create_job(DUMMY_PG_ADD35) job2 = con120.create_job(DUMMY_PG_ADD35) - assert dummy_backend120.batch_jobs["job-000"]["status"] == "created" - assert dummy_backend120.batch_jobs["job-001"]["status"] == "created" - assert dummy_backend120.batch_jobs["job-002"]["status"] == "created" + assert dummy_backend.batch_jobs["job-000"]["status"] == "created" + assert dummy_backend.batch_jobs["job-001"]["status"] == "created" + assert dummy_backend.batch_jobs["job-002"]["status"] == "created" # Note that first status update (to "queued" here) is triggered from `start()`, not `status()` like below job0.start() job1.start() job2.start() - assert dummy_backend120.batch_jobs["job-000"]["status"] == "queued" - assert dummy_backend120.batch_jobs["job-001"]["status"] == "queued" - assert dummy_backend120.batch_jobs["job-002"]["status"] == "queued" + assert dummy_backend.batch_jobs["job-000"]["status"] == "queued" + assert dummy_backend.batch_jobs["job-001"]["status"] == "queued" + assert dummy_backend.batch_jobs["job-002"]["status"] == "queued" # Now go through rest of status flow, through `status()` calls for expected_status in ["queued", "running", "running", "running"]: @@ -101,23 +98,9 @@ def test_setup_simple_job_status_flow_final_per_job(self, dummy_backend120, con1 assert job1.status() == "error" assert job2.status() == "finished" - def test_setup_job_start_failure(self, dummy_backend120): - job = dummy_backend120.connection.create_job(process_graph={}) - dummy_backend120.setup_job_start_failure() + def test_setup_job_start_failure(self, dummy_backend): + job = dummy_backend.connection.create_job(process_graph={}) + dummy_backend.setup_job_start_failure() with pytest.raises(OpenEoApiError, match=re.escape("[500] Internal: No job starting for you, buddy")): job.start() assert job.status() == "error" - - def test_version(self, dummy_backend120, dummy_backend130): - capabilities120 = dummy_backend120.connection.capabilities() - capabilities130 = dummy_backend130.connection.capabilities() - - assert capabilities120.api_version() == "1.2.0" - assert capabilities130.api_version() == "1.3.0" - - def test_jwt_conformance(self, dummy_backend120, dummy_backend130): - capabilities120 = dummy_backend120.connection.capabilities() - capabilities130 = dummy_backend130.connection.capabilities() - - assert capabilities120.has_conformance("https://api.openeo.org/*/authentication/jwt") == False - assert capabilities130.has_conformance("https://api.openeo.org/*/authentication/jwt") == True \ No newline at end of file From c2c5766822bce0f30f23c615c605b2512fcbdd2f Mon Sep 17 00:00:00 2001 From: niebl Date: Wed, 18 Feb 2026 11:45:19 +0100 Subject: [PATCH 30/40] re-add tests for jwt conformance --- tests/rest/test_testing.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/rest/test_testing.py b/tests/rest/test_testing.py index 589dda3dc..1049d1af6 100644 --- a/tests/rest/test_testing.py +++ b/tests/rest/test_testing.py @@ -11,6 +11,11 @@ def dummy_backend(requests_mock, con120): return DummyBackend(requests_mock=requests_mock, connection=con120) +@pytest.fixture +def dummy_backend130(requests_mock, con130): + return DummyBackend(requests_mock=requests_mock, connection=con130) + + DUMMY_PG_ADD35 = { "add35": {"process_id": "add", "arguments": {"x": 3, "y": 5}, "result": True}, } @@ -104,3 +109,20 @@ def test_setup_job_start_failure(self, dummy_backend): with pytest.raises(OpenEoApiError, match=re.escape("[500] Internal: No job starting for you, buddy")): job.start() assert job.status() == "error" + + # for better distinction within the following tests + dummy_backend120 = dummy_backend + + def test_version(self, dummy_backend120, dummy_backend130): + capabilities120 = dummy_backend120.connection.capabilities() + capabilities130 = dummy_backend130.connection.capabilities() + + assert capabilities120.api_version() == "1.2.0" + assert capabilities130.api_version() == "1.3.0" + + def test_jwt_conformance(self, dummy_backend120, dummy_backend130): + capabilities120 = dummy_backend120.connection.capabilities() + capabilities130 = dummy_backend130.connection.capabilities() + + assert capabilities120.has_conformance("https://api.openeo.org/*/authentication/jwt") == False + assert capabilities130.has_conformance("https://api.openeo.org/*/authentication/jwt") == True From 82c00f41b8646a24f2ec8af05aaa1a30ac38c59d Mon Sep 17 00:00:00 2001 From: niebl Date: Wed, 18 Feb 2026 12:42:14 +0100 Subject: [PATCH 31/40] parametrize basic auth tests --- tests/rest/conftest.py | 2 +- tests/rest/test_connection.py | 30 +++++++++++++----------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/tests/rest/conftest.py b/tests/rest/conftest.py index 411c82e1e..7b67f6005 100644 --- a/tests/rest/conftest.py +++ b/tests/rest/conftest.py @@ -14,7 +14,7 @@ API_URL = "https://oeo.test/" -@pytest.fixture(params=["1.0.0"]) +@pytest.fixture(params=["1.0.0", "1.3.0"]) def api_version(request): return request.param diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index 8f515e1a0..a92108e04 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -840,38 +840,34 @@ def test_authenticate_basic_no_support(requests_mock, api_version): def test_authenticate_basic(requests_mock, api_version, basic_auth): - requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": BASIC_ENDPOINTS}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version)) conn = Connection(API_URL) + assert isinstance(conn.auth, NullAuth) conn.authenticate_basic(username=basic_auth.username, password=basic_auth.password) + capabilities = conn.capabilities() assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == "basic//6cc3570k3n" + if api_version == "1.3.0": + assert capabilities.has_conformance("https://api.openeo.org/*/authentication/jwt") == True + assert conn.auth.bearer == "6cc3570k3n" + else: + assert conn.auth.bearer == "basic//6cc3570k3n" def test_authenticate_basic_from_config(requests_mock, api_version, auth_config, basic_auth): - requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": BASIC_ENDPOINTS}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version)) auth_config.set_basic_auth(backend=API_URL, username=basic_auth.username, password=basic_auth.password) conn = Connection(API_URL) assert isinstance(conn.auth, NullAuth) conn.authenticate_basic() assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == "basic//6cc3570k3n" - - -def test_authenticate_basic_jwt_bearer(requests_mock, basic_auth): - requests_mock.get(API_URL, json=build_capabilities(api_version="1.3.0")) - - conn = Connection(API_URL) + if api_version == "1.3.0": + assert conn.auth.bearer == "6cc3570k3n" + else: + assert conn.auth.bearer == "basic//6cc3570k3n" - assert isinstance(conn.auth, NullAuth) - conn.authenticate_basic(username=basic_auth.username, password=basic_auth.password) - capabilities = conn.capabilities() - assert isinstance(conn.auth, BearerAuth) - assert capabilities.api_version() == "1.3.0" - assert capabilities.has_conformance("https://api.openeo.org/*/authentication/jwt") == True - assert conn.auth.bearer == "6cc3570k3n" @pytest.mark.slow def test_authenticate_oidc_authorization_code_100_single_implicit(requests_mock, caplog): From 1d5c20fc7e2beb3ee47aebdaae13acd679b3d9a7 Mon Sep 17 00:00:00 2001 From: niebl Date: Wed, 18 Feb 2026 14:45:43 +0100 Subject: [PATCH 32/40] parametrize oidc tests --- tests/rest/conftest.py | 6 +- tests/rest/test_connection.py | 471 +++++++++++++++++++++++----------- 2 files changed, 328 insertions(+), 149 deletions(-) diff --git a/tests/rest/conftest.py b/tests/rest/conftest.py index 7b67f6005..a84438894 100644 --- a/tests/rest/conftest.py +++ b/tests/rest/conftest.py @@ -14,11 +14,15 @@ API_URL = "https://oeo.test/" -@pytest.fixture(params=["1.0.0", "1.3.0"]) +@pytest.fixture(params=["1.0.0"]) def api_version(request): return request.param +@pytest.fixture(params=["1.0.0", "1.3.0"]) +def api_version_authentication_tests(request): + return request.param + class _Sleeper: def __init__(self): self.history = [] diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index a92108e04..d9be0c0e1 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -721,8 +721,8 @@ def test_api_error_non_json(requests_mock): assert exc.message == "olapola" -def test_create_connection_lazy_auth_config(requests_mock, api_version, basic_auth): - requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": BASIC_ENDPOINTS}) +def test_create_connection_lazy_auth_config(requests_mock, api_version_authentication_tests, basic_auth): + requests_mock.get(API_URL, json={"api_version": api_version_authentication_tests, "endpoints": BASIC_ENDPOINTS}) with mock.patch('openeo.rest.connection.AuthConfig') as AuthConfig: # Don't create default AuthConfig when not necessary @@ -768,8 +768,8 @@ def test_create_connection_lazy_refresh_token_store(requests_mock): ) -def test_list_auth_providers(requests_mock, api_version): - requests_mock.get(API_URL, json=build_capabilities(api_version=api_version)) +def test_list_auth_providers(requests_mock, api_version_authentication_tests): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) requests_mock.get( API_URL + "credentials/oidc", json={ @@ -803,10 +803,10 @@ def test_list_auth_providers(requests_mock, api_version): assert basic["title"] == "Internal" -def test_list_auth_providers_empty(requests_mock, api_version): +def test_list_auth_providers_empty(requests_mock, api_version_authentication_tests): requests_mock.get( API_URL, - json=build_capabilities(api_version=api_version, basic_auth=False, oidc_auth=False), + json=build_capabilities(api_version=api_version_authentication_tests, basic_auth=False, oidc_auth=False), ) conn = Connection(API_URL) @@ -814,8 +814,8 @@ def test_list_auth_providers_empty(requests_mock, api_version): assert len(providers) == 0 -def test_list_auth_providers_invalid(requests_mock, api_version, caplog): - requests_mock.get(API_URL, json=build_capabilities(api_version=api_version, basic_auth=False)) +def test_list_auth_providers_invalid(requests_mock, api_version_authentication_tests, caplog): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests, basic_auth=False)) error_message = "Maintenance ongoing" requests_mock.get( API_URL + "credentials/oidc", @@ -829,8 +829,8 @@ def test_list_auth_providers_invalid(requests_mock, api_version, caplog): assert f"Unable to load the OpenID Connect provider list: {error_message}" in caplog.messages -def test_authenticate_basic_no_support(requests_mock, api_version): - requests_mock.get(API_URL, json={"api_version": api_version, "endpoints": []}) +def test_authenticate_basic_no_support(requests_mock, api_version_authentication_tests): + requests_mock.get(API_URL, json={"api_version": api_version_authentication_tests, "endpoints": []}) conn = Connection(API_URL) assert isinstance(conn.auth, NullAuth) @@ -839,8 +839,8 @@ def test_authenticate_basic_no_support(requests_mock, api_version): assert isinstance(conn.auth, NullAuth) -def test_authenticate_basic(requests_mock, api_version, basic_auth): - requests_mock.get(API_URL, json=build_capabilities(api_version=api_version)) +def test_authenticate_basic(requests_mock, api_version_authentication_tests, basic_auth): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) conn = Connection(API_URL) @@ -848,30 +848,32 @@ def test_authenticate_basic(requests_mock, api_version, basic_auth): conn.authenticate_basic(username=basic_auth.username, password=basic_auth.password) capabilities = conn.capabilities() assert isinstance(conn.auth, BearerAuth) - if api_version == "1.3.0": + if api_version_authentication_tests == "1.3.0": assert capabilities.has_conformance("https://api.openeo.org/*/authentication/jwt") == True assert conn.auth.bearer == "6cc3570k3n" else: assert conn.auth.bearer == "basic//6cc3570k3n" -def test_authenticate_basic_from_config(requests_mock, api_version, auth_config, basic_auth): - requests_mock.get(API_URL, json=build_capabilities(api_version=api_version)) +def test_authenticate_basic_from_config(requests_mock, api_version_authentication_tests, auth_config, basic_auth): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) auth_config.set_basic_auth(backend=API_URL, username=basic_auth.username, password=basic_auth.password) conn = Connection(API_URL) assert isinstance(conn.auth, NullAuth) conn.authenticate_basic() assert isinstance(conn.auth, BearerAuth) - if api_version == "1.3.0": + if api_version_authentication_tests == "1.3.0": assert conn.auth.bearer == "6cc3570k3n" else: assert conn.auth.bearer == "basic//6cc3570k3n" @pytest.mark.slow -def test_authenticate_oidc_authorization_code_100_single_implicit(requests_mock, caplog): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_authorization_code_100_single_implicit( + requests_mock, api_version_authentication_tests, caplog +): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" requests_mock.get(API_URL + 'credentials/oidc', json={ "providers": [{"id": "fauth", "issuer": "https://fauth.test", "title": "Foo Auth", "scopes": ["openid", "im"]}] @@ -891,12 +893,16 @@ def test_authenticate_oidc_authorization_code_100_single_implicit(requests_mock, assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_authorization_code(client_id=client_id, webbrowser_open=oidc_mock.webbrowser_open) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/fauth/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/fauth/" + oidc_mock.state["access_token"] assert "No OIDC provider given, but only one available: 'fauth'. Using that one." in caplog.text -def test_authenticate_oidc_authorization_code_100_single_wrong_id(requests_mock): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_authorization_code_100_single_wrong_id(requests_mock, api_version_authentication_tests): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" requests_mock.get(API_URL + 'credentials/oidc', json={ "providers": [{"id": "fauth", "issuer": "https://fauth.test", "title": "Foo Auth", "scopes": ["openid", "w"]}] @@ -912,8 +918,10 @@ def test_authenticate_oidc_authorization_code_100_single_wrong_id(requests_mock) @pytest.mark.slow -def test_authenticate_oidc_authorization_code_100_multiple_no_given_id(requests_mock, caplog): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_authorization_code_100_multiple_no_given_id( + requests_mock, api_version_authentication_tests, caplog +): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" requests_mock.get(API_URL + 'credentials/oidc', json={ "providers": [ @@ -936,7 +944,11 @@ def test_authenticate_oidc_authorization_code_100_multiple_no_given_id(requests_ assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_authorization_code(client_id=client_id, webbrowser_open=oidc_mock.webbrowser_open) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/fauth/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/fauth/" + oidc_mock.state["access_token"] assert "No OIDC provider given. Using first provider 'fauth' as advertised by backend." in caplog.text @@ -959,8 +971,8 @@ def test_authenticate_oidc_authorization_code_100_multiple_wrong_id(requests_moc @pytest.mark.slow -def test_authenticate_oidc_authorization_code_100_multiple_success(requests_mock): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_authorization_code_100_multiple_success(requests_mock, api_version_authentication_tests): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" requests_mock.get(API_URL + 'credentials/oidc', json={ "providers": [ @@ -984,7 +996,11 @@ def test_authenticate_oidc_authorization_code_100_multiple_success(requests_mock client_id=client_id, provider_id="bauth", webbrowser_open=oidc_mock.webbrowser_open ) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/bauth/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/bauth/" + oidc_mock.state["access_token"] @pytest.mark.slow @@ -997,8 +1013,10 @@ def test_authenticate_oidc_authorization_code_100_multiple_success(requests_mock (True, ["openid", "email", "offline_access"], "offline_access openid"), ] ) -def test_authenticate_oidc_auth_code_pkce_flow(requests_mock, store_refresh_token, scopes_supported, expected_scope): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_auth_code_pkce_flow( + requests_mock, api_version_authentication_tests, store_refresh_token, scopes_supported, expected_scope +): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" issuer = "https://oidc.test" requests_mock.get(API_URL + 'credentials/oidc', json={ @@ -1021,7 +1039,11 @@ def test_authenticate_oidc_auth_code_pkce_flow(requests_mock, store_refresh_toke client_id=client_id, webbrowser_open=oidc_mock.webbrowser_open, store_refresh_token=store_refresh_token ) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] if store_refresh_token: refresh_token = oidc_mock.state["refresh_token"] assert refresh_token_store.mock_calls == [ @@ -1032,8 +1054,10 @@ def test_authenticate_oidc_auth_code_pkce_flow(requests_mock, store_refresh_toke @pytest.mark.slow -def test_authenticate_oidc_auth_code_pkce_flow_client_from_config(requests_mock, auth_config): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_auth_code_pkce_flow_client_from_config( + requests_mock, api_version_authentication_tests, auth_config +): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" issuer = "https://oidc.test" requests_mock.get(API_URL + 'credentials/oidc', json={ @@ -1055,42 +1079,16 @@ def test_authenticate_oidc_auth_code_pkce_flow_client_from_config(requests_mock, assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_authorization_code(webbrowser_open=oidc_mock.webbrowser_open) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] -@pytest.mark.slow -def test_authenticate_oidc_auth_code_pkce_flow_jwt_bearer(requests_mock, auth_config): - requests_mock.get(API_URL, json=build_capabilities(api_version="1.3.0")) - client_id = "myclient" - issuer = "https://oidc.test" - requests_mock.get(API_URL + 'credentials/oidc', json={ - "providers": [{"id": "oi", "issuer": issuer, "title": "example", "scopes": ["openid"]}] - }) - oidc_mock = OidcMock( - requests_mock=requests_mock, - expected_grant_type="authorization_code", - expected_client_id=client_id, - expected_fields={"scope": "openid"}, - oidc_issuer=issuer, - scopes_supported=["openid"], - ) - auth_config.set_oidc_client_config(backend=API_URL, provider_id="oi", client_id=client_id) - - # With all this set up, kick off the openid connect flow - refresh_token_store = mock.Mock() - conn = Connection(API_URL, refresh_token_store=refresh_token_store) - assert isinstance(conn.auth, NullAuth) - conn.authenticate_oidc_authorization_code(webbrowser_open=oidc_mock.webbrowser_open) - capabilities = conn.capabilities() - assert isinstance(conn.auth, BearerAuth) - assert capabilities.api_version() == "1.3.0" - assert capabilities.has_conformance("https://api.openeo.org/*/authentication/jwt") == True - assert conn.auth.bearer == oidc_mock.state["access_token"] - # TODO: check issuer ("iss") value in parsed jwt. this will require the example jwt to be formatted accordingly - assert refresh_token_store.mock_calls == [] -def test_authenticate_oidc_client_credentials(requests_mock): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_client_credentials(requests_mock, api_version_authentication_tests): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" client_secret = "$3cr3t" issuer = "https://oidc.test" @@ -1113,17 +1111,27 @@ def test_authenticate_oidc_client_credentials(requests_mock): client_id=client_id, client_secret=client_secret ) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] # Again but store refresh token conn.authenticate_oidc_client_credentials(client_id=client_id, client_secret=client_secret) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] -def test_authenticate_oidc_client_credentials_client_from_config(requests_mock, auth_config): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_client_credentials_client_from_config( + requests_mock, api_version_authentication_tests, auth_config +): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" client_secret = "$3cr3t" issuer = "https://oidc.test" @@ -1147,7 +1155,11 @@ def test_authenticate_oidc_client_credentials_client_from_config(requests_mock, assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_client_credentials() assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] @@ -1160,9 +1172,9 @@ def test_authenticate_oidc_client_credentials_client_from_config(requests_mock, ], ) def test_authenticate_oidc_client_credentials_client_from_env( - requests_mock, monkeypatch, env_provider_id, expected_provider_id + requests_mock, monkeypatch, env_provider_id, expected_provider_id, api_version_authentication_tests ): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" client_secret = "$3cr3t" monkeypatch.setenv("OPENEO_AUTH_CLIENT_ID", client_id) @@ -1192,7 +1204,11 @@ def test_authenticate_oidc_client_credentials_client_from_env( assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_client_credentials() assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == f"oidc/{expected_provider_id}/" + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == f"oidc/{expected_provider_id}/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] @@ -1221,8 +1237,9 @@ def test_authenticate_oidc_client_credentials_client_precedence( env_client_id, arg_client_id, expected_client_id, + api_version_authentication_tests, ): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_secret = "$3cr3t" if env_client_id: monkeypatch.setenv("OPENEO_AUTH_CLIENT_ID", env_client_id) @@ -1254,7 +1271,11 @@ def test_authenticate_oidc_client_credentials_client_precedence( client_id=arg_client_id, client_secret=client_secret if arg_client_id else None, provider_id=arg_provider_id ) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == f"oidc/{expected_provider_id}/" + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == f"oidc/{expected_provider_id}/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] @@ -1271,9 +1292,16 @@ def test_authenticate_oidc_client_credentials_client_precedence( ], ) def test_authenticate_oidc_client_credentials_client_multiple_provider_resolution( - requests_mock, monkeypatch, auth_config, provider_id_arg, provider_id_env, provider_id_conf, expected_provider_id + requests_mock, + monkeypatch, + auth_config, + provider_id_arg, + provider_id_env, + provider_id_conf, + expected_provider_id, + api_version_authentication_tests, ): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" client_secret = "$3cr3t" monkeypatch.setenv("OPENEO_AUTH_CLIENT_ID", client_id) @@ -1317,12 +1345,16 @@ def test_authenticate_oidc_client_credentials_client_multiple_provider_resolutio assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_client_credentials(provider_id=provider_id_arg) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == f"oidc/{expected_provider_id}/" + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == f"oidc/{expected_provider_id}/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] -def test_authenticate_oidc_resource_owner_password_credentials(requests_mock): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_resource_owner_password_credentials(requests_mock, api_version_authentication_tests): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" client_secret = "$3cr3t" username, password = "john", "j0hn" @@ -1348,7 +1380,11 @@ def test_authenticate_oidc_resource_owner_password_credentials(requests_mock): client_id=client_id, username=username, password=password, client_secret=client_secret ) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] # Again but store refresh token conn.authenticate_oidc_resource_owner_password_credentials( @@ -1356,14 +1392,21 @@ def test_authenticate_oidc_resource_owner_password_credentials(requests_mock): store_refresh_token=True ) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [ mock.call.set_refresh_token(client_id=client_id, issuer=issuer, refresh_token=oidc_mock.state["refresh_token"]) ] -def test_authenticate_oidc_resource_owner_password_credentials_client_from_config(requests_mock, auth_config): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_resource_owner_password_credentials_client_from_config( + requests_mock, auth_config, api_version_authentication_tests +): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) + client_id = "myclient" client_secret = "$3cr3t" username, password = "john", "j0hn" @@ -1390,7 +1433,11 @@ def test_authenticate_oidc_resource_owner_password_credentials_client_from_confi assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_resource_owner_password_credentials(username=username, password=password) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] @@ -1404,9 +1451,14 @@ def test_authenticate_oidc_resource_owner_password_credentials_client_from_confi ] ) def test_authenticate_oidc_device_flow_with_secret( - requests_mock, store_refresh_token, scopes_supported, expected_scopes, oidc_device_code_flow_checker + requests_mock, + store_refresh_token, + scopes_supported, + expected_scopes, + oidc_device_code_flow_checker, + api_version_authentication_tests, ): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" client_secret = "$3cr3t" issuer = "https://oidc.test" @@ -1436,7 +1488,11 @@ def test_authenticate_oidc_device_flow_with_secret( client_id=client_id, client_secret=client_secret, store_refresh_token=store_refresh_token ) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] if store_refresh_token: refresh_token = oidc_mock.state["refresh_token"] assert refresh_token_store.mock_calls == [ @@ -1447,9 +1503,9 @@ def test_authenticate_oidc_device_flow_with_secret( def test_authenticate_oidc_device_flow_with_secret_from_config( - requests_mock, auth_config, caplog, oidc_device_code_flow_checker + requests_mock, auth_config, caplog, oidc_device_code_flow_checker, api_version_authentication_tests ): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" client_secret = "$3cr3t" issuer = "https://oidc.test" @@ -1479,15 +1535,19 @@ def test_authenticate_oidc_device_flow_with_secret_from_config( with oidc_device_code_flow_checker(): conn.authenticate_oidc_device() assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] assert "No OIDC provider given, but only one available: 'oi'. Using that one." in caplog.text assert "Using client_id 'myclient' from config (provider 'oi')" in caplog.text @pytest.mark.slow -def test_authenticate_oidc_device_flow_no_support(requests_mock, auth_config): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_device_flow_no_support(requests_mock, auth_config, api_version_authentication_tests): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" client_secret = "$3cr3t" issuer = "https://oidc.test" @@ -1521,10 +1581,16 @@ def test_authenticate_oidc_device_flow_no_support(requests_mock, auth_config): (False, False), ]) def test_authenticate_oidc_device_flow_pkce_multiple_providers_no_given( - requests_mock, auth_config, caplog, use_pkce, expect_pkce, oidc_device_code_flow_checker + requests_mock, + auth_config, + caplog, + use_pkce, + expect_pkce, + oidc_device_code_flow_checker, + api_version_authentication_tests, ): """OIDC device flow + PKCE with multiple OIDC providers and none specified to use.""" - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" requests_mock.get(API_URL + 'credentials/oidc', json={ "providers": [ @@ -1556,7 +1622,11 @@ def test_authenticate_oidc_device_flow_pkce_multiple_providers_no_given( with oidc_device_code_flow_checker(url=f"{oidc_issuer}/dc"): conn.authenticate_oidc_device(client_id=client_id, use_pkce=use_pkce) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/fauth/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/fauth/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] assert "No OIDC provider given. Using first provider 'fauth' as advertised by backend." in caplog.text @@ -1567,10 +1637,16 @@ def test_authenticate_oidc_device_flow_pkce_multiple_providers_no_given( (False, False), ]) def test_authenticate_oidc_device_flow_pkce_multiple_provider_one_config_no_given( - requests_mock, auth_config, caplog, use_pkce, expect_pkce, oidc_device_code_flow_checker + requests_mock, + auth_config, + caplog, + use_pkce, + expect_pkce, + oidc_device_code_flow_checker, + api_version_authentication_tests, ): """OIDC device flow + PKCE with multiple OIDC providers, one in config and none specified to use.""" - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" requests_mock.get(API_URL + 'credentials/oidc', json={ "providers": [ @@ -1603,19 +1679,23 @@ def test_authenticate_oidc_device_flow_pkce_multiple_provider_one_config_no_give with oidc_device_code_flow_checker(url=f"{oidc_issuer}/dc"): conn.authenticate_oidc_device(use_pkce=use_pkce) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/fauth/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/fauth/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] assert "No OIDC provider given, but only one in config (for backend 'https://oeo.test/'): 'fauth'. Using that one." in caplog.text assert "Using client_id 'myclient' from config (provider 'fauth')" in caplog.text def test_authenticate_oidc_device_flow_pkce_multiple_provider_one_config_no_given_default_client( - requests_mock, auth_config, oidc_device_code_flow_checker + requests_mock, auth_config, oidc_device_code_flow_checker, api_version_authentication_tests ): """ OIDC device flow + default_clients + PKCE with multiple OIDC providers, one in config and none specified to use. """ - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) default_client_id = "dadefaultklient" requests_mock.get(API_URL + 'credentials/oidc', json={ "providers": [ @@ -1651,7 +1731,11 @@ def test_authenticate_oidc_device_flow_pkce_multiple_provider_one_config_no_give with oidc_device_code_flow_checker(url=f"{oidc_issuer}/dc"): conn.authenticate_oidc_device() assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/bauth/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/bauth/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] @@ -1676,11 +1760,12 @@ def test_authenticate_oidc_device_flow_pkce_multiple_provider_resolution( provider_id_conf, expected_provider, monkeypatch, + api_version_authentication_tests, ): """ OIDC device flow + default_clients + PKCE with multiple OIDC providers: provider resolution/precedence """ - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "klientid" requests_mock.get( API_URL + "credentials/oidc", @@ -1725,7 +1810,11 @@ def test_authenticate_oidc_device_flow_pkce_multiple_provider_resolution( with oidc_device_code_flow_checker(url=f"{oidc_issuer}/dc"): conn.authenticate_oidc_device(client_id=client_id, provider_id=provider_id_arg) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == f"oidc/{expected_provider}/" + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == f"oidc/{expected_provider}/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] @@ -1740,12 +1829,12 @@ def test_authenticate_oidc_device_flow_pkce_multiple_provider_resolution( ], ) def test_authenticate_oidc_device_flow_pkce_default_client_handling( - requests_mock, grant_types, use_pkce, expect_pkce, oidc_device_code_flow_checker + requests_mock, grant_types, use_pkce, expect_pkce, oidc_device_code_flow_checker, api_version_authentication_tests ): """ OIDC device authn grant + secret/PKCE/neither: default client grant_types handling """ - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) default_client_id = "dadefaultklient" oidc_issuer = "https://auth.test" requests_mock.get( @@ -1788,13 +1877,19 @@ def test_authenticate_oidc_device_flow_pkce_default_client_handling( with oidc_device_code_flow_checker(url=f"{oidc_issuer}/dc"): conn.authenticate_oidc_device(use_pkce=use_pkce) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/auth/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/auth/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [] -def test_authenticate_oidc_device_flow_pkce_store_refresh_token(requests_mock, oidc_device_code_flow_checker): +def test_authenticate_oidc_device_flow_pkce_store_refresh_token( + requests_mock, oidc_device_code_flow_checker, api_version_authentication_tests +): """OIDC device authn grant + PKCE + refresh token storage""" - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) default_client_id = "dadefaultklient" requests_mock.get(API_URL + 'credentials/oidc', json={ "providers": [ @@ -1829,7 +1924,11 @@ def test_authenticate_oidc_device_flow_pkce_store_refresh_token(requests_mock, o with oidc_device_code_flow_checker(url=f"{oidc_issuer}/dc"): conn.authenticate_oidc_device(store_refresh_token=True) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/auth/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/auth/" + oidc_mock.state["access_token"] assert refresh_token_store.mock_calls == [ mock.call.set_refresh_token( client_id=default_client_id, issuer="https://auth.test", @@ -1838,8 +1937,8 @@ def test_authenticate_oidc_device_flow_pkce_store_refresh_token(requests_mock, o ] -def test_authenticate_oidc_refresh_token(requests_mock): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_refresh_token(requests_mock, api_version_authentication_tests): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" refresh_token = "r3fr35h!" issuer = "https://oidc.test" @@ -1860,11 +1959,15 @@ def test_authenticate_oidc_refresh_token(requests_mock): assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_refresh_token(refresh_token=refresh_token, client_id=client_id) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] -def test_authenticate_oidc_refresh_token_expired(requests_mock): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_refresh_token_expired(requests_mock, api_version_authentication_tests): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" issuer = "https://oidc.test" requests_mock.get(API_URL + 'credentials/oidc', json={ @@ -1900,10 +2003,17 @@ def test_authenticate_oidc_refresh_token_expired(requests_mock): ], ) def test_authenticate_oidc_refresh_token_multiple_provider_resolution( - requests_mock, auth_config, provider_id_arg, provider_id_env, provider_id_conf, expected_provider, monkeypatch + requests_mock, + auth_config, + provider_id_arg, + provider_id_env, + provider_id_conf, + expected_provider, + monkeypatch, + api_version_authentication_tests, ): """Multiple OIDC Providers: provider resolution/precedence""" - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" refresh_token = "r3fr35h!" requests_mock.get( @@ -1946,12 +2056,18 @@ def test_authenticate_oidc_refresh_token_multiple_provider_resolution( assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_refresh_token(refresh_token=refresh_token, client_id=client_id, provider_id=provider_id_arg) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == f"oidc/{expected_provider}/" + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == f"oidc/{expected_provider}/" + oidc_mock.state["access_token"] @pytest.mark.parametrize("store_refresh_token", [True, False]) -def test_authenticate_oidc_auto_with_existing_refresh_token(requests_mock, refresh_token_store, store_refresh_token): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_auto_with_existing_refresh_token( + requests_mock, refresh_token_store, store_refresh_token, api_version_authentication_tests +): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" orig_refresh_token = "r3fr35h!" issuer = "https://oidc.test" @@ -1972,7 +2088,11 @@ def test_authenticate_oidc_auto_with_existing_refresh_token(requests_mock, refre assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc(client_id=client_id, store_refresh_token=store_refresh_token) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] new_refresh_token = refresh_token_store.get_refresh_token(issuer=issuer, client_id=client_id) assert new_refresh_token == orig_refresh_token @@ -1988,9 +2108,14 @@ def test_authenticate_oidc_auto_with_existing_refresh_token(requests_mock, refre ], ) def test_authenticate_oidc_auto_no_existing_refresh_token( - requests_mock, refresh_token_store, use_pkce, expect_pkce, oidc_device_code_flow_checker + requests_mock, + refresh_token_store, + use_pkce, + expect_pkce, + oidc_device_code_flow_checker, + api_version_authentication_tests, ): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" issuer = "https://oidc.test" requests_mock.get(API_URL + 'credentials/oidc', json={ @@ -2017,7 +2142,11 @@ def test_authenticate_oidc_auto_no_existing_refresh_token( with oidc_device_code_flow_checker(): conn.authenticate_oidc(client_id=client_id, use_pkce=use_pkce) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] assert [r["grant_type"] for r in oidc_mock.grant_request_history] == [ "urn:ietf:params:oauth:grant-type:device_code" ] @@ -2032,9 +2161,14 @@ def test_authenticate_oidc_auto_no_existing_refresh_token( ], ) def test_authenticate_oidc_auto_expired_refresh_token( - requests_mock, refresh_token_store, use_pkce, expect_pkce, oidc_device_code_flow_checker + requests_mock, + refresh_token_store, + use_pkce, + expect_pkce, + oidc_device_code_flow_checker, + api_version_authentication_tests, ): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" issuer = "https://oidc.test" requests_mock.get(API_URL + 'credentials/oidc', json={ @@ -2062,7 +2196,10 @@ def test_authenticate_oidc_auto_expired_refresh_token( with oidc_device_code_flow_checker(): conn.authenticate_oidc(client_id=client_id, use_pkce=use_pkce) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] assert [r["grant_type"] for r in oidc_mock.grant_request_history] == [ "refresh_token", "urn:ietf:params:oauth:grant-type:device_code", @@ -2079,7 +2216,7 @@ def test_authenticate_oidc_auto_expired_refresh_token( ], ) def test_authenticate_oidc_method_client_credentials_from_env( - requests_mock, monkeypatch, env_provider_id, expected_provider_id + requests_mock, monkeypatch, env_provider_id, expected_provider_id, api_version_authentication_tests ): client_id = "myclient" client_secret = "$3cr3t!" @@ -2088,7 +2225,7 @@ def test_authenticate_oidc_method_client_credentials_from_env( monkeypatch.setenv("OPENEO_AUTH_CLIENT_SECRET", client_secret) if env_provider_id: monkeypatch.setenv("OPENEO_AUTH_PROVIDER_ID", env_provider_id) - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) requests_mock.get( API_URL + "credentials/oidc", json={ @@ -2111,7 +2248,10 @@ def test_authenticate_oidc_method_client_credentials_from_env( assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc() assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == f"oidc/{expected_provider_id}/" + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == f"oidc/{expected_provider_id}/" + oidc_mock.state["access_token"] def _setup_get_me_handler(requests_mock, oidc_mock: OidcMock, token_invalid_status_code: int = 403): @@ -2144,9 +2284,9 @@ def get_me(request: requests.Request, context): ], ) def test_authenticate_oidc_auto_renew_expired_access_token_initial_refresh_token( - requests_mock, refresh_token_store, invalidate, token_invalid_status_code, caplog + requests_mock, refresh_token_store, invalidate, token_invalid_status_code, caplog, api_version_authentication_tests ): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" initial_refresh_token = "r3fr35h!" oidc_issuer = "https://oidc.test" @@ -2182,7 +2322,10 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_refresh_token refresh_token=initial_refresh_token, client_id=client_id, store_refresh_token=True ) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] # Just one "refresh_token" auth request so far assert [h["grant_type"] for h in oidc_mock.grant_request_history] == ["refresh_token"] access_token1 = oidc_mock.state["access_token"] @@ -2231,9 +2374,15 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_refresh_token ], ) def test_authenticate_oidc_auto_renew_expired_access_token_initial_device_code( - requests_mock, refresh_token_store, invalidate, token_invalid_status_code, caplog, oidc_device_code_flow_checker + requests_mock, + refresh_token_store, + invalidate, + token_invalid_status_code, + caplog, + oidc_device_code_flow_checker, + api_version_authentication_tests, ): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" oidc_issuer = "https://oidc.test" requests_mock.get( @@ -2273,7 +2422,11 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_device_code( with oidc_device_code_flow_checker(): conn.authenticate_oidc_device(client_id=client_id, use_pkce=True, store_refresh_token=True) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] # Just one "refresh_token" auth request so far assert [h["grant_type"] for h in oidc_mock.grant_request_history] == [ "urn:ietf:params:oauth:grant-type:device_code" @@ -2329,9 +2482,14 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_device_code( ], ) def test_authenticate_oidc_auto_renew_expired_access_token_invalid_refresh_token( - requests_mock, refresh_token_store, caplog, oidc_device_code_flow_checker, token_invalid_status_code + requests_mock, + refresh_token_store, + caplog, + oidc_device_code_flow_checker, + token_invalid_status_code, + api_version_authentication_tests, ): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" oidc_issuer = "https://oidc.test" requests_mock.get( @@ -2371,7 +2529,10 @@ def test_authenticate_oidc_auto_renew_expired_access_token_invalid_refresh_token with oidc_device_code_flow_checker(): conn.authenticate_oidc_device(client_id=client_id, use_pkce=True, store_refresh_token=True) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] # Just one "refresh_token" auth request so far assert [h["grant_type"] for h in oidc_mock.grant_request_history] == [ "urn:ietf:params:oauth:grant-type:device_code" @@ -2402,8 +2563,10 @@ def test_authenticate_oidc_auto_renew_expired_access_token_invalid_refresh_token assert "Failed to obtain new access token (grant 'refresh_token')" in caplog.text -def test_authenticate_oidc_auto_renew_expired_access_token_other_errors(requests_mock, caplog): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) +def test_authenticate_oidc_auto_renew_expired_access_token_other_errors( + requests_mock, caplog, api_version_authentication_tests +): + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" initial_refresh_token = "r3fr35h!" oidc_issuer = "https://oidc.test" @@ -2443,7 +2606,10 @@ def get_me(request: requests.Request, context): assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_refresh_token(refresh_token=initial_refresh_token, client_id=client_id) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == 'oidc/oi/' + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] # Do request that will fail with "Internal" error with pytest.raises(OpenEoApiError, match=re.escape("[500] Internal: Something's not right.")): @@ -2461,9 +2627,9 @@ def get_me(request: requests.Request, context): ], ) def test_authenticate_oidc_auto_renew_expired_access_token_initial_client_credentials( - requests_mock, refresh_token_store, invalidate, token_invalid_status_code, caplog + requests_mock, refresh_token_store, invalidate, token_invalid_status_code, caplog, api_version_authentication_tests ): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" client_secret = "$3cr3t" oidc_issuer = "https://oidc.test" @@ -2489,7 +2655,11 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_client_creden assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_client_credentials(client_id=client_id, client_secret=client_secret) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] # Just one "client_credentials" auth request so far assert [h["grant_type"] for h in oidc_mock.grant_request_history] == ["client_credentials"] @@ -2539,9 +2709,9 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_client_creden ], ) def test_authenticate_oidc_auto_renew_expired_access_token_initial_client_credentials_blocked( - requests_mock, refresh_token_store, caplog, token_invalid_status_code + requests_mock, refresh_token_store, caplog, token_invalid_status_code, api_version_authentication_tests ): - requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) client_id = "myclient" client_secret = "$3cr3t" issuer = "https://oidc.test" @@ -2567,7 +2737,11 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_client_creden assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_client_credentials(client_id=client_id, client_secret=client_secret) assert isinstance(conn.auth, BearerAuth) - assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] + if api_version_authentication_tests == "1.3.0": + # TODO: migth require future tests for the issuer encoded in the jwt + assert conn.auth.bearer == oidc_mock.state["access_token"] + else: + assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] # Just one "client_credentials" auth request so far assert [h["grant_type"] for h in oidc_mock.grant_request_history] == ["client_credentials"] access_token1 = oidc_mock.state["access_token"] @@ -5078,6 +5252,7 @@ def auto_validate(self, request) -> bool: @pytest.fixture def connection(self, api_version, requests_mock, api_capabilities, auto_validate) -> Connection: requests_mock.get(API_URL, json=build_capabilities(api_version=api_version, **api_capabilities)) + con = Connection(API_URL, **dict_no_none(auto_validate=auto_validate)) return con From 8f960e8b0f374c5dfada689c0105c2c6af5850ff Mon Sep 17 00:00:00 2001 From: niebl Date: Wed, 18 Feb 2026 15:51:11 +0100 Subject: [PATCH 33/40] use compiled regex to for has_conformance --- openeo/rest/capabilities.py | 6 +++--- tests/rest/test_connection.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openeo/rest/capabilities.py b/openeo/rest/capabilities.py index 25e9b8f46..42504a4be 100644 --- a/openeo/rest/capabilities.py +++ b/openeo/rest/capabilities.py @@ -8,7 +8,8 @@ __all__ = ["OpenEoCapabilities"] -CONFORMANCE_JWT_BEARER = "https://api.openeo.org/*/authentication/jwt" +CONFORMANCE_JWT_BEARER = re.compile(r"https://api\.openeo\.org/[^/]+/authentication/jwt") + class OpenEoCapabilities: """Container of the openEO capabilities document of an openEO backend.""" @@ -41,9 +42,8 @@ def api_version_check(self) -> ComparableVersion: def has_conformance(self, uri: str) -> bool: """Check if backend provides a given conformance string""" - uri = re.escape(uri).replace('\\*', '[^/]+') for conformance_uri in self.capabilities.get("conformsTo", []): - if re.match(uri, conformance_uri): + if re.fullmatch(uri, conformance_uri): return True return False diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index d9be0c0e1..501c1893e 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -39,6 +39,7 @@ from openeo.rest.auth.auth import BearerAuth, NullAuth from openeo.rest.auth.oidc import OidcException from openeo.rest.auth.testing import ABSENT, OidcMock, SimpleBasicAuthMocker +from openeo.rest.capabilities import CONFORMANCE_JWT_BEARER from openeo.rest.connection import ( DEFAULT_TIMEOUT, DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE, @@ -849,7 +850,7 @@ def test_authenticate_basic(requests_mock, api_version_authentication_tests, bas capabilities = conn.capabilities() assert isinstance(conn.auth, BearerAuth) if api_version_authentication_tests == "1.3.0": - assert capabilities.has_conformance("https://api.openeo.org/*/authentication/jwt") == True + assert capabilities.has_conformance(CONFORMANCE_JWT_BEARER) == True assert conn.auth.bearer == "6cc3570k3n" else: assert conn.auth.bearer == "basic//6cc3570k3n" From 33641c47a752967d26717bc9ae561366e5e2f380 Mon Sep 17 00:00:00 2001 From: niebl Date: Mon, 2 Mar 2026 10:35:38 +0100 Subject: [PATCH 34/40] wip: fix parametrized tests --- tests/rest/test_connection.py | 119 +++++++++++++++++++--------------- 1 file changed, 67 insertions(+), 52 deletions(-) diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index 501c1893e..53675338e 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -53,7 +53,7 @@ from openeo.rest.vectorcube import VectorCube from openeo.testing.stac import StacDummyBuilder from openeo.util import ContextTimer, deep_get, dict_no_none -from openeo.utils.version import ApiVersionException +from openeo.utils.version import ApiVersionException, ComparableVersion from .auth.test_cli import auth_config, refresh_token_store @@ -849,7 +849,7 @@ def test_authenticate_basic(requests_mock, api_version_authentication_tests, bas conn.authenticate_basic(username=basic_auth.username, password=basic_auth.password) capabilities = conn.capabilities() assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): assert capabilities.has_conformance(CONFORMANCE_JWT_BEARER) == True assert conn.auth.bearer == "6cc3570k3n" else: @@ -864,7 +864,7 @@ def test_authenticate_basic_from_config(requests_mock, api_version_authenticatio assert isinstance(conn.auth, NullAuth) conn.authenticate_basic() assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): assert conn.auth.bearer == "6cc3570k3n" else: assert conn.auth.bearer == "basic//6cc3570k3n" @@ -894,7 +894,7 @@ def test_authenticate_oidc_authorization_code_100_single_implicit( assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_authorization_code(client_id=client_id, webbrowser_open=oidc_mock.webbrowser_open) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -945,7 +945,7 @@ def test_authenticate_oidc_authorization_code_100_multiple_no_given_id( assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_authorization_code(client_id=client_id, webbrowser_open=oidc_mock.webbrowser_open) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -997,7 +997,7 @@ def test_authenticate_oidc_authorization_code_100_multiple_success(requests_mock client_id=client_id, provider_id="bauth", webbrowser_open=oidc_mock.webbrowser_open ) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1040,7 +1040,7 @@ def test_authenticate_oidc_auth_code_pkce_flow( client_id=client_id, webbrowser_open=oidc_mock.webbrowser_open, store_refresh_token=store_refresh_token ) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1080,7 +1080,7 @@ def test_authenticate_oidc_auth_code_pkce_flow_client_from_config( assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_authorization_code(webbrowser_open=oidc_mock.webbrowser_open) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1112,7 +1112,7 @@ def test_authenticate_oidc_client_credentials(requests_mock, api_version_authent client_id=client_id, client_secret=client_secret ) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1121,7 +1121,7 @@ def test_authenticate_oidc_client_credentials(requests_mock, api_version_authent # Again but store refresh token conn.authenticate_oidc_client_credentials(client_id=client_id, client_secret=client_secret) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1156,7 +1156,7 @@ def test_authenticate_oidc_client_credentials_client_from_config( assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_client_credentials() assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1205,7 +1205,7 @@ def test_authenticate_oidc_client_credentials_client_from_env( assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_client_credentials() assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1272,7 +1272,7 @@ def test_authenticate_oidc_client_credentials_client_precedence( client_id=arg_client_id, client_secret=client_secret if arg_client_id else None, provider_id=arg_provider_id ) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1346,7 +1346,7 @@ def test_authenticate_oidc_client_credentials_client_multiple_provider_resolutio assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_client_credentials(provider_id=provider_id_arg) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1381,7 +1381,7 @@ def test_authenticate_oidc_resource_owner_password_credentials(requests_mock, ap client_id=client_id, username=username, password=password, client_secret=client_secret ) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1393,7 +1393,7 @@ def test_authenticate_oidc_resource_owner_password_credentials(requests_mock, ap store_refresh_token=True ) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1434,7 +1434,7 @@ def test_authenticate_oidc_resource_owner_password_credentials_client_from_confi assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_resource_owner_password_credentials(username=username, password=password) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1489,7 +1489,7 @@ def test_authenticate_oidc_device_flow_with_secret( client_id=client_id, client_secret=client_secret, store_refresh_token=store_refresh_token ) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1536,7 +1536,7 @@ def test_authenticate_oidc_device_flow_with_secret_from_config( with oidc_device_code_flow_checker(): conn.authenticate_oidc_device() assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1623,7 +1623,7 @@ def test_authenticate_oidc_device_flow_pkce_multiple_providers_no_given( with oidc_device_code_flow_checker(url=f"{oidc_issuer}/dc"): conn.authenticate_oidc_device(client_id=client_id, use_pkce=use_pkce) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1680,7 +1680,7 @@ def test_authenticate_oidc_device_flow_pkce_multiple_provider_one_config_no_give with oidc_device_code_flow_checker(url=f"{oidc_issuer}/dc"): conn.authenticate_oidc_device(use_pkce=use_pkce) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1732,7 +1732,7 @@ def test_authenticate_oidc_device_flow_pkce_multiple_provider_one_config_no_give with oidc_device_code_flow_checker(url=f"{oidc_issuer}/dc"): conn.authenticate_oidc_device() assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1811,7 +1811,7 @@ def test_authenticate_oidc_device_flow_pkce_multiple_provider_resolution( with oidc_device_code_flow_checker(url=f"{oidc_issuer}/dc"): conn.authenticate_oidc_device(client_id=client_id, provider_id=provider_id_arg) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1878,7 +1878,7 @@ def test_authenticate_oidc_device_flow_pkce_default_client_handling( with oidc_device_code_flow_checker(url=f"{oidc_issuer}/dc"): conn.authenticate_oidc_device(use_pkce=use_pkce) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1925,7 +1925,7 @@ def test_authenticate_oidc_device_flow_pkce_store_refresh_token( with oidc_device_code_flow_checker(url=f"{oidc_issuer}/dc"): conn.authenticate_oidc_device(store_refresh_token=True) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -1960,7 +1960,7 @@ def test_authenticate_oidc_refresh_token(requests_mock, api_version_authenticati assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_refresh_token(refresh_token=refresh_token, client_id=client_id) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -2057,7 +2057,7 @@ def test_authenticate_oidc_refresh_token_multiple_provider_resolution( assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_refresh_token(refresh_token=refresh_token, client_id=client_id, provider_id=provider_id_arg) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -2089,7 +2089,7 @@ def test_authenticate_oidc_auto_with_existing_refresh_token( assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc(client_id=client_id, store_refresh_token=store_refresh_token) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -2143,7 +2143,7 @@ def test_authenticate_oidc_auto_no_existing_refresh_token( with oidc_device_code_flow_checker(): conn.authenticate_oidc(client_id=client_id, use_pkce=use_pkce) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -2197,7 +2197,7 @@ def test_authenticate_oidc_auto_expired_refresh_token( with oidc_device_code_flow_checker(): conn.authenticate_oidc(client_id=client_id, use_pkce=use_pkce) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): assert conn.auth.bearer == oidc_mock.state["access_token"] else: assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] @@ -2249,28 +2249,40 @@ def test_authenticate_oidc_method_client_credentials_from_env( assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc() assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): assert conn.auth.bearer == oidc_mock.state["access_token"] else: assert conn.auth.bearer == f"oidc/{expected_provider_id}/" + oidc_mock.state["access_token"] -def _setup_get_me_handler(requests_mock, oidc_mock: OidcMock, token_invalid_status_code: int = 403): +def _setup_get_me_handler(requests_mock, oidc_mock: OidcMock, token_invalid_status_code: int = 403, version="1.0.0"): def get_me(request: requests.Request, context): """handler for `GET /me` (with access_token checking)""" auth_header = request.headers["Authorization"] - oidc_provider, access_token = re.match(r"Bearer oidc/(?P

\w+)/(?P.*)", auth_header).group("p", "a") - try: - user_id = oidc_mock.validate_access_token(access_token)["user_id"] - except LookupError: - context.status_code = token_invalid_status_code - return {"code": "TokenInvalid", "message": "Authorization token has expired or is invalid."} - - return { - "user_id": user_id, - # Normally not part of `GET /me` response, but just for test/inspection purposes - "_used_oidc_provider": oidc_provider, - "_used_access_token": access_token, + if ComparableVersion(version) >= ComparableVersion("1.3.0"): + # TODO: parse jwt, also encode appropriate values in test JWT + access_token = re.match(r"Bearer (?P.*)", auth_header).group("a") + + try: + user_id = oidc_mock.validate_access_token(access_token)["user_id"] + except LookupError: + context.status_code = token_invalid_status_code + return {"code": "TokenInvalid", "message": "Authorization token has expired or is invalid."} + + return {"user_id": user_id, "_used_oidc_provider": "oi", "_used_access_token": access_token} + else: + oidc_provider, access_token = re.match(r"Bearer oidc/(?P

\w+)/(?P.*)", auth_header).group("p", "a") + try: + user_id = oidc_mock.validate_access_token(access_token)["user_id"] + except LookupError: + context.status_code = token_invalid_status_code + return {"code": "TokenInvalid", "message": "Authorization token has expired or is invalid."} + + return { + "user_id": user_id, + # Normally not part of `GET /me` response, but just for test/inspection purposes + "_used_oidc_provider": oidc_provider, + "_used_access_token": access_token, } requests_mock.get(API_URL + "me", json=get_me) @@ -2284,7 +2296,7 @@ def get_me(request: requests.Request, context): (True, 401), ], ) -def test_authenticate_oidc_auto_renew_expired_access_token_initial_refresh_token( +def test_authenticate_eoidc_auto_renew_expired_access_token_initial_refresh_token( requests_mock, refresh_token_store, invalidate, token_invalid_status_code, caplog, api_version_authentication_tests ): requests_mock.get(API_URL, json=build_capabilities(api_version=api_version_authentication_tests)) @@ -2312,7 +2324,10 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_refresh_token expected_fields={"refresh_token": initial_refresh_token} ) _setup_get_me_handler( - requests_mock=requests_mock, oidc_mock=oidc_mock, token_invalid_status_code=token_invalid_status_code + requests_mock=requests_mock, + oidc_mock=oidc_mock, + token_invalid_status_code=token_invalid_status_code, + version=api_version_authentication_tests, ) caplog.set_level(logging.INFO) @@ -2323,7 +2338,7 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_refresh_token refresh_token=initial_refresh_token, client_id=client_id, store_refresh_token=True ) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): assert conn.auth.bearer == oidc_mock.state["access_token"] else: assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] @@ -2423,7 +2438,7 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_device_code( with oidc_device_code_flow_checker(): conn.authenticate_oidc_device(client_id=client_id, use_pkce=True, store_refresh_token=True) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -2530,7 +2545,7 @@ def test_authenticate_oidc_auto_renew_expired_access_token_invalid_refresh_token with oidc_device_code_flow_checker(): conn.authenticate_oidc_device(client_id=client_id, use_pkce=True, store_refresh_token=True) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): assert conn.auth.bearer == oidc_mock.state["access_token"] else: assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] @@ -2607,7 +2622,7 @@ def get_me(request: requests.Request, context): assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_refresh_token(refresh_token=initial_refresh_token, client_id=client_id) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): assert conn.auth.bearer == oidc_mock.state["access_token"] else: assert conn.auth.bearer == "oidc/oi/" + oidc_mock.state["access_token"] @@ -2656,7 +2671,7 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_client_creden assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_client_credentials(client_id=client_id, client_secret=client_secret) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: @@ -2738,7 +2753,7 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_client_creden assert isinstance(conn.auth, NullAuth) conn.authenticate_oidc_client_credentials(client_id=client_id, client_secret=client_secret) assert isinstance(conn.auth, BearerAuth) - if api_version_authentication_tests == "1.3.0": + if ComparableVersion(api_version_authentication_tests) >= ComparableVersion("1.3.0"): # TODO: migth require future tests for the issuer encoded in the jwt assert conn.auth.bearer == oidc_mock.state["access_token"] else: From f9436918e85d53c584418e570048e5d0f369f8e9 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Mon, 2 Mar 2026 11:20:50 +0100 Subject: [PATCH 35/40] Fix parsing tokens --- tests/rest/test_connection.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index 53675338e..f96842ec3 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -2261,7 +2261,8 @@ def get_me(request: requests.Request, context): auth_header = request.headers["Authorization"] if ComparableVersion(version) >= ComparableVersion("1.3.0"): # TODO: parse jwt, also encode appropriate values in test JWT - access_token = re.match(r"Bearer (?P.*)", auth_header).group("a") + result = re.match(r"Bearer (?P.*)", auth_header) + access_token = result.group("a") if result else None try: user_id = oidc_mock.validate_access_token(access_token)["user_id"] @@ -2271,7 +2272,8 @@ def get_me(request: requests.Request, context): return {"user_id": user_id, "_used_oidc_provider": "oi", "_used_access_token": access_token} else: - oidc_provider, access_token = re.match(r"Bearer oidc/(?P

\w+)/(?P.*)", auth_header).group("p", "a") + result = re.match(r"Bearer oidc/(?P

\w+)/(?P.*)", auth_header) + oidc_provider, access_token = result.group("p", "a") if result else (None, None) try: user_id = oidc_mock.validate_access_token(access_token)["user_id"] except LookupError: From 9a810c1e49922af7ac4b79ddb99fffe7d79da563 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Mon, 2 Mar 2026 12:57:07 +0100 Subject: [PATCH 36/40] Add origin of token to BearerAuth class --- openeo/rest/auth/auth.py | 8 ++++---- openeo/rest/auth/testing.py | 2 +- openeo/rest/connection.py | 8 ++++---- tests/extra/artifacts/_s3sts/test_s3sts.py | 2 +- tests/rest/test_connection.py | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openeo/rest/auth/auth.py b/openeo/rest/auth/auth.py index cfa3329b4..ae617c4cb 100644 --- a/openeo/rest/auth/auth.py +++ b/openeo/rest/auth/auth.py @@ -29,8 +29,9 @@ class BearerAuth(OpenEoApiAuthBase): https://open-eo.github.io/openeo-api/apireference/#section/Authentication/Bearer """ - def __init__(self, bearer: str): + def __init__(self, bearer: str, origin: str): self.bearer = bearer + self.origin = origin def __call__(self, req: Request) -> Request: # Add bearer authorization header. @@ -42,12 +43,11 @@ class BasicBearerAuth(BearerAuth): """Bearer token for Basic Auth (openEO API 1.0.0 style)""" def __init__(self, access_token: str): - super().__init__(bearer="basic//{t}".format(t=access_token)) + super().__init__(bearer="basic//{t}".format(t=access_token), origin="basic") class OidcBearerAuth(BearerAuth): """Bearer token for OIDC Auth (openEO API 1.0.0 style)""" def __init__(self, provider_id: str, access_token: str): - super().__init__(bearer="oidc/{p}/{t}".format(p=provider_id, t=access_token)) - + super().__init__(bearer="oidc/{p}/{t}".format(p=provider_id, t=access_token), origin="oidc") diff --git a/openeo/rest/auth/testing.py b/openeo/rest/auth/testing.py index 930869f18..b5dd7fed4 100644 --- a/openeo/rest/auth/testing.py +++ b/openeo/rest/auth/testing.py @@ -281,7 +281,7 @@ def validate_access_token(self, access_token: str): raise LookupError("Invalid access token") def invalidate_access_token(self): - self.state["access_token"] = "***invalidated***" + self.state["access_token"] = None def get_request_history( self, url: Optional[str] = None, method: Optional[str] = None diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index 8a40c43a8..918ef4697 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -283,7 +283,7 @@ def authenticate_basic(self, username: Optional[str] = None, password: Optional[ # Switch to bearer based authentication in further requests. if jwt_conformance: - self.auth = BearerAuth(bearer=resp["access_token"]) + self.auth = BearerAuth(bearer=resp["access_token"], origin="basic") else: self.auth = BasicBearerAuth(access_token=resp["access_token"]) return self @@ -426,7 +426,7 @@ def _authenticate_oidc( # check for JWT bearer token conformance jwt_conformance = self.capabilities().has_conformance(CONFORMANCE_JWT_BEARER) if jwt_conformance: - self.auth = BearerAuth(bearer=token) + self.auth = BearerAuth(bearer=token, origin="oidc") else: self.auth = OidcBearerAuth(provider_id=provider_id, access_token=token) self._oidc_auth_renewer = oidc_auth_renewer @@ -738,7 +738,7 @@ def authenticate_bearer_token(self, bearer_token: str) -> Connection: .. versionadded:: 0.38.0 """ - self.auth = BearerAuth(bearer=bearer_token) + self.auth = BearerAuth(bearer=bearer_token, origin="unknown") self._oidc_auth_renewer = None return self @@ -748,7 +748,7 @@ def try_access_token_refresh(self, *, reason: Optional[str] = None) -> bool: Returns whether a new access token was obtained. """ reason = f" Reason: {reason}" if reason else "" - if isinstance(self.auth, OidcBearerAuth) and self._oidc_auth_renewer: + if isinstance(self.auth, BearerAuth) and self.auth.origin == "oidc" and self._oidc_auth_renewer: try: self._authenticate_oidc( authenticator=self._oidc_auth_renewer, diff --git a/tests/extra/artifacts/_s3sts/test_s3sts.py b/tests/extra/artifacts/_s3sts/test_s3sts.py index 4bfbec4cb..7ca03377b 100644 --- a/tests/extra/artifacts/_s3sts/test_s3sts.py +++ b/tests/extra/artifacts/_s3sts/test_s3sts.py @@ -93,7 +93,7 @@ def conn_with_s3sts_capabilities( requests_mock.get(API_URL, json={"api_version": "1.0.0", **extra_api_capabilities}) requests_mock.get(f"{API_URL}me", json={}) conn = Connection(API_URL) - conn.auth = BearerAuth("oidc/fake/token") + conn.auth = BearerAuth("oidc/fake/token", origin="oidc") yield conn diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index f96842ec3..dc1c697ba 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -276,7 +276,7 @@ def debug(request: requests.Request, context): requests_mock.get(url, text=debug) - con = RestApiConnection(api_root, auth=BearerAuth(secret)) + con = RestApiConnection(api_root, auth=BearerAuth(secret, origin="unknown")) res = con.get(url) assert "hello world" in res.text assert "User-Agent': 'openeo-python-client/" in res.text From a6d3cc2e5630753e4d9ce20bd322e01de4713d62 Mon Sep 17 00:00:00 2001 From: niebl Date: Mon, 2 Mar 2026 13:51:13 +0100 Subject: [PATCH 37/40] fix: tests depending on api version number in get_me_handler --- tests/rest/test_connection.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index dc1c697ba..d39128498 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -2536,7 +2536,10 @@ def test_authenticate_oidc_auto_renew_expired_access_token_invalid_refresh_token scopes_supported=["openid"], ) _setup_get_me_handler( - requests_mock=requests_mock, oidc_mock=oidc_mock, token_invalid_status_code=token_invalid_status_code + requests_mock=requests_mock, + oidc_mock=oidc_mock, + token_invalid_status_code=token_invalid_status_code, + version=api_version_authentication_tests, ) caplog.set_level(logging.INFO) @@ -2664,7 +2667,10 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_client_creden ) _setup_get_me_handler( - requests_mock=requests_mock, oidc_mock=oidc_mock, token_invalid_status_code=token_invalid_status_code + requests_mock=requests_mock, + oidc_mock=oidc_mock, + token_invalid_status_code=token_invalid_status_code, + version=api_version_authentication_tests, ) caplog.set_level(logging.INFO) @@ -2746,7 +2752,10 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_client_creden ) _setup_get_me_handler( - requests_mock=requests_mock, oidc_mock=oidc_mock, token_invalid_status_code=token_invalid_status_code + requests_mock=requests_mock, + oidc_mock=oidc_mock, + token_invalid_status_code=token_invalid_status_code, + version=api_version_authentication_tests, ) caplog.set_level(logging.INFO) From 4abf67e3e0086a1990611e2622c6fb6317ae2394 Mon Sep 17 00:00:00 2001 From: niebl Date: Mon, 2 Mar 2026 14:58:35 +0100 Subject: [PATCH 38/40] fix: remaining oidc test --- tests/rest/test_connection.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index d39128498..e7c289d32 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -2429,7 +2429,10 @@ def test_authenticate_oidc_auto_renew_expired_access_token_initial_device_code( scopes_supported=["openid"], ) _setup_get_me_handler( - requests_mock=requests_mock, oidc_mock=oidc_mock, token_invalid_status_code=token_invalid_status_code + requests_mock=requests_mock, + oidc_mock=oidc_mock, + token_invalid_status_code=token_invalid_status_code, + version=api_version_authentication_tests, ) caplog.set_level(logging.INFO) From 7c02e852af5d8fdc329ce18aec5530a2fe59c966 Mon Sep 17 00:00:00 2001 From: niebl Date: Mon, 2 Mar 2026 15:17:45 +0100 Subject: [PATCH 39/40] fix: conformance check --- tests/rest/test_testing.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/rest/test_testing.py b/tests/rest/test_testing.py index 1049d1af6..904e36dc8 100644 --- a/tests/rest/test_testing.py +++ b/tests/rest/test_testing.py @@ -4,6 +4,7 @@ from openeo.rest import OpenEoApiError from openeo.rest._testing import DummyBackend +from openeo.rest.connection import CONFORMANCE_JWT_BEARER @pytest.fixture @@ -124,5 +125,5 @@ def test_jwt_conformance(self, dummy_backend120, dummy_backend130): capabilities120 = dummy_backend120.connection.capabilities() capabilities130 = dummy_backend130.connection.capabilities() - assert capabilities120.has_conformance("https://api.openeo.org/*/authentication/jwt") == False - assert capabilities130.has_conformance("https://api.openeo.org/*/authentication/jwt") == True + assert capabilities120.has_conformance(CONFORMANCE_JWT_BEARER) == False + assert capabilities130.has_conformance(CONFORMANCE_JWT_BEARER) == True From 8ed91bfb0b6ab3eb63f3e35b0cfb8ea15c0aa889 Mon Sep 17 00:00:00 2001 From: niebl Date: Mon, 2 Mar 2026 15:25:04 +0100 Subject: [PATCH 40/40] fix: dummy backend conformance checking --- tests/rest/test_testing.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/rest/test_testing.py b/tests/rest/test_testing.py index 904e36dc8..0e4db9a24 100644 --- a/tests/rest/test_testing.py +++ b/tests/rest/test_testing.py @@ -16,7 +16,6 @@ def dummy_backend(requests_mock, con120): def dummy_backend130(requests_mock, con130): return DummyBackend(requests_mock=requests_mock, connection=con130) - DUMMY_PG_ADD35 = { "add35": {"process_id": "add", "arguments": {"x": 3, "y": 5}, "result": True}, } @@ -111,18 +110,15 @@ def test_setup_job_start_failure(self, dummy_backend): job.start() assert job.status() == "error" - # for better distinction within the following tests - dummy_backend120 = dummy_backend - - def test_version(self, dummy_backend120, dummy_backend130): - capabilities120 = dummy_backend120.connection.capabilities() + def test_version(self, dummy_backend, dummy_backend130): + capabilities120 = dummy_backend.connection.capabilities() capabilities130 = dummy_backend130.connection.capabilities() assert capabilities120.api_version() == "1.2.0" assert capabilities130.api_version() == "1.3.0" - def test_jwt_conformance(self, dummy_backend120, dummy_backend130): - capabilities120 = dummy_backend120.connection.capabilities() + def test_jwt_conformance(self, dummy_backend, dummy_backend130): + capabilities120 = dummy_backend.connection.capabilities() capabilities130 = dummy_backend130.connection.capabilities() assert capabilities120.has_conformance(CONFORMANCE_JWT_BEARER) == False