Remember user consent and make consent page configurable
Closes gh-283
This commit is contained in:
committed by
Joe Grandja
parent
4688b0f879
commit
683dad1443
@@ -15,9 +15,11 @@
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -40,6 +42,7 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.mock.http.client.MockClientHttpResponse;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.security.config.test.SpringTestRule;
|
||||
@@ -71,11 +74,15 @@ import org.springframework.security.oauth2.server.authorization.client.TestRegis
|
||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter;
|
||||
import org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
@@ -115,6 +122,7 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
private static ProviderSettings providerSettings;
|
||||
private static HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter =
|
||||
new OAuth2AccessTokenResponseHttpMessageConverter();
|
||||
private static String consentPage = "/custom-consent";
|
||||
|
||||
@Rule
|
||||
public final SpringTestRule spring = new SpringTestRule();
|
||||
@@ -237,11 +245,9 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
|
||||
private OAuth2AccessTokenResponse assertTokenRequestReturnsAccessTokenResponse(RegisteredClient registeredClient,
|
||||
OAuth2Authorization authorization, String tokenEndpointUri) throws Exception {
|
||||
|
||||
MvcResult mvcResult = this.mvc.perform(post(tokenEndpointUri)
|
||||
.params(getTokenRequestParameters(registeredClient, authorization))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
|
||||
registeredClient.getClientId(), registeredClient.getClientSecret())))
|
||||
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
|
||||
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
|
||||
@@ -296,6 +302,8 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
.params(getTokenRequestParameters(registeredClient, authorization))
|
||||
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
|
||||
.param(PkceParameterNames.CODE_VERIFIER, S256_CODE_VERIFIER))
|
||||
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
|
||||
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.token_type").isNotEmpty())
|
||||
@@ -326,8 +334,128 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
|
||||
this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
|
||||
.params(getTokenRequestParameters(registeredClient, authorization))
|
||||
.header(HttpHeaders.AUTHORIZATION, "Basic " + encodeBasicAuth(
|
||||
registeredClient.getClientId(), registeredClient.getClientSecret())));
|
||||
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenRequiresConsentThenDisplaysConsentPage() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.scopes(scopes -> {
|
||||
scopes.clear();
|
||||
scopes.add("message.read");
|
||||
scopes.add("message.write");
|
||||
})
|
||||
.clientSettings(settings -> settings.requireUserConsent(true))
|
||||
.build();
|
||||
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
String consentPage = this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
.params(getAuthorizationRequestParameters(registeredClient))
|
||||
.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 requestWhenConsentRequestReturnAccessTokenResponse() throws Exception {
|
||||
final String stateParameter = "consent-state";
|
||||
this.spring.register(AuthorizationServerConfiguration.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.scopes(scopes -> {
|
||||
scopes.clear();
|
||||
scopes.add("message.read");
|
||||
scopes.add("message.write");
|
||||
})
|
||||
.clientSettings(settings -> settings.requireUserConsent(true))
|
||||
.build();
|
||||
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
OAuth2Authorization stateTokenAuthorization = TestOAuth2Authorizations.authorization(registeredClient)
|
||||
.principalName("user")
|
||||
.build();
|
||||
|
||||
when(authorizationService.findByToken(
|
||||
eq(stateParameter),
|
||||
eq(new OAuth2TokenType(OAuth2ParameterNames.STATE))))
|
||||
.thenReturn(stateTokenAuthorization);
|
||||
|
||||
MvcResult mvcResult = this.mvc.perform(post(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
.param(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId())
|
||||
.param(OAuth2ParameterNames.SCOPE, "message.read")
|
||||
.param(OAuth2ParameterNames.SCOPE, "message.write")
|
||||
.param(OAuth2ParameterNames.STATE, stateParameter)
|
||||
.param("consent_action", "approve")
|
||||
.with(user("user")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
|
||||
assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
|
||||
verify(authorizationService).save(authorizationCaptor.capture());
|
||||
OAuth2Authorization authorizationCodeAuthorization = authorizationCaptor.getValue();
|
||||
when(authorizationService.findByToken(
|
||||
eq(authorizationCodeAuthorization.getToken(OAuth2AuthorizationCode.class).getToken().getTokenValue()),
|
||||
eq(AUTHORIZATION_CODE_TOKEN_TYPE)))
|
||||
.thenReturn(authorizationCodeAuthorization);
|
||||
|
||||
this.mvc.perform(post(OAuth2TokenEndpointFilter.DEFAULT_TOKEN_ENDPOINT_URI)
|
||||
.params(getTokenRequestParameters(registeredClient, authorizationCodeAuthorization))
|
||||
.header(HttpHeaders.AUTHORIZATION, getAuthorizationHeader(registeredClient)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CACHE_CONTROL, containsString("no-store")))
|
||||
.andExpect(header().string(HttpHeaders.PRAGMA, containsString("no-cache")))
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.token_type").isNotEmpty())
|
||||
.andExpect(jsonPath("$.expires_in").isNotEmpty())
|
||||
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.scope").isNotEmpty())
|
||||
.andReturn();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWhenCustomConsentPageConfiguredThenRedirect() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfigurationCustomConsentPage.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.scopes(scopes -> {
|
||||
scopes.clear();
|
||||
scopes.add("message.read");
|
||||
scopes.add("message.write");
|
||||
})
|
||||
.clientSettings(settings -> settings.requireUserConsent(true))
|
||||
.build();
|
||||
when(registeredClientRepository.findByClientId(eq(registeredClient.getClientId())))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
MvcResult mvcResult = this.mvc.perform(get(OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI)
|
||||
.params(getAuthorizationRequestParameters(registeredClient))
|
||||
.with(user("user")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
|
||||
String locationHeader = URLDecoder.decode(mvcResult.getResponse().getRedirectedUrl(), StandardCharsets.UTF_8.name());
|
||||
UriComponents redirectedUrl = UriComponentsBuilder.fromUriString(locationHeader).build();
|
||||
MultiValueMap<String, String> redirectQueryParams = redirectedUrl.getQueryParams();
|
||||
|
||||
assertThat(redirectedUrl.getPath()).isEqualTo(consentPage);
|
||||
assertThat(redirectQueryParams.getFirst(OAuth2ParameterNames.SCOPE)).isEqualTo("message.read message.write");
|
||||
assertThat(redirectQueryParams.getFirst(OAuth2ParameterNames.CLIENT_ID)).isEqualTo(registeredClient.getClientId());
|
||||
|
||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
|
||||
verify(authorizationService).save(authorizationCaptor.capture());
|
||||
OAuth2Authorization authorization = authorizationCaptor.getValue();
|
||||
assertThat(redirectQueryParams.getFirst(OAuth2ParameterNames.STATE)).isEqualTo(authorization.getAttribute(OAuth2ParameterNames.STATE));
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, String> getAuthorizationRequestParameters(RegisteredClient registeredClient) {
|
||||
@@ -350,12 +478,21 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
private static String encodeBasicAuth(String clientId, String secret) throws Exception {
|
||||
private static String getAuthorizationHeader(RegisteredClient registeredClient) throws Exception {
|
||||
String clientId = registeredClient.getClientId();
|
||||
String secret = registeredClient.getClientSecret();
|
||||
clientId = URLEncoder.encode(clientId, StandardCharsets.UTF_8.name());
|
||||
secret = URLEncoder.encode(secret, StandardCharsets.UTF_8.name());
|
||||
String credentialsString = clientId + ":" + secret;
|
||||
byte[] encodedBytes = Base64.getEncoder().encode(credentialsString.getBytes(StandardCharsets.UTF_8));
|
||||
return new String(encodedBytes, StandardCharsets.UTF_8);
|
||||
return "Basic " + new String(encodedBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String scopeCheckbox(String scope) {
|
||||
return MessageFormat.format(
|
||||
"<input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" value=\"{0}\" id=\"{0}\" checked>",
|
||||
scope
|
||||
);
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@@ -418,4 +555,25 @@ public class OAuth2AuthorizationCodeGrantTests {
|
||||
}
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
static class AuthorizationServerConfigurationCustomConsentPage extends AuthorizationServerConfiguration {
|
||||
// @formatter:off
|
||||
@Bean
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
|
||||
new OAuth2AuthorizationServerConfigurer<>();
|
||||
authorizationServerConfigurer.consentPage(consentPage);
|
||||
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
|
||||
|
||||
http
|
||||
.requestMatcher(endpointsMatcher)
|
||||
.authorizeRequests(authorizeRequests ->
|
||||
authorizeRequests.anyRequest().authenticated()
|
||||
)
|
||||
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
|
||||
.apply(authorizationServerConfigurer);
|
||||
return http.build();
|
||||
}
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link InMemoryOAuth2AuthorizationConsentService}.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
*/
|
||||
public class InMemoryOAuth2AuthorizationConsentServiceTest {
|
||||
private InMemoryOAuth2AuthorizationConsentService consentService;
|
||||
|
||||
private static final String CLIENT_ID = "client-id";
|
||||
private static final String PRINCIPAL_NAME = "principal-name";
|
||||
private static final OAuth2AuthorizationConsent CONSENT = OAuth2AuthorizationConsent
|
||||
.withId(CLIENT_ID, PRINCIPAL_NAME)
|
||||
.authority(new SimpleGrantedAuthority("some.authority"))
|
||||
.build();
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
this.consentService = new InMemoryOAuth2AuthorizationConsentService();
|
||||
this.consentService.save(CONSENT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorVaragsWhenAuthorizationConsentNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new InMemoryOAuth2AuthorizationConsentService((OAuth2AuthorizationConsent) null))
|
||||
.withMessage("authorizationConsent cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorListWhenAuthorizationConsentsNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new InMemoryOAuth2AuthorizationConsentService((List<OAuth2AuthorizationConsent>) null))
|
||||
.withMessage("authorizationConsents cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenDuplicateAuthorizationConsentsThenThrowIllegalArgumentException() {
|
||||
OAuth2AuthorizationConsent authorizationConsent = OAuth2AuthorizationConsent.withId("client-id", "principal-name")
|
||||
.scope("thing.write") // must have at least one scope
|
||||
.build();
|
||||
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new InMemoryOAuth2AuthorizationConsentService(authorizationConsent, authorizationConsent))
|
||||
.withMessage("The authorizationConsent must be unique. Found duplicate, with registered client id: [client-id] and principal name: [principal-name]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveWhenConsentNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.consentService.save(null))
|
||||
.withMessage("authorizationConsent cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveWhenConsentNewThenSaved() {
|
||||
OAuth2AuthorizationConsent expectedConsent = OAuth2AuthorizationConsent
|
||||
.withId("new-client", "new-principal")
|
||||
.authority(new SimpleGrantedAuthority("new.authority"))
|
||||
.build();
|
||||
|
||||
this.consentService.save(expectedConsent);
|
||||
|
||||
OAuth2AuthorizationConsent consent =
|
||||
this.consentService.findById("new-client", "new-principal");
|
||||
assertThat(consent).isEqualTo(expectedConsent);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveWhenConsentExistsThenUpdated() {
|
||||
OAuth2AuthorizationConsent expectedConsent = OAuth2AuthorizationConsent
|
||||
.from(CONSENT)
|
||||
.authority(new SimpleGrantedAuthority("new.authority"))
|
||||
.build();
|
||||
|
||||
this.consentService.save(expectedConsent);
|
||||
|
||||
OAuth2AuthorizationConsent consent =
|
||||
this.consentService.findById(CLIENT_ID, PRINCIPAL_NAME);
|
||||
assertThat(consent).isEqualTo(expectedConsent);
|
||||
assertThat(consent).isNotEqualTo(CONSENT);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.consentService.remove(null))
|
||||
.withMessage("authorizationConsent cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeWhenConsentProvidedThenRemoved() {
|
||||
this.consentService.remove(CONSENT);
|
||||
|
||||
assertThat(this.consentService.findById(CLIENT_ID, PRINCIPAL_NAME))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findWhenRegisteredClientIdNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.consentService.findById(null, "some-user"))
|
||||
.withMessage("registeredClientId cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findWhenPrincipalNameNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.consentService.findById("some-client", null))
|
||||
.withMessage("principalName cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findWhenConsentExistsThenFound() {
|
||||
assertThat(this.consentService.findById(CLIENT_ID, PRINCIPAL_NAME))
|
||||
.isEqualTo(CONSENT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findWhenConsentDoesNotExistThenNull() {
|
||||
this.consentService.save(CONSENT);
|
||||
|
||||
assertThat(this.consentService.findById("unknown-client", PRINCIPAL_NAME)).isNull();
|
||||
assertThat(this.consentService.findById(CLIENT_ID, "unkown-user")).isNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2020-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link OAuth2AuthorizationConsent}.
|
||||
*
|
||||
* @author Daniel Garnier-Moiroux
|
||||
*/
|
||||
public class OAuth2AuthorizationConsentTest {
|
||||
@Test
|
||||
public void fromWhenAuthorizationConsentNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> OAuth2AuthorizationConsent.from(null))
|
||||
.withMessage("authorizationConsent cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withClientIdAndPrincipalWhenClientIdNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> OAuth2AuthorizationConsent.withId(null, "some-user"))
|
||||
.withMessage("registeredClientId cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withClientIdAndPrincipalWhenPrincipalNullThenThrowIllegalArgumentException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> OAuth2AuthorizationConsent.withId("some-client", null))
|
||||
.withMessage("principalName cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAuthoritiesEmptyThenThrowIllegalArgumentException() {
|
||||
OAuth2AuthorizationConsent.Builder builder = OAuth2AuthorizationConsent.withId("some-client", "some-user");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(builder::build)
|
||||
.withMessage("authorities cannot be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildWhenAllAttributesAreProvidedThenAllAttributesAreSet() {
|
||||
OAuth2AuthorizationConsent consent = OAuth2AuthorizationConsent
|
||||
.withId("some-client", "some-user")
|
||||
.scope("resource.read")
|
||||
.scope("resource.write")
|
||||
.authority(new SimpleGrantedAuthority("CLAIM_email"))
|
||||
.build();
|
||||
|
||||
assertThat(consent.getPrincipalName()).isEqualTo("some-user");
|
||||
assertThat(consent.getRegisteredClientId()).isEqualTo("some-client");
|
||||
assertThat(consent.getScopes())
|
||||
.containsExactlyInAnyOrder(
|
||||
"resource.read",
|
||||
"resource.write"
|
||||
);
|
||||
assertThat(consent.getAuthorities())
|
||||
.containsExactlyInAnyOrder(
|
||||
new SimpleGrantedAuthority("SCOPE_resource.read"),
|
||||
new SimpleGrantedAuthority("SCOPE_resource.write"),
|
||||
new SimpleGrantedAuthority("CLAIM_email")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromWhenAuthorizationConsentProvidedThenCopied() {
|
||||
OAuth2AuthorizationConsent previousConsent = OAuth2AuthorizationConsent
|
||||
.withId("some-client", "some-principal")
|
||||
.scope("first.scope")
|
||||
.scope("second.scope")
|
||||
.authority(new SimpleGrantedAuthority("CLAIM_email"))
|
||||
.build();
|
||||
|
||||
OAuth2AuthorizationConsent consent = OAuth2AuthorizationConsent.from(previousConsent).build();
|
||||
|
||||
assertThat(consent.getPrincipalName()).isEqualTo("some-principal");
|
||||
assertThat(consent.getRegisteredClientId()).isEqualTo("some-client");
|
||||
assertThat(consent.getAuthorities())
|
||||
.containsExactlyInAnyOrder(
|
||||
new SimpleGrantedAuthority("SCOPE_first.scope"),
|
||||
new SimpleGrantedAuthority("SCOPE_second.scope"),
|
||||
new SimpleGrantedAuthority("CLAIM_email")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void authoritiesThenCustomizesAuthorities() {
|
||||
OAuth2AuthorizationConsent consent = OAuth2AuthorizationConsent
|
||||
.withId("some-client", "some-user")
|
||||
.authority(new SimpleGrantedAuthority("some.authority"))
|
||||
.authorities(authorities -> {
|
||||
authorities.clear();
|
||||
authorities.add(new SimpleGrantedAuthority("other.authority"));
|
||||
})
|
||||
.build();
|
||||
|
||||
assertThat(consent.getAuthorities()).containsExactly(new SimpleGrantedAuthority("other.authority"));
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,13 @@
|
||||
*/
|
||||
package org.springframework.security.oauth2.server.authorization.web;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.Principal;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -28,7 +33,7 @@ import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
@@ -46,20 +51,24 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.TestRegisteredClients;
|
||||
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationCode;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -80,13 +89,15 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
private OAuth2AuthorizationEndpointFilter filter;
|
||||
private OAuth2AuthorizationConsentService consentService;
|
||||
private TestingAuthenticationToken authentication;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
this.registeredClientRepository = mock(RegisteredClientRepository.class);
|
||||
this.authorizationService = mock(OAuth2AuthorizationService.class);
|
||||
this.filter = new OAuth2AuthorizationEndpointFilter(this.registeredClientRepository, this.authorizationService);
|
||||
this.consentService = mock(OAuth2AuthorizationConsentService.class);
|
||||
this.filter = new OAuth2AuthorizationEndpointFilter(this.registeredClientRepository, this.authorizationService, this.consentService);
|
||||
this.authentication = new TestingAuthenticationToken("principalName", "password");
|
||||
this.authentication.setAuthenticated(true);
|
||||
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
|
||||
@@ -101,21 +112,28 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
|
||||
@Test
|
||||
public void constructorWhenRegisteredClientRepositoryNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new OAuth2AuthorizationEndpointFilter(null, this.authorizationService))
|
||||
assertThatThrownBy(() -> new OAuth2AuthorizationEndpointFilter(null, this.authorizationService, this.consentService))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("registeredClientRepository cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthorizationServiceNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new OAuth2AuthorizationEndpointFilter(this.registeredClientRepository, null))
|
||||
assertThatThrownBy(() -> new OAuth2AuthorizationEndpointFilter(this.registeredClientRepository, null, this.consentService))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("authorizationService cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenConsentServiceNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new OAuth2AuthorizationEndpointFilter(this.registeredClientRepository, this.authorizationService, (OAuth2AuthorizationConsentService) null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("consentService cannot be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void constructorWhenAuthorizationEndpointUriNullThenThrowIllegalArgumentException() {
|
||||
assertThatThrownBy(() -> new OAuth2AuthorizationEndpointFilter(this.registeredClientRepository, this.authorizationService, null))
|
||||
assertThatThrownBy(() -> new OAuth2AuthorizationEndpointFilter(this.registeredClientRepository, this.authorizationService, this.consentService, null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessage("authorizationEndpointUri cannot be empty");
|
||||
}
|
||||
@@ -468,7 +486,7 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
scopes.clear();
|
||||
scopes.add(OidcScopes.OPENID);
|
||||
})
|
||||
.clientSettings(ClientSettings::requireUserConsent)
|
||||
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
|
||||
.build();
|
||||
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
|
||||
doFilterWhenAuthorizationRequestThenAuthorizationResponse(registeredClient, request);
|
||||
@@ -570,7 +588,7 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserConsentRequiredAndAuthorizationRequestThenUserConsentResponse() throws Exception {
|
||||
public void doFilterWhenUserConsentRequiredAndAuthorizationRequestThenSavesAuthorization() throws Exception {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
|
||||
.build();
|
||||
@@ -583,11 +601,6 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
|
||||
assertThat(response.getContentType().equals(new MediaType("text", "html", StandardCharsets.UTF_8).toString()));
|
||||
|
||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
|
||||
|
||||
verify(this.authorizationService).save(authorizationCaptor.capture());
|
||||
@@ -617,6 +630,150 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
assertThat(authorizationRequest.getAdditionalParameters()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserConsentRequiredAndAuthorizationRequestThenUserConsentResponse() throws Exception {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.scopes(scopes -> {
|
||||
scopes.clear();
|
||||
scopes.add("message.read");
|
||||
scopes.add("message.write");
|
||||
})
|
||||
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
|
||||
.build();
|
||||
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
|
||||
assertThat(response.getContentType().equals(new MediaType("text", "html", StandardCharsets.UTF_8).toString()));
|
||||
|
||||
assertThat(response.getContentAsString()).contains(scopeCheckbox("message.read"));
|
||||
assertThat(response.getContentAsString()).contains(scopeCheckbox("message.write"));
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserConsentRequiredAndPreviouslyApprovedAndAuthorizationRequestThenUserConsentResponse() throws Exception {
|
||||
String unrelatedPreviouslyApprovedScope = "unrelated.scope";
|
||||
String previouslyApprovedScope = "message.read";
|
||||
String newScope = "message.write";
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.scopes(scopes -> {
|
||||
scopes.clear();
|
||||
scopes.add(previouslyApprovedScope);
|
||||
scopes.add(newScope);
|
||||
})
|
||||
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
|
||||
.build();
|
||||
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
|
||||
.thenReturn(registeredClient);
|
||||
OAuth2AuthorizationConsent previousConsent = createConsent(
|
||||
registeredClient.getClientId(),
|
||||
this.authentication.getName(),
|
||||
Arrays.asList(previouslyApprovedScope, unrelatedPreviouslyApprovedScope)
|
||||
);
|
||||
when(this.consentService.findById(
|
||||
eq(registeredClient.getClientId()),
|
||||
eq(this.authentication.getName())))
|
||||
.thenReturn(previousConsent);
|
||||
|
||||
|
||||
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
|
||||
assertThat(response.getContentType().equals(new MediaType("text", "html", StandardCharsets.UTF_8).toString()));
|
||||
|
||||
assertThat(response.getContentAsString()).contains(scopeCheckbox(newScope));
|
||||
assertThat(response.getContentAsString()).contains(disabledScopeCheckbox(previouslyApprovedScope));
|
||||
assertThat(response.getContentAsString()).doesNotContain(unrelatedPreviouslyApprovedScope);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserConsentRequiredAndCustomConsentUriAndAuthorizationRequestThenRedirects() throws Exception {
|
||||
this.filter.setUserConsentUri("/consent");
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.scopes(scopes -> {
|
||||
scopes.clear();
|
||||
scopes.add("message.read");
|
||||
scopes.add("message.write");
|
||||
})
|
||||
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
|
||||
.build();
|
||||
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
|
||||
|
||||
verify(this.authorizationService).save(authorizationCaptor.capture());
|
||||
OAuth2Authorization authorization = authorizationCaptor.getValue();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
|
||||
|
||||
String consentRedirectHeader = URLDecoder.decode(response.getHeader(HttpHeaders.LOCATION), StandardCharsets.UTF_8.name());
|
||||
UriComponents consentRedirectUri = UriComponentsBuilder.fromUriString(consentRedirectHeader).build();
|
||||
String[] redirectScopes = consentRedirectUri.getQueryParams().getFirst(OAuth2ParameterNames.SCOPE).split(" ");
|
||||
String redirectState = consentRedirectUri.getQueryParams().getFirst(OAuth2ParameterNames.STATE);
|
||||
|
||||
assertThat(consentRedirectUri.getPath()).isEqualTo("/consent");
|
||||
assertThat(consentRedirectUri.getQueryParams().getFirst(OAuth2ParameterNames.CLIENT_ID)).isEqualTo(registeredClient.getClientId());
|
||||
assertThat(redirectScopes).containsExactlyInAnyOrder("message.read", "message.write");
|
||||
assertThat(redirectState).isEqualTo(authorization.getAttribute(OAuth2ParameterNames.STATE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserConsentRequiredAndAllScopesPreviouslyApprovedAndAuthorizationRequestThenAuthorizationResponse() throws Exception {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.scopes(scopes -> {
|
||||
scopes.clear();
|
||||
scopes.add("message.read");
|
||||
scopes.add("message.write");
|
||||
})
|
||||
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
|
||||
.build();
|
||||
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
|
||||
.thenReturn(registeredClient);
|
||||
OAuth2AuthorizationConsent previousConsent = createConsent(
|
||||
registeredClient.getClientId(), this.authentication.getName(), Arrays.asList("message.read", "message.write")
|
||||
);
|
||||
when(this.consentService.findById(
|
||||
eq(registeredClient.getClientId()),
|
||||
eq(this.authentication.getName())))
|
||||
.thenReturn(previousConsent);
|
||||
|
||||
MockHttpServletRequest request = createAuthorizationRequest(registeredClient);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyNoInteractions(filterChain);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpStatus.FOUND.value());
|
||||
assertThat(response.getRedirectedUrl()).matches("https://example.com\\?code=.{15,}&state=state");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserConsentRequestMissingStateThenInvalidRequestError() throws Exception {
|
||||
doFilterWhenUserConsentRequestInvalidParameterThenError(
|
||||
@@ -838,6 +995,154 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
.isEqualTo(registeredClient.getScopes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserConsentRequestApprovedThenSaveConsent() throws Exception {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.scopes(scopes -> {
|
||||
scopes.clear();
|
||||
scopes.add("message.read");
|
||||
scopes.add("message.write");
|
||||
})
|
||||
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
|
||||
.build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
|
||||
.principalName(this.authentication.getName())
|
||||
.attributes(attrs -> attrs.remove(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME))
|
||||
.build();
|
||||
when(this.authorizationService.findByToken(eq("state"), eq(STATE_TOKEN_TYPE)))
|
||||
.thenReturn(authorization);
|
||||
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
MockHttpServletRequest request = createUserConsentRequest(registeredClient);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
ArgumentCaptor<OAuth2AuthorizationConsent> consentCaptor = ArgumentCaptor.forClass(OAuth2AuthorizationConsent.class);
|
||||
|
||||
verify(this.consentService).save(consentCaptor.capture());
|
||||
OAuth2AuthorizationConsent consent = consentCaptor.getValue();
|
||||
assertThat(consent.getPrincipalName()).isEqualTo(this.authentication.getName());
|
||||
assertThat(consent.getRegisteredClientId()).isEqualTo(registeredClient.getClientId());
|
||||
assertThat(consent.getScopes()).containsExactlyInAnyOrder("message.read", "message.write");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserConsentRequestApprovedAndNoScopesThenConsentNotSaved() throws Exception {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.scopes(Set::clear)
|
||||
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
|
||||
.build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
|
||||
.principalName(this.authentication.getName())
|
||||
.attributes(attrs -> attrs.remove(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME))
|
||||
.build();
|
||||
when(this.authorizationService.findByToken(eq("state"), eq(STATE_TOKEN_TYPE)))
|
||||
.thenReturn(authorization);
|
||||
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
|
||||
.thenReturn(registeredClient);
|
||||
|
||||
MockHttpServletRequest request = createUserConsentRequest(registeredClient);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(this.consentService, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserConsentRequestApprovedAndPreviousConsentExistsThenUpdatesConsent() throws Exception {
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.scopes(scopes -> {
|
||||
scopes.clear();
|
||||
scopes.add("message.read");
|
||||
scopes.add("message.write");
|
||||
})
|
||||
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
|
||||
.build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
|
||||
.principalName(this.authentication.getName())
|
||||
.attributes(attrs -> attrs.remove(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME))
|
||||
.build();
|
||||
when(this.authorizationService.findByToken(eq("state"), eq(STATE_TOKEN_TYPE)))
|
||||
.thenReturn(authorization);
|
||||
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
|
||||
.thenReturn(registeredClient);
|
||||
OAuth2AuthorizationConsent previousConsent =
|
||||
createConsent(
|
||||
registeredClient.getClientId(),
|
||||
this.authentication.getName(),
|
||||
Collections.singleton("message.read")
|
||||
);
|
||||
when(this.consentService.findById(
|
||||
eq(registeredClient.getClientId()),
|
||||
eq(this.authentication.getName())))
|
||||
.thenReturn(previousConsent);
|
||||
|
||||
MockHttpServletRequest request = createUserConsentRequest(registeredClient);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
ArgumentCaptor<OAuth2AuthorizationConsent> consentCaptor = ArgumentCaptor.forClass(OAuth2AuthorizationConsent.class);
|
||||
|
||||
verify(this.consentService).save(consentCaptor.capture());
|
||||
OAuth2AuthorizationConsent consent = consentCaptor.getValue();
|
||||
assertThat(consent.getPrincipalName()).isEqualTo(this.authentication.getName());
|
||||
assertThat(consent.getRegisteredClientId()).isEqualTo(registeredClient.getClientId());
|
||||
assertThat(consent.getScopes()).containsExactlyInAnyOrder("message.read", "message.write");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenUserConsentRequestApprovedAndPreviousConsentExistsThenSavesOAuth2Authorization() throws Exception {
|
||||
String newScope = "message.write";
|
||||
String previouslyApprovedScope = "message.read";
|
||||
String unrelatedPreviouslyApprovedScope = "unrelated.scope";
|
||||
RegisteredClient registeredClient = TestRegisteredClients.registeredClient()
|
||||
.scopes(scopes -> {
|
||||
scopes.clear();
|
||||
scopes.add(previouslyApprovedScope);
|
||||
scopes.add(newScope);
|
||||
})
|
||||
.clientSettings(clientSettings -> clientSettings.requireUserConsent(true))
|
||||
.build();
|
||||
OAuth2Authorization authorization = TestOAuth2Authorizations.authorization(registeredClient)
|
||||
.principalName(this.authentication.getName())
|
||||
.attributes(attrs -> attrs.remove(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME))
|
||||
.build();
|
||||
when(this.authorizationService.findByToken(eq("state"), eq(STATE_TOKEN_TYPE)))
|
||||
.thenReturn(authorization);
|
||||
when(this.registeredClientRepository.findByClientId((eq(registeredClient.getClientId()))))
|
||||
.thenReturn(registeredClient);
|
||||
OAuth2AuthorizationConsent previousConsent =
|
||||
createConsent(
|
||||
registeredClient.getClientId(),
|
||||
this.authentication.getName(),
|
||||
Arrays.asList(previouslyApprovedScope, unrelatedPreviouslyApprovedScope)
|
||||
);
|
||||
when(this.consentService.findById(
|
||||
eq(registeredClient.getClientId()),
|
||||
eq(this.authentication.getName())))
|
||||
.thenReturn(previousConsent);
|
||||
|
||||
MockHttpServletRequest request = createUserConsentRequest(registeredClient);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
this.filter.doFilter(request, response, filterChain);
|
||||
|
||||
ArgumentCaptor<OAuth2Authorization> authorizationCaptor = ArgumentCaptor.forClass(OAuth2Authorization.class);
|
||||
|
||||
verify(this.authorizationService).save(authorizationCaptor.capture());
|
||||
Set<String> savedAuthorizationScopes = authorizationCaptor.getValue().getAttribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME);
|
||||
assertThat(savedAuthorizationScopes).containsExactlyInAnyOrder(newScope, previouslyApprovedScope);
|
||||
assertThat(savedAuthorizationScopes).doesNotContain(unrelatedPreviouslyApprovedScope);
|
||||
}
|
||||
|
||||
// gh-243
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationRequestIPv4LoopbackRedirectUriAndDifferentPortThenAuthorizationResponse()
|
||||
@@ -1008,6 +1313,20 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
request.addParameter(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
|
||||
}
|
||||
|
||||
private static OAuth2AuthorizationConsent createConsent(
|
||||
String registeredClientId,
|
||||
String prinicpalName,
|
||||
Collection<String> scopes
|
||||
) {
|
||||
OAuth2AuthorizationConsent.Builder consentBuilder = OAuth2AuthorizationConsent
|
||||
.withId(registeredClientId, prinicpalName);
|
||||
for (String scope : scopes) {
|
||||
consentBuilder.scope(scope);
|
||||
}
|
||||
return consentBuilder.build();
|
||||
|
||||
}
|
||||
|
||||
private static MockHttpServletRequest createUserConsentRequest(RegisteredClient registeredClient) {
|
||||
String requestUri = OAuth2AuthorizationEndpointFilter.DEFAULT_AUTHORIZATION_ENDPOINT_URI;
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", requestUri);
|
||||
@@ -1024,4 +1343,18 @@ public class OAuth2AuthorizationEndpointFilterTests {
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private static String scopeCheckbox(String scope) {
|
||||
return MessageFormat.format(
|
||||
"<input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" value=\"{0}\" id=\"{0}\" checked>",
|
||||
scope
|
||||
);
|
||||
}
|
||||
|
||||
private static String disabledScopeCheckbox(String scope) {
|
||||
return MessageFormat.format(
|
||||
"<input class=\"form-check-input\" type=\"checkbox\" name=\"scope\" id=\"{0}\" checked disabled>",
|
||||
scope
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user