Allow configuration of oauth2 login through nested builder

Issue: gh-5557
This commit is contained in:
Eleftheria Stein 2019-07-09 13:00:27 -04:00
parent bf1bbd14e9
commit e47389e60b
3 changed files with 337 additions and 0 deletions

View File

@ -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.
*

View File

@ -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.
*/

View File

@ -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 {