From e47389e60b660e33a472e7671a3576a36479e619 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein Date: Tue, 9 Jul 2019 13:00:27 -0400 Subject: [PATCH] Allow configuration of oauth2 login through nested builder Issue: gh-5557 --- .../annotation/web/builders/HttpSecurity.java | 97 ++++++++++ .../oauth2/client/OAuth2LoginConfigurer.java | 57 ++++++ .../client/OAuth2LoginConfigurerTests.java | 183 ++++++++++++++++++ 3 files changed, 337 insertions(+) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index d1ab52e23c..6e5b4c8281 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -1948,6 +1948,103 @@ public final class HttpSecurity extends return getOrApply(new OAuth2LoginConfigurer<>()); } + /** + * Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider. + *
+ *
+ * + * The "authentication flow" is implemented using the Authorization Code Grant, as specified in the + * OAuth 2.0 Authorization Framework + * and OpenID Connect Core 1.0 + * specification. + *
+ *
+ * + * As a prerequisite to using this feature, you must register a client with a provider. + * The client registration information may than be used for configuring + * a {@link org.springframework.security.oauth2.client.registration.ClientRegistration} using a + * {@link org.springframework.security.oauth2.client.registration.ClientRegistration.Builder}. + *
+ *
+ * + * {@link org.springframework.security.oauth2.client.registration.ClientRegistration}(s) are composed within a + * {@link org.springframework.security.oauth2.client.registration.ClientRegistrationRepository}, + * which is required and must be registered with the {@link ApplicationContext} or + * configured via oauth2Login().clientRegistrationRepository(..). + *
+ *
+ * + * The default configuration provides an auto-generated login page at "/login" and + * redirects to "/login?error" when an authentication error occurs. + * The login page will display each of the clients with a link + * that is capable of initiating the "authentication flow". + *
+ *
+ * + *

+ *

Example Configuration

+ * + * The following example shows the minimal configuration required, using Google as the Authentication Provider. + * + *
+	 * @Configuration
+	 * public class OAuth2LoginConfig {
+	 *
+	 * 	@EnableWebSecurity
+	 * 	public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
+	 * 		@Override
+	 * 		protected void configure(HttpSecurity http) throws Exception {
+	 * 			http
+	 * 				.authorizeRequests(authorizeRequests ->
+	 * 					authorizeRequests
+	 * 						.anyRequest().authenticated()
+	 * 				)
+	 * 				.oauth2Login(withDefaults());
+	 *		}
+	 *	}
+	 *
+	 *	@Bean
+	 *	public ClientRegistrationRepository clientRegistrationRepository() {
+	 *		return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
+	 *	}
+	 *
+	 * 	private ClientRegistration googleClientRegistration() {
+	 * 		return ClientRegistration.withRegistrationId("google")
+	 * 			.clientId("google-client-id")
+	 * 			.clientSecret("google-client-secret")
+	 * 			.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
+	 * 			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+	 * 			.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
+	 * 			.scope("openid", "profile", "email", "address", "phone")
+	 * 			.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
+	 * 			.tokenUri("https://www.googleapis.com/oauth2/v4/token")
+	 * 			.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
+	 * 			.userNameAttributeName(IdTokenClaimNames.SUB)
+	 * 			.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
+	 * 			.clientName("Google")
+	 * 			.build();
+	 *	}
+	 * }
+	 * 
+ * + *

+ * For more advanced configuration, see {@link OAuth2LoginConfigurer} for available options to customize the defaults. + * + * @see Section 4.1 Authorization Code Grant + * @see Section 3.1 Authorization Code Flow + * @see org.springframework.security.oauth2.client.registration.ClientRegistration + * @see org.springframework.security.oauth2.client.registration.ClientRegistrationRepository + * + * @param oauth2LoginCustomizer the {@link Customizer} to provide more options for + * the {@link OAuth2LoginConfigurer} + * @return the {@link HttpSecurity} for further customizations + * @throws Exception + */ + public HttpSecurity oauth2Login(Customizer> oauth2LoginCustomizer) throws Exception { + oauth2LoginCustomizer.customize(getOrApply(new OAuth2LoginConfigurer<>())); + return HttpSecurity.this; + } + /** * Configures OAuth 2.0 Client support. * diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 5e0bb87a2f..b54b522591 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.core.ResolvableType; import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; @@ -201,6 +202,20 @@ public final class OAuth2LoginConfigurer> exten return this.authorizationEndpointConfig; } + /** + * Configures the Authorization Server's Authorization Endpoint. + * + * @param authorizationEndpointCustomizer the {@link Customizer} to provide more options for + * the {@link AuthorizationEndpointConfig} + * @return the {@link OAuth2LoginConfigurer} for further customizations + * @throws Exception + */ + public OAuth2LoginConfigurer authorizationEndpoint(Customizer authorizationEndpointCustomizer) + throws Exception { + authorizationEndpointCustomizer.customize(this.authorizationEndpointConfig); + return this; + } + /** * Configuration options for the Authorization Server's Authorization Endpoint. */ @@ -268,6 +283,20 @@ public final class OAuth2LoginConfigurer> exten return this.tokenEndpointConfig; } + /** + * Configures the Authorization Server's Token Endpoint. + * + * @param tokenEndpointCustomizer the {@link Customizer} to provide more options for + * the {@link TokenEndpointConfig} + * @return the {@link OAuth2LoginConfigurer} for further customizations + * @throws Exception + */ + public OAuth2LoginConfigurer tokenEndpoint(Customizer tokenEndpointCustomizer) + throws Exception { + tokenEndpointCustomizer.customize(this.tokenEndpointConfig); + return this; + } + /** * Configuration options for the Authorization Server's Token Endpoint. */ @@ -310,6 +339,20 @@ public final class OAuth2LoginConfigurer> exten return this.redirectionEndpointConfig; } + /** + * Configures the Client's Redirection Endpoint. + * + * @param redirectionEndpointCustomizer the {@link Customizer} to provide more options for + * the {@link RedirectionEndpointConfig} + * @return the {@link OAuth2LoginConfigurer} for further customizations + * @throws Exception + */ + public OAuth2LoginConfigurer redirectionEndpoint(Customizer redirectionEndpointCustomizer) + throws Exception { + redirectionEndpointCustomizer.customize(this.redirectionEndpointConfig); + return this; + } + /** * Configuration options for the Client's Redirection Endpoint. */ @@ -350,6 +393,20 @@ public final class OAuth2LoginConfigurer> exten return this.userInfoEndpointConfig; } + /** + * Configures the Authorization Server's UserInfo Endpoint. + * + * @param userInfoEndpointCustomizer the {@link Customizer} to provide more options for + * the {@link UserInfoEndpointConfig} + * @return the {@link OAuth2LoginConfigurer} for further customizations + * @throws Exception + */ + public OAuth2LoginConfigurer userInfoEndpoint(Customizer userInfoEndpointCustomizer) + throws Exception { + userInfoEndpointCustomizer.customize(this.userInfoEndpointConfig); + return this; + } + /** * Configuration options for the Authorization Server's UserInfo Endpoint. */ diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java index f33c9d0201..a61a99f872 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurerTests.java @@ -176,6 +176,25 @@ public class OAuth2LoginConfigurerTests { .isInstanceOf(OAuth2UserAuthority.class).hasToString("ROLE_USER"); } + @Test + public void requestWhenOauth2LoginInLambdaThenAuthenticationContainsOauth2UserAuthority() throws Exception { + loadConfig(OAuth2LoginInLambdaConfig.class); + OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest(); + this.authorizationRequestRepository.saveAuthorizationRequest( + authorizationRequest, this.request, this.response); + this.request.setParameter("code", "code123"); + this.request.setParameter("state", authorizationRequest.getState()); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + Authentication authentication = this.securityContextRepository + .loadContext(new HttpRequestResponseHolder(this.request, this.response)) + .getAuthentication(); + assertThat(authentication.getAuthorities()).hasSize(1); + assertThat(authentication.getAuthorities()).first() + .isInstanceOf(OAuth2UserAuthority.class).hasToString("ROLE_USER"); + } + // gh-6009 @Test public void oauth2LoginWhenSuccessThenAuthenticationSuccessEventPublished() throws Exception { @@ -303,6 +322,29 @@ public class OAuth2LoginConfigurerTests { assertThat(this.response.getRedirectedUrl()).isEqualTo("https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=clientId&scope=openid+profile+email&state=state&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fgoogle&custom-param1=custom-value1"); } + @Test + public void requestWhenOauth2LoginWithCustomAuthorizationRequestParametersThenParametersInRedirectedUrl() + throws Exception { + loadConfig(OAuth2LoginConfigCustomAuthorizationRequestResolverInLambda.class); + OAuth2AuthorizationRequestResolver resolver = this.context.getBean( + OAuth2LoginConfigCustomAuthorizationRequestResolverInLambda.class).resolver; + OAuth2AuthorizationRequest result = OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri("https://accounts.google.com/authorize") + .clientId("client-id") + .state("adsfa") + .authorizationRequestUri("https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=clientId&scope=openid+profile+email&state=state&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fgoogle&custom-param1=custom-value1") + .build(); + when(resolver.resolve(any())).thenReturn(result); + + String requestUri = "/oauth2/authorization/google"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).isEqualTo("https://accounts.google.com/o/oauth2/v2/auth?response_type=code&client_id=clientId&scope=openid+profile+email&state=state&redirect_uri=http%3A%2F%2Flocalhost%2Flogin%2Foauth2%2Fcode%2Fgoogle&custom-param1=custom-value1"); + } + // gh-5347 @Test public void oauth2LoginWithOneClientConfiguredThenRedirectForAuthorization() throws Exception { @@ -374,6 +416,19 @@ public class OAuth2LoginConfigurerTests { assertThat(this.response.getRedirectedUrl()).matches("http://localhost/custom-login"); } + @Test + public void requestWhenOauth2LoginWithCustomLoginPageInLambdaThenRedirectCustomLoginPage() throws Exception { + loadConfig(OAuth2LoginConfigCustomLoginPageInLambda.class); + + String requestUri = "/"; + this.request = new MockHttpServletRequest("GET", requestUri); + this.request.setServletPath(requestUri); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + assertThat(this.response.getRedirectedUrl()).matches("http://localhost/custom-login"); + } + @Test public void oidcLogin() throws Exception { // setup application context @@ -400,6 +455,32 @@ public class OAuth2LoginConfigurerTests { .isInstanceOf(OidcUserAuthority.class).hasToString("ROLE_USER"); } + @Test + public void requestWhenOauth2LoginInLambdaAndOidcThenAuthenticationContainsOidcUserAuthority() throws Exception { + // setup application context + loadConfig(OAuth2LoginInLambdaConfig.class, JwtDecoderFactoryConfig.class); + + // setup authorization request + OAuth2AuthorizationRequest authorizationRequest = createOAuth2AuthorizationRequest("openid"); + this.authorizationRequestRepository.saveAuthorizationRequest( + authorizationRequest, this.request, this.response); + + // setup authentication parameters + this.request.setParameter("code", "code123"); + this.request.setParameter("state", authorizationRequest.getState()); + + // perform test + this.springSecurityFilterChain.doFilter(this.request, this.response, this.filterChain); + + // assertions + Authentication authentication = this.securityContextRepository + .loadContext(new HttpRequestResponseHolder(this.request, this.response)) + .getAuthentication(); + assertThat(authentication.getAuthorities()).hasSize(1); + assertThat(authentication.getAuthorities()).first() + .isInstanceOf(OidcUserAuthority.class).hasToString("ROLE_USER"); + } + @Test public void oidcLoginCustomWithConfigurer() throws Exception { // setup application context @@ -521,6 +602,30 @@ public class OAuth2LoginConfigurerTests { } } + @EnableWebSecurity + static class OAuth2LoginInLambdaConfig extends CommonLambdaWebSecurityConfigurerAdapter + implements ApplicationListener { + static List EVENTS = new ArrayList<>(); + + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .oauth2Login(oauth2Login -> + oauth2Login + .clientRegistrationRepository( + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)) + ); + // @formatter:on + super.configure(http); + } + + @Override + public void onApplicationEvent(AuthenticationSuccessEvent event) { + EVENTS.add(event); + } + } + @EnableWebSecurity static class OAuth2LoginConfigCustomWithConfigurer extends CommonWebSecurityConfigurerAdapter { @Override @@ -586,6 +691,28 @@ public class OAuth2LoginConfigurerTests { } } + @EnableWebSecurity + static class OAuth2LoginConfigCustomAuthorizationRequestResolverInLambda extends CommonLambdaWebSecurityConfigurerAdapter { + private ClientRegistrationRepository clientRegistrationRepository = + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION); + + OAuth2AuthorizationRequestResolver resolver = mock(OAuth2AuthorizationRequestResolver.class); + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .oauth2Login(oauth2Login -> + oauth2Login + .clientRegistrationRepository(this.clientRegistrationRepository) + .authorizationEndpoint(authorizationEndpoint -> + authorizationEndpoint + .authorizationRequestResolver(this.resolver) + ) + ); + super.configure(http); + } + } + @EnableWebSecurity static class OAuth2LoginConfigMultipleClients extends CommonWebSecurityConfigurerAdapter { @Override @@ -612,6 +739,23 @@ public class OAuth2LoginConfigurerTests { } } + @EnableWebSecurity + static class OAuth2LoginConfigCustomLoginPageInLambda extends CommonLambdaWebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .oauth2Login(oauth2Login -> + oauth2Login + .clientRegistrationRepository( + new InMemoryClientRegistrationRepository(GOOGLE_CLIENT_REGISTRATION)) + .loginPage("/custom-login") + ); + // @formatter:on + super.configure(http); + } + } + @EnableWebSecurity static class OAuth2LoginConfigWithOidcLogoutSuccessHandler extends CommonWebSecurityConfigurerAdapter { @Override @@ -667,6 +811,45 @@ public class OAuth2LoginConfigurerTests { } } + private static abstract class CommonLambdaWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .anyRequest().authenticated() + ) + .securityContext(securityContext -> + securityContext + .securityContextRepository(securityContextRepository()) + ) + .oauth2Login(oauth2Login -> + oauth2Login + .tokenEndpoint(tokenEndpoint -> + tokenEndpoint + .accessTokenResponseClient(createOauth2AccessTokenResponseClient()) + ) + .userInfoEndpoint(userInfoEndpoint -> + userInfoEndpoint + .userService(createOauth2UserService()) + .oidcUserService(createOidcUserService()) + ) + ); + // @formatter:on + } + + @Bean + SecurityContextRepository securityContextRepository() { + return new HttpSessionSecurityContextRepository(); + } + + @Bean + HttpSessionOAuth2AuthorizationRequestRepository oauth2AuthorizationRequestRepository() { + return new HttpSessionOAuth2AuthorizationRequestRepository(); + } + } + @Configuration static class JwtDecoderFactoryConfig {