Skip consent for developers in staging
With the custom authorization in place, the current user is always the developer while the authorization request token may contain a custom principal. When Spring Authorization Server attempts to validate the submitted consent, it checks that the current user is the same as the authorization request token - which it is not. The easiest solution is to disable consent in staging for developers.
This commit is contained in:
parent
119e27f5da
commit
f1fbd306e2
src
main/java/se/su/dsv/oauth2
test/java/se/su/dsv/oauth2
@ -18,6 +18,7 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
|
||||
@ -31,11 +32,12 @@ import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||
import se.su.dsv.oauth2.shibboleth.Entitlement;
|
||||
import se.su.dsv.oauth2.shibboleth.ShibbolethConfigurer;
|
||||
import se.su.dsv.oauth2.shibboleth.ShibbolethTokenPopulator;
|
||||
import se.su.dsv.oauth2.staging.SkipConsentForDevelopers;
|
||||
import se.su.dsv.oauth2.staging.StagingSecurityConfigurer;
|
||||
import se.su.dsv.oauth2.web.client.DeveloperAccessCheck;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@SpringBootApplication(proxyBeanMethods = false)
|
||||
@EnableWebSecurity
|
||||
@ -75,7 +77,9 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
||||
/// the session and allow the flow to start.
|
||||
@Bean
|
||||
@Order(1)
|
||||
public SecurityFilterChain oauth2Endpoints(HttpSecurity http, Optional<HttpSecurityCustomizer> customizer)
|
||||
public SecurityFilterChain oauth2Endpoints(
|
||||
HttpSecurity http,
|
||||
List<HttpSecurityCustomizer> customizer)
|
||||
throws Exception
|
||||
{
|
||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
||||
@ -100,7 +104,7 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
||||
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
|
||||
));
|
||||
|
||||
customizer.ifPresent(c -> {
|
||||
customizer.forEach(c -> {
|
||||
try {
|
||||
c.customize(http);
|
||||
} catch (Exception e) {
|
||||
@ -130,6 +134,27 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
||||
};
|
||||
}
|
||||
|
||||
/// Disable consent in staging. With the custom authorization in place, the current user is always the developer
|
||||
/// while the authorization request token may contain a custom principal. When Spring Authorization Server attempts
|
||||
/// to validate the submitted consent, it checks that the current user is the same as the authorization request
|
||||
/// token - which it is not. The easiest solution is to disable consent in staging for developers.
|
||||
@Bean
|
||||
@Profile("staging")
|
||||
public HttpSecurityCustomizer disableConsentInStaging(
|
||||
DeveloperAccessCheck developerAccessCheck)
|
||||
{
|
||||
ObjectPostProcessor<OAuth2AuthorizationCodeRequestAuthenticationProvider> postprocessor = new ObjectPostProcessor<>() {
|
||||
@Override
|
||||
public <O extends OAuth2AuthorizationCodeRequestAuthenticationProvider> O postProcess(final O object) {
|
||||
object.setAuthorizationConsentRequired(new SkipConsentForDevelopers(developerAccessCheck));
|
||||
return object;
|
||||
}
|
||||
};
|
||||
return http -> http
|
||||
.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.withObjectPostProcessor(postprocessor);
|
||||
}
|
||||
|
||||
public interface HttpSecurityCustomizer {
|
||||
void customize(HttpSecurity http) throws Exception;
|
||||
}
|
||||
@ -205,4 +230,5 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
package se.su.dsv.oauth2.staging;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class SkipConsentForDevelopers implements Predicate<OAuth2AuthorizationCodeRequestAuthenticationContext> {
|
||||
private final Predicate<Authentication> developerAccessCheck;
|
||||
|
||||
public SkipConsentForDevelopers(final Predicate<Authentication> developerAccessCheck) {
|
||||
this.developerAccessCheck = developerAccessCheck;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(final OAuth2AuthorizationCodeRequestAuthenticationContext context) {
|
||||
Authentication currentUser = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (developerAccessCheck.test(currentUser)) {
|
||||
return false;
|
||||
} else {
|
||||
// TODO maybe check if consent is already given? useful to always request consent during testing?
|
||||
return context.getRegisteredClient().getClientSettings().isRequireAuthorizationConsent();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
package se.su.dsv.oauth2;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.nimbusds.jwt.JWTClaimsSet;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
import org.testcontainers.containers.MariaDBContainer;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
import static se.su.dsv.oauth2.ShibbolethRequestProcessor.remoteUser;
|
||||
|
||||
@SpringBootTest(
|
||||
properties = {
|
||||
"se.su.dsv.oauth2.developer-entitlement=" + ConsentFlowCustomAuthorizationTest.DEVELOPER_ENTITLEMENT
|
||||
},
|
||||
classes = ConsentFlowTest.TestConfig.class)
|
||||
@ActiveProfiles("staging")
|
||||
public class ConsentFlowCustomAuthorizationTest extends AbstractMetadataTest {
|
||||
public static final String DEVELOPER_ENTITLEMENT = "developer";
|
||||
|
||||
@ServiceConnection
|
||||
static MariaDBContainer<?> mariaDBContainer = new MariaDBContainer<>("mariadb:10.11");
|
||||
|
||||
@Test
|
||||
public void consent_is_disabled_for_developers() throws Exception {
|
||||
String developerPrincipal = "master-of-the-universe";
|
||||
String customPrincipal = "lowly-regular-user";
|
||||
|
||||
MvcResult authorizationResult = mockMvc.perform(post(getAuthorizationEndpoint())
|
||||
.with(remoteUser(developerPrincipal)
|
||||
.entitlement(DEVELOPER_ENTITLEMENT))
|
||||
.queryParam("client_id", ConsentFlowTest.TestConfig.CLIENT_ID)
|
||||
.queryParam("response_type", "code")
|
||||
.formField("principal", customPrincipal))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrlPattern(ConsentFlowTest.TestConfig.REDIRECT_URI + "?*"))
|
||||
.andReturn();
|
||||
|
||||
String redirectUrl = getRedirectUrl(authorizationResult);
|
||||
String authorizationCode = UriComponentsBuilder.fromUriString(redirectUrl)
|
||||
.build()
|
||||
.getQueryParams()
|
||||
.getFirst("code");
|
||||
|
||||
assertNotNull(authorizationCode, "Should have received an authorization code");
|
||||
|
||||
MvcResult tokenResult = mockMvc.perform(post(getTokenEndpoint())
|
||||
.with(httpBasic(ConsentFlowTest.TestConfig.CLIENT_ID, ConsentFlowTest.TestConfig.CLIENT_SECRET))
|
||||
.formField("grant_type", "authorization_code")
|
||||
.formField("code", authorizationCode))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").exists())
|
||||
.andReturn();
|
||||
|
||||
JsonNode tokenResponse = objectMapper.readTree(tokenResult.getResponse().getContentAsString());
|
||||
JWTClaimsSet claims = verifyToken(tokenResponse.required("access_token").asText());
|
||||
|
||||
assertEquals(customPrincipal, claims.getSubject());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regular_users_are_prompted_for_consent() throws Exception {
|
||||
String regularPrincipal = "lowly-regular-user";
|
||||
|
||||
MvcResult authorizationResult = mockMvc.perform(get(getAuthorizationEndpoint())
|
||||
.with(remoteUser(regularPrincipal))
|
||||
.queryParam("client_id", ConsentFlowTest.TestConfig.CLIENT_ID)
|
||||
.queryParam("response_type", "code"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrlPattern("**/oauth2/consent?*"))
|
||||
.andReturn();
|
||||
|
||||
String redirectUrl = getRedirectUrl(authorizationResult);
|
||||
MultiValueMap<String, String> queryParams = UriComponentsBuilder.fromUriString(redirectUrl)
|
||||
.build()
|
||||
.getQueryParams();
|
||||
|
||||
mockMvc.perform(post(getAuthorizationEndpoint())
|
||||
.with(remoteUser(regularPrincipal))
|
||||
.formField("client_id", queryParams.getFirst("client_id"))
|
||||
.formField("state", queryParams.getFirst("state")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(redirectedUrlPattern(ConsentFlowTest.TestConfig.REDIRECT_URI + "?*"));
|
||||
}
|
||||
|
||||
private static String getRedirectUrl(final MvcResult result) {
|
||||
String redirectedUrl = result.getResponse().getRedirectedUrl();
|
||||
assertNotNull(redirectedUrl, "Should have gotten a redirect URL");
|
||||
return URLDecoder.decode(redirectedUrl, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user