Implement support for user consent #4
@ -9,13 +9,16 @@ import org.springframework.context.annotation.Profile;
|
|||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.config.Customizer;
|
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.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.core.GrantedAuthority;
|
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.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
|
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
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.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
||||||
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
|
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
|
||||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
|
||||||
@ -81,6 +84,9 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
|||||||
http
|
http
|
||||||
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
|
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
|
||||||
.with(authorizationServerConfigurer, authorizationServer -> authorizationServer
|
.with(authorizationServerConfigurer, authorizationServer -> authorizationServer
|
||||||
|
.authorizationEndpoint(authorizeEndpoint -> authorizeEndpoint
|
||||||
|
.consentPage("/oauth2/consent"))
|
||||||
|
.withObjectPostProcessor(enableGivingConsentWithNoScopes())
|
||||||
.oidc(Customizer.withDefaults()))
|
.oidc(Customizer.withDefaults()))
|
||||||
.with(new ShibbolethConfigurer(), Customizer.withDefaults())
|
.with(new ShibbolethConfigurer(), Customizer.withDefaults())
|
||||||
.sessionManagement(session -> session
|
.sessionManagement(session -> session
|
||||||
@ -105,6 +111,25 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
|||||||
return http.build();
|
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 {
|
public interface HttpSecurityCustomizer {
|
||||||
void customize(HttpSecurity http) throws Exception;
|
void customize(HttpSecurity http) throws Exception;
|
||||||
}
|
}
|
||||||
|
|||||||
96
src/test/java/se/su/dsv/oauth2/ConsentFlowTest.java
Normal file
96
src/test/java/se/su/dsv/oauth2/ConsentFlowTest.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user