Skip to content

OAuth2 Pushed Authorization Request does not request scopes when asking for a consent #2175

@willemvd

Description

@willemvd

Describe the bug
When a registeredClient is configured with requireAuthorizationConsent set to true combined with a OAuth2 Pushed Authorization Request, the end user gets a consent form displayed but without any scopes. Also when configuring a custom .consentPage() on the OAuth2AuthorizationEndpointConfigurer the scope parameter remains empty (?scope=&client_id=....

To Reproduce
Initiate a OAuth2 Pushed Authorization Request with a registeredClient with .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build(); or using

spring:
  security:
    oauth2:
      authorizationserver:
        client:
          my-client:
            require-authorization-consent: true

Expected behavior

  1. The default consent page should request the scopes to the end user as initially provided in the OAuth2 Pushed Authorization Request
  2. If a consentPage() is configured on the OAuth2AuthorizationEndpointConfigurer it should fill the scope parameter with the initially provided scopes in the OAuth2 Pushed Authorization Request

Sample

Add the following tests and configuration to the class org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationCodeGrantTests.
This is a combination of the existing requestWhenPushedAuthorizationRequestThenReturnAccessTokenResponse, requestWhenRequiresConsentThenDisplaysConsentPage and requestWhenCustomConsentPageConfiguredThenRedirect tests already present in the test class

	@Test
	public void requestWhenPushedAuthorizationRequestAndRequiresConsentThenDisplaysConsentPage() throws Exception {
		this.spring.register(AuthorizationServerConfigurationWithPushedAuthorizationRequests.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scopes((scopes) -> {
			scopes.clear();
			scopes.add("message.read");
			scopes.add("message.write");
		}).clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build();
		this.registeredClientRepository.save(registeredClient);

		MvcResult mvcResult = this.mvc
				.perform(post("/oauth2/par").params(getAuthorizationRequestParameters(registeredClient))
						.param(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE)
						.param(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")
						.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
				.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
				.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
				.andExpect(status().isCreated())
				.andExpect(jsonPath("$.request_uri").isNotEmpty())
				.andExpect(jsonPath("$.expires_in").isNotEmpty())
				.andReturn();

		String requestUri = JsonPath.read(mvcResult.getResponse().getContentAsString(), "$.request_uri");

		String consentPage = this.mvc
				.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
						.queryParam(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
						.queryParam(OAuth2ParameterNames.REQUEST_URI, requestUri)
						.with(user("user")))
				.andExpect(status().is2xxSuccessful())
				.andReturn()
				.getResponse()
				.getContentAsString();

		assertThat(consentPage).contains("Consent required");
		assertThat(consentPage).contains(scopeCheckbox("message.read"));
		assertThat(consentPage).contains(scopeCheckbox("message.write"));
	}

	@Test
	public void requestWhenWhenPushedAuthorizationRequestAndCustomConsentPageConfiguredThenRedirect() throws Exception {
		this.spring.register(AuthorizationServerConfigurationWithPushedAuthorizationRequestsAndCustomConsentPage.class).autowire();

		RegisteredClient registeredClient = TestRegisteredClients.registeredClient().scopes((scopes) -> {
			scopes.clear();
			scopes.add("message.read");
			scopes.add("message.write");
		}).clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build();
		this.registeredClientRepository.save(registeredClient);

		MvcResult mvcResult = this.mvc
				.perform(post("/oauth2/par").params(getAuthorizationRequestParameters(registeredClient))
						.param(PkceParameterNames.CODE_CHALLENGE, S256_CODE_CHALLENGE)
						.param(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256")
						.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
				.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
				.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
				.andExpect(status().isCreated())
				.andExpect(jsonPath("$.request_uri").isNotEmpty())
				.andExpect(jsonPath("$.expires_in").isNotEmpty())
				.andReturn();

		String requestUri = JsonPath.read(mvcResult.getResponse().getContentAsString(), "$.request_uri");

		mvcResult = this.mvc
				.perform(get(DEFAULT_AUTHORIZATION_ENDPOINT_URI)
						.queryParam(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
						.queryParam(OAuth2ParameterNames.REQUEST_URI, requestUri)
						.with(user("user")))
				.andExpect(status().is3xxRedirection())
				.andReturn();
		String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
		assertThat(redirectedUrl).matches("http://localhost/oauth2/consent\\?scope=.+&client_id=.+&state=.+");

		String locationHeader = URLDecoder.decode(redirectedUrl, StandardCharsets.UTF_8.name());
		UriComponents uriComponents = UriComponentsBuilder.fromUriString(locationHeader).build();
		MultiValueMap<String, String> redirectQueryParams = uriComponents.getQueryParams();

		assertThat(uriComponents.getPath()).isEqualTo(consentPage);
		assertThat(redirectQueryParams.getFirst(OAuth2ParameterNames.SCOPE)).isEqualTo("message.read message.write");
		assertThat(redirectQueryParams.getFirst(OAuth2ParameterNames.CLIENT_ID))
				.isEqualTo(registeredClient.getClientId());

		String state = extractParameterFromRedirectUri(redirectedUrl, "state");
		OAuth2Authorization authorization = this.authorizationService.findByToken(state, STATE_TOKEN_TYPE);
		assertThat(authorization).isNotNull();
	}

	@EnableWebSecurity
	@Configuration(proxyBeanMethods = false)
	static class AuthorizationServerConfigurationWithPushedAuthorizationRequestsAndCustomConsentPage
			extends AuthorizationServerConfiguration {

		// @formatter:off
		@Bean
		SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
			OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
					OAuth2AuthorizationServerConfigurer.authorizationServer();
			http
					.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
					.with(authorizationServerConfigurer, (authorizationServer) ->
							authorizationServer
									.pushedAuthorizationRequestEndpoint(Customizer.withDefaults())
									.authorizationEndpoint((authorizationEndpoint) ->
											authorizationEndpoint.consentPage(consentPage))
					)
					.authorizeHttpRequests((authorize) ->
							authorize.anyRequest().authenticated()
					);
			return http.build();
		}
		// @formatter:on

	}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions