@@ -241,7 +241,7 @@ async def _prepare_runtime_credentials(credentials: Dict[str, Any], flow_name: s
241241 logger .warning ("Failed to prepare runtime OAuth credentials for %s flow: %s" , flow_name , exc )
242242 return credentials
243243
244- async def _post_token_request (self , url : str , data : Any , ca_certificate : Optional [str ] = None , client_cert : Optional [str ] = None , client_key : Optional [str ] = None ) -> httpx .Response :
244+ async def _post_token_request (self , url : str , data : Any , ca_certificate : Optional [str ] = None , client_cert : Optional [str ] = None , client_key : Optional [str ] = None , headers : Optional [ dict ] = None ) -> httpx .Response :
245245 """POST to a token endpoint, using a custom SSL context when CA certs are provided.
246246
247247 When ``ca_certificate`` is supplied, an isolated ``httpx.AsyncClient``
@@ -256,16 +256,17 @@ async def _post_token_request(self, url: str, data: Any, ca_certificate: Optiona
256256 ca_certificate: Optional PEM-encoded CA certificate.
257257 client_cert: Optional client certificate for mTLS.
258258 client_key: Optional client private key for mTLS.
259+ headers: Optional extra headers (e.g. Authorization: Basic for client_secret_basic).
259260
260261 Returns:
261262 The HTTP response from the token endpoint.
262263 """
263264 if ca_certificate :
264265 ssl_context = get_cached_ssl_context (ca_certificate , client_cert = client_cert , client_key = client_key )
265266 async with httpx .AsyncClient (verify = ssl_context ) as client :
266- return await client .post (url , data = data , timeout = self .request_timeout )
267+ return await client .post (url , data = data , headers = headers , timeout = self .request_timeout )
267268 client = await self ._get_client ()
268- return await client .post (url , data = data , timeout = self .request_timeout )
269+ return await client .post (url , data = data , headers = headers , timeout = self .request_timeout )
269270
270271 # Keys whose values must never be echoed in error messages or logs.
271272 _SENSITIVE_TOKEN_KEYS = frozenset ({"access_token" , "refresh_token" , "id_token" , "client_secret" , "password" })
@@ -1408,17 +1409,31 @@ async def _exchange_code_for_tokens(
14081409 token_url = runtime_credentials ["token_url" ]
14091410 redirect_uri = runtime_credentials ["redirect_uri" ]
14101411
1412+ # Determine token endpoint authentication method (RFC 6749 Section 2.3)
1413+ # - "client_secret_post" (default): client_id and client_secret in POST body
1414+ # - "client_secret_basic": credentials in Authorization: Basic header
1415+ token_auth_method = credentials .get ("token_endpoint_auth_method" , "client_secret_post" )
1416+ use_basic_auth = token_auth_method == "client_secret_basic" and client_secret
1417+
1418+ # Build HTTP Basic Auth header if required by the provider
1419+ auth_header = None
1420+ if use_basic_auth :
1421+ basic_credentials = base64 .b64encode (f"{ client_id } :{ client_secret } " .encode ()).decode ()
1422+ auth_header = {"Authorization" : f"Basic { basic_credentials } " }
1423+
14111424 # Prepare token exchange data
14121425 token_data = {
14131426 "grant_type" : "authorization_code" ,
14141427 "code" : code ,
14151428 "redirect_uri" : redirect_uri ,
1416- "client_id" : client_id ,
14171429 }
14181430
1419- # Only include client_secret if present (public clients don't have secrets)
1420- if client_secret :
1421- token_data ["client_secret" ] = client_secret
1431+ # Include client credentials in POST body only when not using Basic auth
1432+ if not use_basic_auth :
1433+ token_data ["client_id" ] = client_id
1434+ # Only include client_secret if present (public clients don't have secrets)
1435+ if client_secret :
1436+ token_data ["client_secret" ] = client_secret
14221437
14231438 # Add PKCE code_verifier if present (RFC 7636)
14241439 if code_verifier :
@@ -1442,7 +1457,7 @@ async def _exchange_code_for_tokens(
14421457 # Exchange code for token with retries
14431458 for attempt in range (self .max_retries ):
14441459 try :
1445- response = await self ._post_token_request (token_url , token_data , ca_certificate = ca_certificate , client_cert = client_cert , client_key = client_key )
1460+ response = await self ._post_token_request (token_url , token_data , ca_certificate = ca_certificate , client_cert = client_cert , client_key = client_key , headers = auth_header )
14461461 response .raise_for_status ()
14471462
14481463 token_response = self ._parse_token_response (response )
@@ -1502,16 +1517,28 @@ async def refresh_token(
15021517 if not client_id :
15031518 raise OAuthError ("No client_id configured for OAuth provider" )
15041519
1520+ # Determine token endpoint authentication method (RFC 6749 Section 2.3)
1521+ token_auth_method = credentials .get ("token_endpoint_auth_method" , "client_secret_post" )
1522+ use_basic_auth = token_auth_method == "client_secret_basic" and client_secret
1523+
1524+ # Build HTTP Basic Auth header if required by the provider
1525+ auth_header = None
1526+ if use_basic_auth :
1527+ basic_credentials = base64 .b64encode (f"{ client_id } :{ client_secret } " .encode ()).decode ()
1528+ auth_header = {"Authorization" : f"Basic { basic_credentials } " }
1529+
15051530 # Prepare token refresh request
15061531 token_data = {
15071532 "grant_type" : "refresh_token" ,
15081533 "refresh_token" : refresh_token ,
1509- "client_id" : client_id ,
15101534 }
15111535
1512- # Add client_secret if available (some providers require it)
1513- if client_secret :
1514- token_data ["client_secret" ] = client_secret
1536+ # Include client credentials in POST body only when not using Basic auth
1537+ if not use_basic_auth :
1538+ token_data ["client_id" ] = client_id
1539+ # Add client_secret if available (some providers require it)
1540+ if client_secret :
1541+ token_data ["client_secret" ] = client_secret
15151542
15161543 # Add resource parameter for JWT access token (RFC 8707)
15171544 # Must be included in refresh requests to maintain JWT token type
@@ -1531,7 +1558,7 @@ async def refresh_token(
15311558 # Attempt token refresh with retries
15321559 for attempt in range (self .max_retries ):
15331560 try :
1534- response = await self ._post_token_request (token_url , token_data , ca_certificate = ca_certificate , client_cert = client_cert , client_key = client_key )
1561+ response = await self ._post_token_request (token_url , token_data , ca_certificate = ca_certificate , client_cert = client_cert , client_key = client_key , headers = auth_header )
15351562 if response .status_code == 200 :
15361563 token_response = self ._parse_token_response (response )
15371564
0 commit comments