@@ -347,3 +347,127 @@ async def test_oversized_cross_origin_request_gets_413_with_cors_headers(client:
347347 )
348348 assert response .status_code == 413
349349 assert response .headers ["access-control-allow-origin" ] == "*"
350+
351+
352+ @pytest .mark .anyio
353+ async def test_rfc6749_token_unauthorized_client (client : httpx2 .AsyncClient , oauth_provider : MockOAuthProvider ):
354+ """RFC 6749 §5.2: when a grant is supported by the server but not authorized for this client,
355+ the server must return error='unauthorized_client'.
356+ """
357+ reg_response = await client .post (
358+ "/register" ,
359+ json = {
360+ "client_name" : "Test Client" ,
361+ "redirect_uris" : ["https://client.example.com/callback" ],
362+ "grant_types" : ["authorization_code" ],
363+ "token_endpoint_auth_method" : "client_secret_post" ,
364+ },
365+ )
366+ client_data = reg_response .json ()
367+
368+ response = await client .post (
369+ "/token" ,
370+ data = {
371+ "grant_type" : "refresh_token" ,
372+ "refresh_token" : "some-token" ,
373+ "client_id" : client_data ["client_id" ],
374+ "client_secret" : client_data ["client_secret" ],
375+ },
376+ )
377+ assert response .status_code == 400
378+ assert response .json ()["error" ] == "unauthorized_client"
379+
380+
381+ @pytest .mark .anyio
382+ async def test_rfc6749_token_unsupported_grant_type (client : httpx2 .AsyncClient , oauth_provider : MockOAuthProvider ):
383+ """RFC 6749 §5.2: when a grant type is not supported by the server,
384+ the server must return error='unsupported_grant_type'.
385+ """
386+ reg_response = await client .post (
387+ "/register" ,
388+ json = {
389+ "client_name" : "Test Client" ,
390+ "redirect_uris" : ["https://client.example.com/callback" ],
391+ "grant_types" : ["authorization_code" ],
392+ "token_endpoint_auth_method" : "client_secret_post" ,
393+ },
394+ )
395+ client_data = reg_response .json ()
396+
397+ response = await client .post (
398+ "/token" ,
399+ data = {
400+ "grant_type" : "client_credentials" ,
401+ "client_id" : client_data ["client_id" ],
402+ "client_secret" : client_data ["client_secret" ],
403+ },
404+ )
405+ assert response .status_code == 400
406+ assert response .json ()["error" ] == "unsupported_grant_type"
407+
408+
409+ @pytest .mark .anyio
410+ async def test_rfc6749_token_basic_auth_failure_www_authenticate (
411+ client : httpx2 .AsyncClient , oauth_provider : MockOAuthProvider
412+ ):
413+ """RFC 6749 §5.2: failed client authentication via Authorization header must return 401
414+ with invalid_client and a matching WWW-Authenticate header.
415+ """
416+ reg_response = await client .post (
417+ "/register" ,
418+ json = {
419+ "client_name" : "Test Client" ,
420+ "redirect_uris" : ["https://client.example.com/callback" ],
421+ "grant_types" : ["authorization_code" ],
422+ "token_endpoint_auth_method" : "client_secret_basic" ,
423+ },
424+ )
425+ client_data = reg_response .json ()
426+
427+ auth_header = "Basic " + base64 .b64encode (f"{ client_data ['client_id' ]} :wrong_secret" .encode ()).decode ()
428+ response = await client .post (
429+ "/token" ,
430+ headers = {"Authorization" : auth_header },
431+ data = {
432+ "grant_type" : "authorization_code" ,
433+ "code" : "any_code" ,
434+ "client_id" : client_data ["client_id" ],
435+ },
436+ )
437+ assert response .status_code == 401
438+ assert response .json ()["error" ] == "invalid_client"
439+ assert "www-authenticate" in response .headers
440+ assert response .headers ["www-authenticate" ].lower ().startswith ("basic" )
441+
442+
443+ @pytest .mark .anyio
444+ async def test_rfc7009_revoke_invalid_client_and_www_authenticate (
445+ client : httpx2 .AsyncClient , oauth_provider : MockOAuthProvider
446+ ):
447+ """RFC 7009 §2.2.1: failed client authentication on /revoke must return error='invalid_client'
448+ (not 'unauthorized_client'), status 401, with matching WWW-Authenticate header.
449+ """
450+ reg_response = await client .post (
451+ "/register" ,
452+ json = {
453+ "client_name" : "Test Client" ,
454+ "redirect_uris" : ["https://client.example.com/callback" ],
455+ "grant_types" : ["authorization_code" ],
456+ "token_endpoint_auth_method" : "client_secret_basic" ,
457+ },
458+ )
459+ client_data = reg_response .json ()
460+
461+ auth_header = "Basic " + base64 .b64encode (f"{ client_data ['client_id' ]} :wrong_secret" .encode ()).decode ()
462+ response = await client .post (
463+ "/revoke" ,
464+ headers = {"Authorization" : auth_header },
465+ data = {
466+ "token" : "any_token" ,
467+ "client_id" : client_data ["client_id" ],
468+ },
469+ )
470+ assert response .status_code == 401
471+ assert response .json ()["error" ] == "invalid_client"
472+ assert "www-authenticate" in response .headers
473+ assert response .headers ["www-authenticate" ].lower ().startswith ("basic" )
0 commit comments