requests / requests-oauthlib

OAuthlib support for Python-Requests!
https://requests-oauthlib.readthedocs.org/
ISC License
1.71k stars 421 forks source link

Oauth2Session.fetch_token() : Invalid client_id for client_credentials grant with BackendApplicationClient #479

Open carolinarsm opened 2 years ago

carolinarsm commented 2 years ago

tl;dr: when implementing oauth2 with client_credentials grant type, setting include_client_id=True in fetch_token works for the intended purposes.

I'm implementing a server-to-server client as specified here: https://hl7.org/fhir/uv/bulkdata/authorization/index.html I don't think many apps use this workflow, but maybe this info could help someone.

For an oauth2 backend confidential client with grant type client_credentials (i.e., RS384 JWT), setting include_client_id=True when calling fetch_token works for the intended purpose, however, this is a bit misleading, since the client id is encoded in the signed JWT that's added to the body for POST request, and commonly client_id is not sent explicitly.

I was a bit confused with fetch_token, because of this:

param include_client_id: Should the request body include the
                                  `client_id` parameter. Default is `None`,
                                  which will attempt to autodetect. This can be
                                  forced to always include (True) or never
                                  include (False).

I intuitively used include_client_id=False because of what I mentioned in the beginning, but mainly because it matches the oauthlib.oauth2.rfc6749.clients definition for BackendApplicationClient, where the default in BackendApplicationClient.prepare_request_body is include_client_id=False.

What is happening when using include_client_id=None or include_client_id=False (pseudocode)

from requests_oauthlib import OAuth2Session
from oauthlib.oauth2 import BackendApplicationClient
from authlib.jose import jwt  # this is from authlib

# Client and Session
client = BackendApplicationClient(client_id=preregistered_backend_client_id)
session = OAuth2Session(client=client)

# Generate a signature with our preregistered credentials, and create a signed JWT
signed_jwt = jwt.encode(header=jwt_header, payload=jwt_payload, key=my_private_key).decode()

token_request_body = {
            'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 
            'client_assertion': signed_jwt
        } 

encoded_jwt_body = client.prepare_request_body(body=urlencode(token_request_body))

# Fetching access_token:
token_dict = session.fetch_token(token_url, body=encoded_jwt_body)
>>>  oauthlib.oauth2.rfc6749.errors.InvalidClientIdError: (invalid_request) Invalid client_id parameter value

Since in our call we are not passing an auth and include_client_id is not True, the logic in requests_oauthlib.OAuth2Session.fetch_token results in

if auth is not None: 
            if include_client_id is None:
                include_client_id = False

else: 
    if include_client_id is not True:
        if client_id:
                    log.debug(
                        'Encoding `client_id` "%s" with `client_secret` '
                        "as Basic auth credentials.",
                        client_id,
                    )
                    client_secret = client_secret if client_secret is not None else ""
                    auth = requests.auth.HTTPBasicAuth(client_id, client_secret)

The encoded body we passed to fetch_token is added to request_kwargs, wich is correct and should be enough, however, the faulty auth is also in the request:

if method.upper() == "POST":
            request_kwargs["params" if force_querystring else "data"] = dict(
                urldecode(body)
            )

r = self.request(
            method=method,
            url=token_url,
            timeout=timeout,
            headers=headers,
            **auth=auth**, # ---> Nope
            verify=verify,
            proxies=proxies,
            **request_kwargs
        )

This request should work correctly with the token_url and **kwargs containing the encoded body, but maybe I'm no seeing something? Any feedback welcome.

-Cheers