1818)
1919from mcp .shared .auth import OAuthClientMetadata
2020
21-
2221# ============================================================================
2322# Fixtures
2423# ============================================================================
@@ -376,9 +375,10 @@ async def test_exchange_id_jag_for_access_token_success(sample_id_jag, mock_toke
376375 )
377376
378377 # Set up OAuth metadata
379- from mcp .shared .auth import OAuthMetadata
380378 from pydantic import HttpUrl
381379
380+ from mcp .shared .auth import OAuthMetadata
381+
382382 provider .context .oauth_metadata = OAuthMetadata (
383383 issuer = HttpUrl ("https://auth.mcp-server.example/" ),
384384 authorization_endpoint = HttpUrl ("https://auth.mcp-server.example/oauth2/authorize" ),
@@ -559,6 +559,60 @@ async def test_exchange_token_with_client_authentication(sample_id_token, sample
559559 assert call_args [1 ]["data" ]["client_secret" ] == "test-client-secret"
560560
561561
562+ @pytest .mark .anyio
563+ async def test_exchange_token_with_client_id_only (sample_id_token , sample_id_jag , mock_token_storage ):
564+ """Test token exchange with client_id but no client_secret (covers branch 232->235)."""
565+ from mcp .shared .auth import OAuthClientInformationFull
566+
567+ token_exchange_params = TokenExchangeParameters .from_id_token (
568+ id_token = sample_id_token ,
569+ mcp_server_auth_issuer = "https://auth.mcp-server.example/" ,
570+ mcp_server_resource_id = "https://mcp-server.example/" ,
571+ scope = "read write" ,
572+ )
573+
574+ provider = EnterpriseAuthOAuthClientProvider (
575+ server_url = "https://mcp-server.example/" ,
576+ client_metadata = OAuthClientMetadata (
577+ redirect_uris = ["http://localhost:8080/callback" ],
578+ client_name = "Test Client" ,
579+ ),
580+ storage = mock_token_storage ,
581+ idp_token_endpoint = "https://idp.example.com/oauth2/token" ,
582+ token_exchange_params = token_exchange_params ,
583+ )
584+
585+ # Set client info WITHOUT secret (client_secret=None)
586+ provider .context .client_info = OAuthClientInformationFull (
587+ client_id = "test-client-id" ,
588+ client_secret = None , # No secret
589+ redirect_uris = ["http://localhost:8080/callback" ],
590+ )
591+
592+ # Mock HTTP response
593+ mock_response = httpx .Response (
594+ status_code = 200 ,
595+ json = {
596+ "issued_token_type" : "urn:ietf:params:oauth:token-type:id-jag" ,
597+ "access_token" : sample_id_jag ,
598+ "token_type" : "N_A" ,
599+ "scope" : "read write" ,
600+ "expires_in" : 300 ,
601+ },
602+ )
603+
604+ mock_client = Mock (spec = httpx .AsyncClient )
605+ mock_client .post = AsyncMock (return_value = mock_response )
606+
607+ # Perform token exchange
608+ id_jag = await provider .exchange_token_for_id_jag (mock_client )
609+
610+ # Verify client_id was included but NOT client_secret
611+ call_args = mock_client .post .call_args
612+ assert call_args [1 ]["data" ]["client_id" ] == "test-client-id"
613+ assert "client_secret" not in call_args [1 ]["data" ]
614+
615+
562616@pytest .mark .anyio
563617async def test_exchange_token_http_error (sample_id_token , mock_token_storage ):
564618 """Test token exchange with HTTP error."""
@@ -656,7 +710,10 @@ async def test_exchange_token_warning_for_non_na_token_type(sample_id_token, sam
656710
657711 # Should succeed but log warning
658712 import logging
659- with patch .object (logging .getLogger ("mcp.client.auth.extensions.enterprise_managed_auth" ), "warning" ) as mock_warning :
713+
714+ with patch .object (
715+ logging .getLogger ("mcp.client.auth.extensions.enterprise_managed_auth" ), "warning"
716+ ) as mock_warning :
660717 id_jag = await provider .exchange_token_for_id_jag (mock_client )
661718 assert id_jag == sample_id_jag
662719 mock_warning .assert_called_once ()
@@ -665,9 +722,10 @@ async def test_exchange_token_warning_for_non_na_token_type(sample_id_token, sam
665722@pytest .mark .anyio
666723async def test_exchange_id_jag_with_client_authentication (sample_id_jag , mock_token_storage ):
667724 """Test JWT bearer grant with client authentication."""
668- from mcp .shared .auth import OAuthClientInformationFull , OAuthMetadata
669725 from pydantic import HttpUrl
670726
727+ from mcp .shared .auth import OAuthClientInformationFull , OAuthMetadata
728+
671729 token_exchange_params = TokenExchangeParameters .from_id_token (
672730 id_token = "dummy-token" ,
673731 mcp_server_auth_issuer = "https://auth.mcp-server.example/" ,
@@ -715,18 +773,86 @@ async def test_exchange_id_jag_with_client_authentication(sample_id_jag, mock_to
715773 # Perform JWT bearer grant
716774 token = await provider .exchange_id_jag_for_access_token (mock_client , sample_id_jag )
717775
776+ # Verify token was returned
777+ assert token .access_token == "mcp-access-token-12345"
778+
718779 # Verify client credentials were included
719780 call_args = mock_client .post .call_args
720781 assert call_args [1 ]["data" ]["client_id" ] == "test-client-id"
721782 assert call_args [1 ]["data" ]["client_secret" ] == "test-client-secret"
722783
723784
785+ @pytest .mark .anyio
786+ async def test_exchange_id_jag_with_client_id_only (sample_id_jag , mock_token_storage ):
787+ """Test JWT bearer grant with client_id but no client_secret (covers branch 304->307)."""
788+ from pydantic import HttpUrl
789+
790+ from mcp .shared .auth import OAuthClientInformationFull , OAuthMetadata
791+
792+ token_exchange_params = TokenExchangeParameters .from_id_token (
793+ id_token = "dummy-token" ,
794+ mcp_server_auth_issuer = "https://auth.mcp-server.example/" ,
795+ mcp_server_resource_id = "https://mcp-server.example/" ,
796+ )
797+
798+ provider = EnterpriseAuthOAuthClientProvider (
799+ server_url = "https://mcp-server.example/" ,
800+ client_metadata = OAuthClientMetadata (
801+ redirect_uris = ["http://localhost:8080/callback" ],
802+ ),
803+ storage = mock_token_storage ,
804+ idp_token_endpoint = "https://idp.example.com/oauth2/token" ,
805+ token_exchange_params = token_exchange_params ,
806+ )
807+
808+ # Set client info WITHOUT secret (client_secret=None)
809+ provider .context .client_info = OAuthClientInformationFull (
810+ client_id = "test-client-id" ,
811+ client_secret = None , # No secret
812+ redirect_uris = ["http://localhost:8080/callback" ],
813+ )
814+
815+ # Set up OAuth metadata
816+ provider .context .oauth_metadata = OAuthMetadata (
817+ issuer = HttpUrl ("https://auth.mcp-server.example/" ),
818+ authorization_endpoint = HttpUrl ("https://auth.mcp-server.example/oauth2/authorize" ),
819+ token_endpoint = HttpUrl ("https://auth.mcp-server.example/oauth2/token" ),
820+ )
821+
822+ # Mock HTTP response
823+ mock_response = httpx .Response (
824+ status_code = 200 ,
825+ json = {
826+ "token_type" : "Bearer" ,
827+ "access_token" : "mcp-access-token-12345" ,
828+ "expires_in" : 3600 ,
829+ "scope" : "read write" ,
830+ },
831+ )
832+
833+ mock_client = Mock (spec = httpx .AsyncClient )
834+ mock_client .post = AsyncMock (return_value = mock_response )
835+
836+ # Perform JWT bearer grant
837+ token = await provider .exchange_id_jag_for_access_token (mock_client , sample_id_jag )
838+
839+ # Verify token was returned correctly
840+ assert token .access_token == "mcp-access-token-12345"
841+ assert token .token_type == "Bearer"
842+
843+ # Verify client_id was included but NOT client_secret
844+ call_args = mock_client .post .call_args
845+ assert call_args [1 ]["data" ]["client_id" ] == "test-client-id"
846+ assert "client_secret" not in call_args [1 ]["data" ]
847+
848+
724849@pytest .mark .anyio
725850async def test_exchange_id_jag_error_response (sample_id_jag , mock_token_storage ):
726851 """Test JWT bearer grant with error response."""
727- from mcp .shared .auth import OAuthMetadata
728852 from pydantic import HttpUrl
729853
854+ from mcp .shared .auth import OAuthMetadata
855+
730856 token_exchange_params = TokenExchangeParameters .from_id_token (
731857 id_token = "dummy-token" ,
732858 mcp_server_auth_issuer = "https://auth.mcp-server.example/" ,
@@ -770,9 +896,10 @@ async def test_exchange_id_jag_error_response(sample_id_jag, mock_token_storage)
770896@pytest .mark .anyio
771897async def test_exchange_id_jag_non_json_error (sample_id_jag , mock_token_storage ):
772898 """Test JWT bearer grant with non-JSON error response."""
773- from mcp .shared .auth import OAuthMetadata
774899 from pydantic import HttpUrl
775900
901+ from mcp .shared .auth import OAuthMetadata
902+
776903 token_exchange_params = TokenExchangeParameters .from_id_token (
777904 id_token = "dummy-token" ,
778905 mcp_server_auth_issuer = "https://auth.mcp-server.example/" ,
@@ -814,9 +941,10 @@ async def test_exchange_id_jag_non_json_error(sample_id_jag, mock_token_storage)
814941@pytest .mark .anyio
815942async def test_exchange_id_jag_http_error (sample_id_jag , mock_token_storage ):
816943 """Test JWT bearer grant with HTTP error."""
817- from mcp .shared .auth import OAuthMetadata
818944 from pydantic import HttpUrl
819945
946+ from mcp .shared .auth import OAuthMetadata
947+
820948 token_exchange_params = TokenExchangeParameters .from_id_token (
821949 id_token = "dummy-token" ,
822950 mcp_server_auth_issuer = "https://auth.mcp-server.example/" ,
@@ -872,5 +1000,3 @@ def test_validate_token_exchange_params_missing_resource():
8721000
8731001 with pytest .raises (ValueError , match = "resource is required" ):
8741002 validate_token_exchange_params (params )
875-
876-
0 commit comments