@@ -339,9 +339,59 @@ async def test_token_validation_error(self, test_client: httpx.AsyncClient):
339339 },
340340 )
341341 error_response = response .json ()
342- assert error_response ["error" ] == "unauthorized_client"
342+ # Per RFC 6749 Section 5.2, authentication failures (missing client_id)
343+ # must return "invalid_client", not "unauthorized_client"
344+ assert error_response ["error" ] == "invalid_client"
343345 assert "error_description" in error_response # Contains error message
344346
347+ @pytest .mark .anyio
348+ async def test_token_invalid_client_secret_returns_invalid_client (
349+ self ,
350+ test_client : httpx .AsyncClient ,
351+ registered_client : dict [str , Any ],
352+ pkce_challenge : dict [str , str ],
353+ mock_oauth_provider : MockOAuthProvider ,
354+ ):
355+ """Test token endpoint returns 'invalid_client' for wrong client_secret per RFC 6749.
356+
357+ RFC 6749 Section 5.2 defines:
358+ - invalid_client: Client authentication failed (wrong credentials, unknown client)
359+ - unauthorized_client: Authenticated client not authorized for grant type
360+
361+ When client_secret is wrong, this is an authentication failure, so the
362+ error code MUST be 'invalid_client'.
363+ """
364+ # Create an auth code for the registered client
365+ auth_code = f"code_{ int (time .time ())} "
366+ mock_oauth_provider .auth_codes [auth_code ] = AuthorizationCode (
367+ code = auth_code ,
368+ client_id = registered_client ["client_id" ],
369+ code_challenge = pkce_challenge ["code_challenge" ],
370+ redirect_uri = AnyUrl ("https://client.example.com/callback" ),
371+ redirect_uri_provided_explicitly = True ,
372+ scopes = ["read" , "write" ],
373+ expires_at = time .time () + 600 ,
374+ )
375+
376+ # Try to exchange the auth code with a WRONG client_secret
377+ response = await test_client .post (
378+ "/token" ,
379+ data = {
380+ "grant_type" : "authorization_code" ,
381+ "client_id" : registered_client ["client_id" ],
382+ "client_secret" : "wrong_secret_that_does_not_match" ,
383+ "code" : auth_code ,
384+ "code_verifier" : pkce_challenge ["code_verifier" ],
385+ "redirect_uri" : "https://client.example.com/callback" ,
386+ },
387+ )
388+
389+ assert response .status_code == 401
390+ error_response = response .json ()
391+ # RFC 6749 Section 5.2: authentication failures MUST return "invalid_client"
392+ assert error_response ["error" ] == "invalid_client"
393+ assert "Invalid client_secret" in error_response ["error_description" ]
394+
345395 @pytest .mark .anyio
346396 async def test_token_invalid_auth_code (
347397 self ,
@@ -1070,7 +1120,8 @@ async def test_wrong_auth_method_without_valid_credentials_fails(
10701120 )
10711121 assert response .status_code == 401
10721122 error_response = response .json ()
1073- assert error_response ["error" ] == "unauthorized_client"
1123+ # RFC 6749: authentication failures return "invalid_client"
1124+ assert error_response ["error" ] == "invalid_client"
10741125 assert "Client secret is required" in error_response ["error_description" ]
10751126
10761127 @pytest .mark .anyio
@@ -1114,7 +1165,8 @@ async def test_basic_auth_without_header_fails(
11141165 )
11151166 assert response .status_code == 401
11161167 error_response = response .json ()
1117- assert error_response ["error" ] == "unauthorized_client"
1168+ # RFC 6749: authentication failures return "invalid_client"
1169+ assert error_response ["error" ] == "invalid_client"
11181170 assert "Missing or invalid Basic authentication" in error_response ["error_description" ]
11191171
11201172 @pytest .mark .anyio
@@ -1158,7 +1210,8 @@ async def test_basic_auth_invalid_base64_fails(
11581210 )
11591211 assert response .status_code == 401
11601212 error_response = response .json ()
1161- assert error_response ["error" ] == "unauthorized_client"
1213+ # RFC 6749: authentication failures return "invalid_client"
1214+ assert error_response ["error" ] == "invalid_client"
11621215 assert "Invalid Basic authentication header" in error_response ["error_description" ]
11631216
11641217 @pytest .mark .anyio
@@ -1205,7 +1258,8 @@ async def test_basic_auth_no_colon_fails(
12051258 )
12061259 assert response .status_code == 401
12071260 error_response = response .json ()
1208- assert error_response ["error" ] == "unauthorized_client"
1261+ # RFC 6749: authentication failures return "invalid_client"
1262+ assert error_response ["error" ] == "invalid_client"
12091263 assert "Invalid Basic authentication header" in error_response ["error_description" ]
12101264
12111265 @pytest .mark .anyio
@@ -1252,7 +1306,8 @@ async def test_basic_auth_client_id_mismatch_fails(
12521306 )
12531307 assert response .status_code == 401
12541308 error_response = response .json ()
1255- assert error_response ["error" ] == "unauthorized_client"
1309+ # RFC 6749: authentication failures return "invalid_client"
1310+ assert error_response ["error" ] == "invalid_client"
12561311 assert "Client ID mismatch" in error_response ["error_description" ]
12571312
12581313 @pytest .mark .anyio
0 commit comments