@@ -228,6 +228,7 @@ def __init__(
228228 callback_handler : Callable [[], Awaitable [tuple [str , str | None ]]] | None = None ,
229229 timeout : float = 300.0 ,
230230 client_metadata_url : str | None = None ,
231+ fixed_client_info : OAuthClientInformationFull | None = None ,
231232 ):
232233 """Initialize OAuth2 authentication.
233234
@@ -262,6 +263,11 @@ def __init__(
262263 timeout = timeout ,
263264 client_metadata_url = client_metadata_url ,
264265 )
266+ self ._fixed_client_info = fixed_client_info
267+ if fixed_client_info is not None :
268+ # In multi-protocol OAuth flow, we may drive oauth_401_flow_generator directly
269+ # without calling _initialize(); ensure client_info is available upfront.
270+ self .context .client_info = fixed_client_info
265271 self ._initialized = False
266272
267273 async def _handle_protected_resource_response (self , response : httpx .Response ) -> bool :
@@ -297,6 +303,10 @@ async def _handle_protected_resource_response(self, response: httpx.Response) ->
297303
298304 async def _perform_authorization (self ) -> httpx .Request :
299305 """Perform the authorization flow."""
306+ grant_types = set (self .context .client_metadata .grant_types or [])
307+ if "client_credentials" in grant_types :
308+ token_request = await self ._exchange_token_client_credentials ()
309+ return token_request
300310 auth_code , code_verifier = await self ._perform_authorization_code_grant ()
301311 token_request = await self ._exchange_token_authorization_code (auth_code , code_verifier )
302312 return token_request
@@ -362,6 +372,31 @@ def _get_token_endpoint(self) -> str:
362372 token_url = urljoin (auth_base_url , "/token" )
363373 return token_url
364374
375+ async def _exchange_token_client_credentials (self ) -> httpx .Request :
376+ """Build token exchange request for client_credentials flow."""
377+ if not self .context .client_info :
378+ raise OAuthFlowError ("Missing client info for client_credentials flow" )
379+
380+ token_url = self ._get_token_endpoint ()
381+ token_data : dict [str , str ] = {
382+ "grant_type" : "client_credentials" ,
383+ }
384+
385+ # Some servers require explicit client_id in the form body (especially for client_secret_post).
386+ if self .context .client_info .client_id :
387+ token_data ["client_id" ] = self .context .client_info .client_id
388+
389+ # Only include resource param if conditions are met
390+ if self .context .should_include_resource_param (self .context .protocol_version ):
391+ token_data ["resource" ] = self .context .get_resource_url () # RFC 8707
392+
393+ if self .context .client_metadata .scope :
394+ token_data ["scope" ] = self .context .client_metadata .scope
395+
396+ headers = {"Content-Type" : "application/x-www-form-urlencoded" }
397+ token_data , headers = self .context .prepare_token_auth (token_data , headers )
398+ return httpx .Request ("POST" , token_url , data = token_data , headers = headers )
399+
365400 async def _exchange_token_authorization_code (
366401 self , auth_code : str , code_verifier : str , * , token_data : dict [str , Any ] | None = {}
367402 ) -> httpx .Request :
@@ -543,15 +578,14 @@ async def run_authentication(
543578 self .context .client_info = client_information
544579 await self .context .storage .set_client_info (client_information )
545580
546- auth_code , code_verifier = await self ._perform_authorization_code_grant ()
547- token_request = await self ._exchange_token_authorization_code (auth_code , code_verifier )
581+ token_request = await self ._perform_authorization ()
548582 token_response = await http_client .send (token_request )
549583 await self ._handle_token_response (token_response )
550584
551585 async def _initialize (self ) -> None : # pragma: no cover
552586 """Load stored tokens and client info."""
553587 self .context .current_tokens = await self .context .storage .get_tokens ()
554- self .context .client_info = await self .context .storage .get_client_info ()
588+ self .context .client_info = self . _fixed_client_info or await self .context .storage .get_client_info ()
555589 self ._initialized = True
556590
557591 def _add_auth_header (self , request : httpx .Request ) -> None :
0 commit comments