@@ -1572,6 +1572,52 @@ def test_run_raises_when_authorization_endpoint_is_malformed_uri
15721572 assert_match ( /authorization_endpoint/i , error . message )
15731573 end
15741574
1575+ def test_run_replaces_authorization_request_parameters_the_endpoint_url_already_carries
1576+ # An `authorization_endpoint` may carry a query of its own. A parameter of the same name as one the flow sets is
1577+ # replaced rather than sent twice, so the URL cannot speak for the client's identity, redirect URI, or PKCE challenge;
1578+ # the rest of the query is kept.
1579+ query = authorization_url_query_for_endpoint_query (
1580+ "client_id=other&redirect_uri=https%3A%2F%2Fother.example.com%2Fcb&state=fixed&code_challenge=theirs&audience=api" ,
1581+ )
1582+
1583+ assert_equal (
1584+ [ "audience" , "response_type" , "client_id" , "redirect_uri" , "code_challenge" , "code_challenge_method" , "state" , "resource" ] ,
1585+ query . map ( &:first ) ,
1586+ )
1587+ assert_equal ( "api" , query . to_h [ "audience" ] )
1588+ assert_equal ( "test-client" , query . to_h [ "client_id" ] )
1589+ assert_equal ( "http://localhost:0/callback" , query . to_h [ "redirect_uri" ] )
1590+ refute_equal ( "theirs" , query . to_h [ "code_challenge" ] )
1591+ refute_equal ( "fixed" , query . to_h [ "state" ] )
1592+ end
1593+
1594+ def test_run_drops_request_object_parameters_the_endpoint_url_carries
1595+ # RFC 9101 has an authorization server take the whole authorization request from `request` or `request_uri`,
1596+ # over every parameter in the query, so neither may come from the endpoint URL even though the flow sets no
1597+ # parameter of either name.
1598+ query = authorization_url_query_for_endpoint_query (
1599+ "request_uri=https%3A%2F%2Fother.example.com%2Frequest&request=opaque&audience=api" ,
1600+ )
1601+
1602+ assert_equal (
1603+ [ "audience" , "response_type" , "client_id" , "redirect_uri" , "code_challenge" , "code_challenge_method" , "state" , "resource" ] ,
1604+ query . map ( &:first ) ,
1605+ )
1606+ end
1607+
1608+ def test_run_keeps_an_endpoint_scope_when_the_flow_has_none
1609+ # An authorization server may place a default `scope` on its own endpoint URL. With no scope of its own
1610+ # (none requested, none in the resource metadata, none on the provider) the flow leaves it there,
1611+ # as the TypeScript SDK does.
1612+ query = authorization_url_query_for_endpoint_query ( "scope=openid" )
1613+
1614+ assert_equal (
1615+ [ "scope" , "response_type" , "client_id" , "redirect_uri" , "code_challenge" , "code_challenge_method" , "state" , "resource" ] ,
1616+ query . map ( &:first ) ,
1617+ )
1618+ assert_equal ( "openid" , query . to_h [ "scope" ] )
1619+ end
1620+
15751621 def test_run_raises_when_prm_resource_is_malformed_uri
15761622 stub_request ( :get , @prm_url ) . to_return (
15771623 status : 200 ,
@@ -4436,6 +4482,38 @@ def test_run_sends_server_url_as_resource_when_prm_omits_it
44364482
44374483 private
44384484
4485+ # Serves authorization server metadata whose `authorization_endpoint` carries `endpoint_query`,
4486+ # runs the authorization-code flow to completion, and returns the query of the URL the browser was
4487+ # sent to as name/value pairs in order.
4488+ def authorization_url_query_for_endpoint_query ( endpoint_query )
4489+ stub_request ( :get , @as_metadata_url ) . to_return (
4490+ status : 200 ,
4491+ headers : { "Content-Type" => "application/json" } ,
4492+ body : JSON . generate (
4493+ issuer : @auth_base ,
4494+ authorization_endpoint : "#{ @auth_base } /authorize?#{ endpoint_query } " ,
4495+ token_endpoint : "#{ @auth_base } /token" ,
4496+ registration_endpoint : "#{ @auth_base } /register" ,
4497+ response_types_supported : [ "code" ] ,
4498+ code_challenge_methods_supported : [ "S256" ] ,
4499+ token_endpoint_auth_methods_supported : [ "none" ] ,
4500+ ) ,
4501+ )
4502+ holder = { }
4503+ provider = Provider . new (
4504+ **authorization_code_provider_arguments (
4505+ -> ( url ) { holder [ :authorization_url ] = url } ,
4506+ -> { [ "test-auth-code" , URI . decode_www_form ( holder [ :authorization_url ] . query ) . to_h . fetch ( "state" ) ] } ,
4507+ ) ,
4508+ )
4509+
4510+ result = Flow . new ( provider : provider ) . run! ( server_url : @server_url , resource_metadata_url : @prm_url )
4511+
4512+ assert_equal ( :authorized , result )
4513+
4514+ URI . decode_www_form ( holder [ :authorization_url ] . query )
4515+ end
4516+
44394517 def refresh_only_provider
44404518 provider = Provider . new (
44414519 client_metadata : { redirect_uris : [ "http://localhost:0/callback" ] } ,
0 commit comments