From c28a4556765e41ba6356dc87723ba940f63ec2e3 Mon Sep 17 00:00:00 2001 From: Stefaan Lippens Date: Wed, 24 Mar 2021 18:19:59 +0100 Subject: [PATCH] Streamline OIDC usage: when backend lists multiple OIDC providers, but user config just one: use that one related to #192 --- openeo/rest/connection.py | 13 +++-- tests/rest/test_connection.py | 95 ++++++++++++++++++++++++++++++++++- 2 files changed, 104 insertions(+), 4 deletions(-) diff --git a/openeo/rest/connection.py b/openeo/rest/connection.py index 5394b266b..80f1b0704 100644 --- a/openeo/rest/connection.py +++ b/openeo/rest/connection.py @@ -328,9 +328,16 @@ def _get_oidc_provider(self, provider_id: Union[str, None] = None) -> Tuple[str, # No provider id given, but there is only one anyway: we can handle that. provider_id, provider = providers.popitem() else: - raise OpenEoClientException("No provider_id given. Available: {p!r}.".format( - p=list(providers.keys())) - ) + # Check if there is a single provider in the config to use. + provider_configs = self._get_auth_config().get_oidc_provider_configs(backend=self._orig_url) + intersection = set(provider_configs.keys()).intersection(providers.keys()) + if len(intersection) == 1: + provider_id = intersection.pop() + provider = providers[provider_id] + else: + raise OpenEoClientException("No provider_id given but multiple to choose from: {p!r}.".format( + p=list(providers.keys())) + ) provider = OidcProviderInfo.from_dict(provider) else: # Per spec: '/credentials/oidc' will redirect to OpenID Connect discovery document diff --git a/tests/rest/test_connection.py b/tests/rest/test_connection.py index ab89e5230..cd991a309 100644 --- a/tests/rest/test_connection.py +++ b/tests/rest/test_connection.py @@ -443,7 +443,7 @@ def test_authenticate_oidc_100_multiple_no_id(requests_mock): # With all this set up, kick off the openid connect flow conn = Connection(API_URL) assert isinstance(conn.auth, NullAuth) - match = r"No provider_id given. Available: \[('fauth', 'bauth'|'bauth', 'fauth')\]\." + match = r"No provider_id given but multiple to choose from: \[('fauth', 'bauth'|'bauth', 'fauth')\]\." with pytest.raises(OpenEoClientException, match=match): conn.authenticate_OIDC(client_id=client_id, webbrowser_open=pytest.fail) @@ -795,6 +795,99 @@ def test_authenticate_oidc_device_flow_client_from_config(requests_mock, auth_co assert refresh_token_store.mock_calls == [] +def test_authenticate_oidc_device_flow_multiple_providers_no_given(requests_mock, auth_config): + """OIDC device flow with multiple OIDC providers and none specified to use.""" + requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + client_id = "myclient" + requests_mock.get(API_URL + 'credentials/oidc', json={ + "providers": [ + {"id": "fauth", "issuer": "https://fauth.test", "title": "Foo Auth", "scopes": ["openid", "w"]}, + {"id": "bauth", "issuer": "https://bauth.test", "title": "Bar Auth", "scopes": ["openid", "w"]}, + ] + }) + assert auth_config.load() == {} + + # With all this set up, kick off the openid connect flow + conn = Connection(API_URL) + assert isinstance(conn.auth, NullAuth) + match = r"No provider_id given but multiple to choose from: \[('fauth', 'bauth'|'bauth', 'fauth')\]\." + with pytest.raises(OpenEoClientException, match=match): + conn.authenticate_oidc_device(client_id=client_id) + + +def test_authenticate_oidc_device_flow_multiple_provider_one_config_no_given(requests_mock, auth_config): + """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"}) + client_id = "myclient" + requests_mock.get(API_URL + 'credentials/oidc', json={ + "providers": [ + {"id": "fauth", "issuer": "https://fauth.test", "title": "Foo", "scopes": ["openid"]}, + {"id": "bauth", "issuer": "https://bauth.test", "title": "Bar", "scopes": ["openid"]}, + ] + }) + oidc_mock = OidcMock( + requests_mock=requests_mock, + expected_grant_type="urn:ietf:params:oauth:grant-type:device_code", + expected_client_id=client_id, + expected_fields={ + "scope": "openid", "code_verifier": True, "code_challenge": True + }, + scopes_supported=["openid"], + oidc_discovery_url="https://fauth.test/.well-known/openid-configuration", + ) + assert auth_config.load() == {} + auth_config.set_oidc_client_config(backend=API_URL, provider_id="fauth", 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) + oidc_mock.state["device_code_callback_timeline"] = ["great success"] + conn.authenticate_oidc_device() + assert isinstance(conn.auth, BearerAuth) + assert conn.auth.bearer == 'oidc/fauth/' + oidc_mock.state["access_token"] + assert refresh_token_store.mock_calls == [] + + +def test_authenticate_oidc_device_flow_multiple_provider_one_config_no_given_default_client(requests_mock, auth_config): + """ + OIDC device flow + default_client + PKCE with multiple OIDC providers, one in config and none specified to use. + """ + requests_mock.get(API_URL, json={"api_version": "1.0.0"}) + default_client_id = "dadefaultklient" + requests_mock.get(API_URL + 'credentials/oidc', json={ + "providers": [ + {"id": "fauth", "issuer": "https://fauth.test", "title": "Foo", "scopes": ["openid"]}, + { + "id": "bauth", "issuer": "https://bauth.test", "title": "Bar", "scopes": ["openid"], + "default_client": {"id": default_client_id} + }, + ] + }) + oidc_mock = OidcMock( + requests_mock=requests_mock, + expected_grant_type="urn:ietf:params:oauth:grant-type:device_code", + expected_client_id=default_client_id, + expected_fields={ + "scope": "openid", "code_verifier": True, "code_challenge": True + }, + scopes_supported=["openid"], + oidc_discovery_url="https://bauth.test/.well-known/openid-configuration", + ) + assert auth_config.load() == {} + auth_config.set_oidc_client_config(backend=API_URL, provider_id="bauth", client_id=None) + + # 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) + oidc_mock.state["device_code_callback_timeline"] = ["great success"] + conn.authenticate_oidc_device() + assert isinstance(conn.auth, BearerAuth) + assert conn.auth.bearer == 'oidc/bauth/' + oidc_mock.state["access_token"] + assert refresh_token_store.mock_calls == [] + + def test_load_collection_arguments_040(requests_mock): requests_mock.get(API_URL, json={"api_version": "0.4.0"}) conn = Connection(API_URL)