From 26422dc5f658be3e6074a2ee391f22a374db717a Mon Sep 17 00:00:00 2001 From: niebl Date: Thu, 5 Feb 2026 17:48:04 +0100 Subject: [PATCH 01/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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"