1212
1313import httpx
1414import pytest
15- from pydantic import AnyHttpUrl
15+ from pydantic import AnyHttpUrl , AnyUrl
1616from starlette .applications import Starlette
1717
1818from mcp .server .auth .provider import (
@@ -320,7 +320,7 @@ async def test_metadata_endpoint(self, test_client: httpx.AsyncClient):
320320 assert metadata ["revocation_endpoint" ] == "https://auth.example.com/revoke"
321321 assert metadata ["response_types_supported" ] == ["code" ]
322322 assert metadata ["code_challenge_methods_supported" ] == ["S256" ]
323- assert metadata ["token_endpoint_auth_methods_supported" ] == ["client_secret_post" ]
323+ assert metadata ["token_endpoint_auth_methods_supported" ] == ["client_secret_post" , "client_secret_basic" ]
324324 assert metadata ["grant_types_supported" ] == [
325325 "authorization_code" ,
326326 "refresh_token" ,
@@ -339,8 +339,8 @@ async def test_token_validation_error(self, test_client: httpx.AsyncClient):
339339 },
340340 )
341341 error_response = response .json ()
342- assert error_response ["error" ] == "invalid_request "
343- assert "error_description" in error_response # Contains validation error messages
342+ assert error_response ["error" ] == "unauthorized_client "
343+ assert "error_description" in error_response # Contains error message
344344
345345 @pytest .mark .anyio
346346 async def test_token_invalid_auth_code (
@@ -976,6 +976,147 @@ async def test_client_registration_default_response_types(
976976 assert "response_types" in data
977977 assert data ["response_types" ] == ["code" ]
978978
979+ @pytest .mark .anyio
980+ async def test_client_secret_basic_authentication (
981+ self , test_client : httpx .AsyncClient , mock_oauth_provider : MockOAuthProvider , pkce_challenge : dict [str , str ]
982+ ):
983+ """Test that client_secret_basic authentication works correctly."""
984+ client_metadata = {
985+ "redirect_uris" : ["https://client.example.com/callback" ],
986+ "client_name" : "Basic Auth Client" ,
987+ "token_endpoint_auth_method" : "client_secret_basic" ,
988+ "grant_types" : ["authorization_code" , "refresh_token" ],
989+ }
990+
991+ response = await test_client .post ("/register" , json = client_metadata )
992+ assert response .status_code == 201
993+ client_info = response .json ()
994+ assert client_info ["token_endpoint_auth_method" ] == "client_secret_basic"
995+
996+ auth_code = f"code_{ int (time .time ())} "
997+ mock_oauth_provider .auth_codes [auth_code ] = AuthorizationCode (
998+ code = auth_code ,
999+ client_id = client_info ["client_id" ],
1000+ code_challenge = pkce_challenge ["code_challenge" ],
1001+ redirect_uri = AnyUrl ("https://client.example.com/callback" ),
1002+ redirect_uri_provided_explicitly = True ,
1003+ scopes = ["read" , "write" ],
1004+ expires_at = time .time () + 600 ,
1005+ )
1006+
1007+ credentials = f"{ client_info ['client_id' ]} :{ client_info ['client_secret' ]} "
1008+ encoded_credentials = base64 .b64encode (credentials .encode ()).decode ()
1009+
1010+ response = await test_client .post (
1011+ "/token" ,
1012+ headers = {"Authorization" : f"Basic { encoded_credentials } " },
1013+ data = {
1014+ "grant_type" : "authorization_code" ,
1015+ "client_id" : client_info ["client_id" ],
1016+ "code" : auth_code ,
1017+ "code_verifier" : pkce_challenge ["code_verifier" ],
1018+ "redirect_uri" : "https://client.example.com/callback" ,
1019+ },
1020+ )
1021+ assert response .status_code == 200
1022+ token_response = response .json ()
1023+ assert "access_token" in token_response
1024+
1025+ @pytest .mark .anyio
1026+ async def test_wrong_auth_method_without_valid_credentials_fails (
1027+ self , test_client : httpx .AsyncClient , mock_oauth_provider : MockOAuthProvider , pkce_challenge : dict [str , str ]
1028+ ):
1029+ """Test that using the wrong authentication method fails when credentials are missing."""
1030+ client_metadata = {
1031+ "redirect_uris" : ["https://client.example.com/callback" ],
1032+ "client_name" : "Post Auth Client" ,
1033+ "token_endpoint_auth_method" : "client_secret_post" ,
1034+ "grant_types" : ["authorization_code" , "refresh_token" ],
1035+ }
1036+
1037+ response = await test_client .post ("/register" , json = client_metadata )
1038+ assert response .status_code == 201
1039+ client_info = response .json ()
1040+ assert client_info ["token_endpoint_auth_method" ] == "client_secret_post"
1041+
1042+ auth_code = f"code_{ int (time .time ())} "
1043+ mock_oauth_provider .auth_codes [auth_code ] = AuthorizationCode (
1044+ code = auth_code ,
1045+ client_id = client_info ["client_id" ],
1046+ code_challenge = pkce_challenge ["code_challenge" ],
1047+ redirect_uri = AnyUrl ("https://client.example.com/callback" ),
1048+ redirect_uri_provided_explicitly = True ,
1049+ scopes = ["read" , "write" ],
1050+ expires_at = time .time () + 600 ,
1051+ )
1052+
1053+ # Try to use Basic auth when client_secret_post is registered (without secret in body)
1054+ # This should fail because the secret is missing from the expected location
1055+
1056+ credentials = f"{ client_info ['client_id' ]} :{ client_info ['client_secret' ]} "
1057+ encoded_credentials = base64 .b64encode (credentials .encode ()).decode ()
1058+
1059+ response = await test_client .post (
1060+ "/token" ,
1061+ headers = {"Authorization" : f"Basic { encoded_credentials } " },
1062+ data = {
1063+ "grant_type" : "authorization_code" ,
1064+ "client_id" : client_info ["client_id" ],
1065+ # client_secret NOT in body where it should be
1066+ "code" : auth_code ,
1067+ "code_verifier" : pkce_challenge ["code_verifier" ],
1068+ "redirect_uri" : "https://client.example.com/callback" ,
1069+ },
1070+ )
1071+ assert response .status_code == 401
1072+ error_response = response .json ()
1073+ assert error_response ["error" ] == "unauthorized_client"
1074+ assert "Client secret is required" in error_response ["error_description" ]
1075+
1076+ @pytest .mark .anyio
1077+ async def test_basic_auth_without_header_fails (
1078+ self , test_client : httpx .AsyncClient , mock_oauth_provider : MockOAuthProvider , pkce_challenge : dict [str , str ]
1079+ ):
1080+ """Test that omitting Basic auth when client_secret_basic is registered fails."""
1081+ client_metadata = {
1082+ "redirect_uris" : ["https://client.example.com/callback" ],
1083+ "client_name" : "Basic Auth Client" ,
1084+ "token_endpoint_auth_method" : "client_secret_basic" ,
1085+ "grant_types" : ["authorization_code" , "refresh_token" ],
1086+ }
1087+
1088+ response = await test_client .post ("/register" , json = client_metadata )
1089+ assert response .status_code == 201
1090+ client_info = response .json ()
1091+ assert client_info ["token_endpoint_auth_method" ] == "client_secret_basic"
1092+
1093+ auth_code = f"code_{ int (time .time ())} "
1094+ mock_oauth_provider .auth_codes [auth_code ] = AuthorizationCode (
1095+ code = auth_code ,
1096+ client_id = client_info ["client_id" ],
1097+ code_challenge = pkce_challenge ["code_challenge" ],
1098+ redirect_uri = AnyUrl ("https://client.example.com/callback" ),
1099+ redirect_uri_provided_explicitly = True ,
1100+ scopes = ["read" , "write" ],
1101+ expires_at = time .time () + 600 ,
1102+ )
1103+
1104+ response = await test_client .post (
1105+ "/token" ,
1106+ data = {
1107+ "grant_type" : "authorization_code" ,
1108+ "client_id" : client_info ["client_id" ],
1109+ "client_secret" : client_info ["client_secret" ], # Secret in body (ignored)
1110+ "code" : auth_code ,
1111+ "code_verifier" : pkce_challenge ["code_verifier" ],
1112+ "redirect_uri" : "https://client.example.com/callback" ,
1113+ },
1114+ )
1115+ assert response .status_code == 401
1116+ error_response = response .json ()
1117+ assert error_response ["error" ] == "unauthorized_client"
1118+ assert "Missing or invalid Basic authentication" in error_response ["error_description" ]
1119+
9791120
9801121class TestAuthorizeEndpointErrors :
9811122 """Test error handling in the OAuth authorization endpoint."""
0 commit comments