lepture / authlib

The ultimate Python library in building OAuth, OpenID Connect clients and servers. JWS,JWE,JWK,JWA,JWT included.
https://authlib.org/
BSD 3-Clause "New" or "Revised" License
4.39k stars 436 forks source link

Token refresh failed when using AsyncOAuth2Client with client credentials #650

Open radiophysicist opened 1 month ago

radiophysicist commented 1 month ago

Describe the bug

In case of using AsyncOAuth2Client with client credential I'm obtaining a TypeError exception after token expiration when making POST request.

Error Stacks

{
  "exc_type": "TypeError",
  "exc_value": "Invalid type for url.  Expected str or httpx.URL, got <class 'NoneType'>: None",
  "frames": [
    ...
    {
      "filename": "/app/support_integration/api_client/support.py",
      "line": "",
      "lineno": 55,
      "locals": {
        "data_inner": "\"{'personalNumber': 1951254, 'initialTemplateId': 18184, 'fieldQuestionAnswer': [\"+73",
        "file_size": "7557",
        "initial_template_id": "18184",
        "log": "\"<BoundLoggerFilteringAtNotset(context={'logger_name': 'support_integrati\"+774",
        "personal_number": "1951254",
        "request_data": "'{\\'objectBinding\\': \\'{\"personalNumber\": 1951254, \"initialTemplateId\": 18184, \"fiel'+238",
        "self": "'<support_integration.api_client.support.SupportAPIClient object at 0x7f1'+10",
        "template_question_answers": "[SupportAPIQA(question='Укажите номер магазина SAP/Торг.ERP:', answer='4063')]",
        "url": "https://support.stage.api.xxx.xxx/api/external/utp/appeal",
        "xls_file": "<tempfile._TemporaryFileWrapper object at 0x7f1525f29e90>"
      },
      "name": "create_ticket"
    },
    {
      "filename": "/venv/lib/python3.11/site-packages/httpx/_client.py",
      "line": "",
      "lineno": 1892,
      "locals": {
        "auth": "<httpx._client.UseClientDefault object at 0x7f152ab926d0>",
        "content": "None",
        "cookies": "None",
        "data": "'{\\'objectBinding\\': \\'{\"personalNumber\": 1951254, \"initialTemplateId\": 18184, \"fiel'+238",
        "extensions": "None",
        "files": "{'file': <tempfile._TemporaryFileWrapper object at 0x7f1525f29e90>}",
        "follow_redirects": "<httpx._client.UseClientDefault object at 0x7f152ab926d0>",
        "headers": "None",
        "json": "None",
        "params": "None",
        "self": "'<support_integration.api_client.support.SupportAPIClient object at 0x7f1'+10",
        "timeout": "10",
        "url": "https://support.stage.api.xxx.xxx/api/external/utp/appeal"
      },
      "name": "post"
    },
    {
      "filename": "/venv/lib/python3.11/site-packages/authlib/integrations/httpx_client/oauth2_client.py",
      "line": "",
      "lineno": 86,
      "locals": {
        "__class__": "<class 'authlib.integrations.httpx_client.oauth2_client.AsyncOAuth2Client'>",
        "auth": "<httpx._client.UseClientDefault object at 0x7f152ab926d0>",
        "kwargs": "'{\\'content\\': None, \\'data\\': {\\'objectBinding\\': \\'{\"personalNumber\": 1951254, \"initia'+521",
        "method": "POST",
        "self": "'<support_integration.api_client.support.SupportAPIClient object at 0x7f1'+10",
        "url": "https://support.stage.api.xxx.xxx/api/external/utp/appeal",
        "withhold_token": "False"
      },
      "name": "request"
    },
    {
      "filename": "/venv/lib/python3.11/site-packages/authlib/integrations/httpx_client/oauth2_client.py",
      "line": "",
      "lineno": 116,
      "locals": {
        "access_token": "'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJVZFBBcjhTdmoyYURjQ21ES2l6NTAw'+1245",
        "refresh_token": "None",
        "self": "'<support_integration.api_client.support.SupportAPIClient object at 0x7f1'+10",
        "token": "\"{'access_token': 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJVZFBBcjhTdm\"+1409",
        "url": "None"
      },
      "name": "ensure_active_token"
    },
    {
      "filename": "/venv/lib/python3.11/site-packages/authlib/integrations/httpx_client/oauth2_client.py",
      "line": "",
      "lineno": 125,
      "locals": {
        "auth": "'<authlib.integrations.httpx_client.oauth2_client.OAuth2ClientAuth object at 0x7f'+11",
        "body": "grant_type=client_credentials&scope=email",
        "headers": "\"{'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencode\"+17",
        "kwargs": "{}",
        "method": "POST",
        "self": "'<support_integration.api_client.support.SupportAPIClient object at 0x7f1'+10",
        "url": "None"
      },
      "name": "_fetch_token"
    },
    {
      "filename": "/venv/lib/python3.11/site-packages/httpx/_client.py",
      "line": "",
      "lineno": 1892,
      "locals": {
        "auth": "'<authlib.integrations.httpx_client.oauth2_client.OAuth2ClientAuth object at 0x7f'+11",
        "content": "None",
        "cookies": "None",
        "data": "{'grant_type': 'client_credentials', 'scope': 'email'}",
        "extensions": "None",
        "files": "None",
        "follow_redirects": "<httpx._client.UseClientDefault object at 0x7f152ab926d0>",
        "headers": "\"{'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencode\"+17",
        "json": "None",
        "params": "None",
        "self": "'<support_integration.api_client.support.SupportAPIClient object at 0x7f1'+10",
        "timeout": "<httpx._client.UseClientDefault object at 0x7f152ab926d0>",
        "url": "None"
      },
      "name": "post"
    },
    {
      "filename": "/venv/lib/python3.11/site-packages/authlib/integrations/httpx_client/oauth2_client.py",
      "line": "",
      "lineno": 90,
      "locals": {
        "__class__": "<class 'authlib.integrations.httpx_client.oauth2_client.AsyncOAuth2Client'>",
        "auth": "'<authlib.integrations.httpx_client.oauth2_client.OAuth2ClientAuth object at 0x7f'+11",
        "kwargs": "\"{'content': None, 'data': {'grant_type': 'client_credentials', 'scope': 'email'}\"+342",
        "method": "POST",
        "self": "'<support_integration.api_client.support.SupportAPIClient object at 0x7f1'+10",
        "url": "None",
        "withhold_token": "False"
      },
      "name": "request"
    },
    {
      "filename": "/venv/lib/python3.11/site-packages/httpx/_client.py",
      "line": "",
      "lineno": 1561,
      "locals": {
        "auth": "'<authlib.integrations.httpx_client.oauth2_client.OAuth2ClientAuth object at 0x7f'+11",
        "content": "None",
        "cookies": "None",
        "data": "{'grant_type': 'client_credentials', 'scope': 'email'}",
        "extensions": "None",
        "files": "None",
        "follow_redirects": "<httpx._client.UseClientDefault object at 0x7f152ab926d0>",
        "headers": "\"{'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencode\"+17",
        "json": "None",
        "method": "POST",
        "params": "None",
        "self": "'<support_integration.api_client.support.SupportAPIClient object at 0x7f1'+10",
        "timeout": "<httpx._client.UseClientDefault object at 0x7f152ab926d0>",
        "url": "None"
      },
      "name": "request"
    },
    {
      "filename": "/venv/lib/python3.11/site-packages/httpx/_client.py",
      "line": "",
      "lineno": 345,
      "locals": {
        "content": "None",
        "cookies": "None",
        "data": "{'grant_type': 'client_credentials', 'scope': 'email'}",
        "extensions": "None",
        "files": "None",
        "headers": "\"{'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencode\"+17",
        "json": "None",
        "method": "POST",
        "params": "None",
        "self": "'<support_integration.api_client.support.SupportAPIClient object at 0x7f1'+10",
        "timeout": "<httpx._client.UseClientDefault object at 0x7f152ab926d0>",
        "url": "None"
      },
      "name": "build_request"
    },
    {
      "filename": "/venv/lib/python3.11/site-packages/httpx/_client.py",
      "line": "",
      "lineno": 375,
      "locals": {
        "self": "'<support_integration.api_client.support.SupportAPIClient object at 0x7f1'+10",
        "url": "None"
      },
      "name": "_merge_url"
    },
    {
      "filename": "/venv/lib/python3.11/site-packages/httpx/_urls.py",
      "line": "",
      "lineno": 119,
      "locals": {
        "kwargs": "{}",
        "self": "<repr-error \"'URL' object has no attribute '_uri_reference'\">",
        "url": "None"
      },
      "name": "__init__"
    }
  ],
  "is_cause": false,
  "syntax_error": null
}

To Reproduce My code simplified:

class BaseAPIClient(AsyncOAuth2Client):
    """
    Base class for client of API published via integration platform.
    Authorization is OAUTH 2.0 with client ID
    """

    def __init__(
        self,
        client_id: str,
        client_secret: str,
        oauth2_token_url: str,
        api_base_url: str,
    ):
        super().__init__(
            client_id=client_id,
            client_secret=client_secret,
            scope="email",
        )
        self._token_url = oauth2_token_url
        self._api_base_url = api_base_url

    async def start(self) -> None:
        if not self.token:
            try:
                await self.fetch_token(url=self._token_url)
            except Exception:
                self._handle_exception(sys.exc_info(), in_auth=True)

            self.headers["Authorization"] = f"Bearer {self.token['access_token']}"
            logger.info("Authorized successfully", token_url=self._token_url)

    def _handle_exception(self, exc_info: tuple, in_auth: bool = False) -> None:
        exc_type, exc = exc_info[:2]
        if exc_type == OAuthError or in_auth:
            msg = "Failed to authorize on API platform"
            logger.error(msg, token_url=self._token_url, exc_info=exc_info)
            raise BaseAPIClientError(msg) from exc

        msg = "Unexpected API error"
        logger.error(msg, exc_info=exc_info)
        raise BaseAPIClientError(msg) from exc

        cc = SupportAPIClient(
            client_id=settings.api_reg_client_id,
            client_secret=settings.api_reg_client_secret,
            oauth2_token_url=settings.api_reg_token_url,
        )
       await cc.start()
      # Wait some time for token to expire
      await cc.post(...)  # <-- Crashes

Expected behavior

Expected the token to be refreshed / re-obtained and than request complete successfully

Environment:

Additional context

The problem seems to originate from line: https://github.com/lepture/authlib/blob/610622e54b6cbc810ad9fda97569f13401614348/authlib/oauth2/client.py#L275 In case of client credentials there are both no token_endpoint in metadata and url in token. Token data:

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJVZFBBcjhTdmoyYURjQ21ES2l6NTAwUFBmTnB0X0hRdE5FWldLdWlKLV9rIn0.eyJleHAiOjE3MTU3NjMxMDYsImlhdCI6MTcxNTc2MjIwNiwianRpIjoiMGNjNTg0ZTAtM2QyMy00YWQ1LTg2MDItMGU1NWVlYmNjN2Y5IiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay54NS5ydS9yZWFsbXMvQ1NJUCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI3YTA3MjA1OC0zOGNiLTQ3YTAtYmY3Yi1jMzNjODJiYmU2MzgiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJwcm9kdWN0X3JlcHJlc2VudGF0aW9uX3N5c3RlbSIsImFjciI6IjEiLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsiZGVmYXVsdC1yb2xlcy1jc2lwIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiY2xpZW50SG9zdCI6IjEwMC4xMjYuMzIuMTgzIiwicHJlZmVycmVkX3VzZXJuYW1lIjoic2VydmljZS1hY2NvdW50LXByb2R1Y3RfcmVwcmVzZW50YXRpb25fc3lzdGVtIiwiY2xpZW50QWRkcmVzcyI6IjEwMC4xMjYuMzIuMTgzIiwiY2xpZW50X2lkIjoicHJvZHVjdF9yZXByZXNlbnRhdGlvbl9zeXN0ZW0ifQ.g8cwDaPrS-ya1CxwgqUq3pKYMti2Ruy-aO3hs9sopQAx1cPvAsEV_-S7l95BW3ww1crO6RcS57OuQhUZ97tenpOlHcGjBXspDwB8RRRgzkUGDIAUY4HX_IG5UEfqz4VFeuVqWMNvKZXtskFRRdqnBIFF_QJlTODZeT-FrIW0voui6iH-OPCrH4e-0E4xpnCSa4inxDBIK9rAoEE4A78EhakPqODXVxriykqtUnHrqf2FbR9l6zJ4KjpFpFUeiTHDQHgHSyP6MnbVnnewDSbMdmhsvbFDO0IwoSHd6pywuc8yvLH-3SXWM-QnZn4KXePZEFz_0l7d9UUHU5X0ypnZFg",
    "expires_at": 1715763106,
    "expires_in": 900,
    "not-before-policy": 0,
    "refresh_expires_in": 0,
    "scope": "profile email",
    "token_type": "Bearer"
  }

Metadata:

{
    "grant_type": "client_credentials"
}

Is it intended, a bug or my client misconfiguration?