소스 검색

Allow configuration of oauth2 login through nested builder

Issue: gh-5557
Eleftheria Stein 6 년 전
부모
커밋
e47389e60b

+ 97 - 0
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.
+	 * <br>
+	 * <br>
+	 *
+	 * The &quot;authentication flow&quot; is implemented using the <b>Authorization Code Grant</b>, as specified in the
+	 * <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">OAuth 2.0 Authorization Framework</a>
+	 * and <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth">OpenID Connect Core 1.0</a>
+	 * specification.
+	 * <br>
+	 * <br>
+	 *
+	 * 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}.
+	 * <br>
+	 * <br>
+	 *
+	 * {@link org.springframework.security.oauth2.client.registration.ClientRegistration}(s) are composed within a
+	 * {@link org.springframework.security.oauth2.client.registration.ClientRegistrationRepository},
+	 * which is <b>required</b> and must be registered with the {@link ApplicationContext} or
+	 * configured via <code>oauth2Login().clientRegistrationRepository(..)</code>.
+	 * <br>
+	 * <br>
+	 *
+	 * The default configuration provides an auto-generated login page at <code>&quot;/login&quot;</code> and
+	 * redirects to <code>&quot;/login?error&quot;</code> when an authentication error occurs.
+	 * The login page will display each of the clients with a link
+	 * that is capable of initiating the &quot;authentication flow&quot;.
+	 * <br>
+	 * <br>
+	 *
+	 * <p>
+	 * <h2>Example Configuration</h2>
+	 *
+	 * The following example shows the minimal configuration required, using Google as the Authentication Provider.
+	 *
+	 * <pre>
+	 * &#064;Configuration
+	 * public class OAuth2LoginConfig {
+	 *
+	 * 	&#064;EnableWebSecurity
+	 * 	public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
+	 * 		&#064;Override
+	 * 		protected void configure(HttpSecurity http) throws Exception {
+	 * 			http
+	 * 				.authorizeRequests(authorizeRequests ->
+	 * 					authorizeRequests
+	 * 						.anyRequest().authenticated()
+	 * 				)
+	 * 				.oauth2Login(withDefaults());
+	 *		}
+	 *	}
+	 *
+	 *	&#064;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();
+	 *	}
+	 * }
+	 * </pre>
+	 *
+	 * <p>
+	 * For more advanced configuration, see {@link OAuth2LoginConfigurer} for available options to customize the defaults.
+	 *
+	 * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant</a>
+	 * @see <a target="_blank" href="https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth">Section 3.1 Authorization Code Flow</a>
+	 * @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<OAuth2LoginConfigurer<HttpSecurity>> oauth2LoginCustomizer) throws Exception {
+		oauth2LoginCustomizer.customize(getOrApply(new OAuth2LoginConfigurer<>()));
+		return HttpSecurity.this;
+	}
+
 	/**
 	 * Configures OAuth 2.0 Client support.
 	 *

+ 57 - 0
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<B extends HttpSecurityBuilder<B>> 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<B> authorizationEndpoint(Customizer<AuthorizationEndpointConfig> 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<B extends HttpSecurityBuilder<B>> 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<B> tokenEndpoint(Customizer<TokenEndpointConfig> 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<B extends HttpSecurityBuilder<B>> 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<B> redirectionEndpoint(Customizer<RedirectionEndpointConfig> 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<B extends HttpSecurityBuilder<B>> 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<B> userInfoEndpoint(Customizer<UserInfoEndpointConfig> userInfoEndpointCustomizer)
+			throws Exception {
+		userInfoEndpointCustomizer.customize(this.userInfoEndpointConfig);
+		return this;
+	}
+
 	/**
 	 * Configuration options for the Authorization Server's UserInfo Endpoint.
 	 */

+ 183 - 0
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<AuthenticationSuccessEvent> {
+		static List<AuthenticationSuccessEvent> 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 {