Test for authorization code flow with consent

This commit is contained in:
Andreas Svanberg 2025-04-16 21:10:05 +02:00
parent ea5c3a1c00
commit 1b08cdaf44
Signed by: ansv7779
GPG Key ID: 729B051CFFD42F92
2 changed files with 121 additions and 0 deletions
src
main/java/se/su/dsv/oauth2
test/java/se/su/dsv/oauth2

@ -9,13 +9,16 @@ import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
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.OAuth2AuthorizationConsentAuthenticationProvider;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
@ -81,6 +84,9 @@ public class AuthorizationServer extends SpringBootServletInitializer {
http
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
.with(authorizationServerConfigurer, authorizationServer -> authorizationServer
.authorizationEndpoint(authorizeEndpoint -> authorizeEndpoint
.consentPage("/oauth2/consent"))
.withObjectPostProcessor(enableGivingConsentWithNoScopes())
.oidc(Customizer.withDefaults()))
.with(new ShibbolethConfigurer(), Customizer.withDefaults())
.sessionManagement(session -> session
@ -105,6 +111,25 @@ public class AuthorizationServer extends SpringBootServletInitializer {
return http.build();
}
/// The [OAuth2AuthorizationConsentAuthenticationProvider], which is the component
/// that handles incoming consent, requires there to be at least one scope that is
/// approved by the user. This is a problem since we want to allow the user to give
/// consent without any scopes to enable, for example, just using this service as a
/// way to authenticate the end user (getting access to the "sub" claim).
///
/// To get around this limitation, a dummy scope is added that is always approved.
private ObjectPostProcessor<OAuth2AuthorizationConsentAuthenticationProvider> enableGivingConsentWithNoScopes() {
return new ObjectPostProcessor<>() {
@Override
public <O extends OAuth2AuthorizationConsentAuthenticationProvider> O postProcess(final O object) {
object.setAuthorizationConsentCustomizer(consentContext ->
consentContext.getAuthorizationConsent()
.authority(new SimpleGrantedAuthority("CONSENT")));
return object;
}
};
}
public interface HttpSecurityCustomizer {
void customize(HttpSecurity http) throws Exception;
}

@ -0,0 +1,96 @@
package se.su.dsv.oauth2;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.util.UriComponents;
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.assertNotNull;
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
public class ConsentFlowTest extends AbstractMetadataTest {
@ServiceConnection
static MariaDBContainer<?> mariaDBContainer = new MariaDBContainer<>("mariadb:10.11");
@Test
public void asks_end_user_for_consent() throws Exception {
MvcResult authorizeResult = mockMvc.perform(get(getAuthorizationEndpoint())
.with(remoteUser("some-end-user"))
.queryParam("response_type", "code")
.queryParam("client_id", TestConfig.CLIENT_ID)
.queryParam("redirect_uri", TestConfig.REDIRECT_URI))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("**/oauth2/consent?**"))
.andReturn();
UriComponents redirectUrl = parseUrl(authorizeResult.getResponse().getRedirectedUrl());
String state = redirectUrl.getQueryParams().getFirst("state");
String clientId = redirectUrl.getQueryParams().getFirst("client_id");
MvcResult consentResult = mockMvc.perform(post(getAuthorizationEndpoint())
.with(remoteUser("some-end-user"))
.formField("client_id", clientId)
.formField("state", state))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern(TestConfig.REDIRECT_URI + "?**"))
.andReturn();
UriComponents callbackUrl = parseUrl(consentResult.getResponse().getRedirectedUrl());
String code = callbackUrl.getQueryParams().getFirst("code");
assertNotNull(code, "Should have received a one time authorization code");
}
private static UriComponents parseUrl(final String url) {
assertNotNull(url);
String decodedUrl = URLDecoder.decode(url, StandardCharsets.UTF_8);
return UriComponentsBuilder
.fromUriString(decodedUrl)
.build();
}
@TestConfiguration
public static class TestConfig {
public static final String CLIENT_ID = "client";
public static final String CLIENT_SECRET = "secret";
public static final String REDIRECT_URI = "http://localhost/login/oauth2/code/client";
@Bean
@Primary
// Required to override the default RegisteredClientRepository
InMemoryRegisteredClientRepository testRegisteredClientRepository()
{
RegisteredClient registeredClient = RegisteredClient.withId("id")
.clientId(CLIENT_ID)
.clientSecret("{noop}" + CLIENT_SECRET)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri(REDIRECT_URI)
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(true)
.build())
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
}
}