Open-EO / openeo-python-client

Python client API for OpenEO
https://open-eo.github.io/openeo-python-client/
Apache License 2.0
147 stars 37 forks source link

authenticate_oidc: No client_id found (when OIDC provider does not list refresh_token grant type) #530

Open m-mohr opened 7 months ago

m-mohr commented 7 months ago

I have a backend with the following response to GET /credentials/oidc:

{
  "providers":[
    {
      "id":"google",
      "issuer":"https://accounts.google.com",
      "title":"Google",
      "description":"Login with your Google Earth Engine account.",
      "scopes":[
        "openid",
        "email",
        "https://www.googleapis.com/auth/earthengine"
      ],
      "default_clients":[
        {
          "id":"123.apps.googleusercontent.com",
          "grant_types":[
            "implicit"
          ],
          "redirect_urls":[
            "https://editor.openeo.org/",
            "http://localhost/"
          ]
        },
        {
          "id":"abc.apps.googleusercontent.com",
          "grant_types":[
            "urn:ietf:params:oauth:grant-type:device_code+pkce"
          ]
        }
      ]
    }
  ]
}

I'm running:

import openeo
connection = openeo.connect("http://localhost:8080")
connection.authenticate_oidc()

And get unexpectedly the following error:

OpenEoClientException: No client_id found.

Why is that? I think there's a reasonable default client ID available. (All client IDs are placeholders in the example.)

soxofaan commented 7 months ago

Looking into that. Do you have the full stack trace of that error?

soxofaan commented 7 months ago

first guess is that refresh_token grant type is not listed in your /credentials/oidc: response

soxofaan commented 7 months ago
"grant_types":[
            "urn:ietf:params:oauth:grant-type:device_code+pkce"
          ]

this indicates that you are only interested in device code grant, and not refresh token. So .authenticate_oidc() is not really recommended to use (as automated refresh token handling is one of it's key features).

Using .authenticate_oidc_device() should work against that /credentials/oidc setup however. Can you confirm that?

That being said, I think there is a case to be made to make .authenticate_oidc() work againsts your setup too

m-mohr commented 7 months ago

I've also tried it with urn:ietf:params:oauth:grant-type:device_code as default client and that lead to the same error. I need to check the other parts...

soxofaan commented 7 months ago

I've also tried it with urn:ietf:params:oauth:grant-type:device_code as default client and that lead to the same error.

indeed, with or without the +pkce doesn't matter here

m-mohr commented 7 months ago

The Google OIDC is a bit weird in that it supports refresh_token, but not the offline_access scope. I always thought they were somewhat related, maybe they are not though. Also fiddling around with it in https://github.com/Open-EO/openeo-web-editor/pull/319 - Maybe the primary issue is actually that we don't usually list offline_access in the scopes that are reported by the default clients? Should they do that?

Stack trace based on your last commit:

---------------------------------------------------------------------------
OpenEoClientException                     Traceback (most recent call last)
Cell In[9], line 1
----> 1 connection.authenticate_oidc()

File /mnt/c/Dev/openeo-python-client/openeo/rest/connection.py:716, in Connection.authenticate_oidc(self, provider_id, client_id, client_secret, store_refresh_token, use_pkce, display, max_poll_time)
    714 else:
    715     default_client_grant_check = lambda grants: (_g.DEVICE_CODE in grants or _g.DEVICE_CODE_PKCE in grants)
--> 716 provider_id, client_info = self._get_oidc_provider_and_client_info(
    717     provider_id=provider_id,
    718     client_id=client_id,
    719     client_secret=client_secret,
    720     default_client_grant_check=default_client_grant_check,
    721 )
    723 # Try refresh token first.
    724 refresh_token = self._get_refresh_token_store().get_refresh_token(
    725     issuer=client_info.provider.issuer,
    726     client_id=client_info.client_id
    727 )

File /mnt/c/Dev/openeo-python-client/openeo/rest/connection.py:454, in Connection._get_oidc_provider_and_client_info(self, provider_id, client_id, client_secret, default_client_grant_check)
    450         _log.info("Using default client_id {c!r} from OIDC provider {p!r} info.".format(
    451             c=client_id, p=provider_id
    452         ))
    453 if client_id is None:
--> 454     raise OpenEoClientException("No client_id found.")
    456 client_info = OidcClientInfo(client_id=client_id, client_secret=client_secret, provider=provider)
    458 return provider_id, client_info

OpenEoClientException: No client_id found.
soxofaan commented 7 months ago

Stack trace based on your last commit:

With that POC commit you should also disable refresh token storage connection.authenticate_oidc(store_refresh_token=False)

FYI The other workaround mentioned above (which should work with a normal release of python client) is to use

connection.authenticate_oidc_device()

Have you tried that already with your setup?

m-mohr commented 7 months ago

connection.authenticate_oidc(store_refresh_token=False)

This works indeed, thanks. A bit cumbersome that users have to specify a parameter.

But generally, I still struggle to understand whether the Google Auth supports PKCE for device_code or not. Some docy imply it does, but all requests ask me to provide a secret. So may not be supported.

Generally, I got it working in the Editor, but have still issues connecting through Google without a secret...

Have you tried that already with your setup?

Yes, works (with the caveats above). This also works: connection.authenticate_oidc_device(provider_id="google", client_id="abc.apps.googleusercontent.com", client_secret="..." use_pkce=False)

More to be investigated (also the relation between offline_access scope and refresh_token grant).