mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-26 22:02:41 +00:00
Allow configuration of oauth2 login through nested builder
Issue: gh-5557
This commit is contained in:
parent
bf1bbd14e9
commit
e47389e60b
@ -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 "authentication flow" 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>"/login"</code> and
|
||||
* redirects to <code>"/login?error"</code> 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".
|
||||
* <br>
|
||||
* <br>
|
||||
*
|
||||
* <p>
|
||||
* <h2>Example Configuration</h2>
|
||||
*
|
||||
* The following example shows the minimal configuration required, using Google as the Authentication Provider.
|
||||
*
|
||||
* <pre>
|
||||
* @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();
|
||||
* }
|
||||
* }
|
||||
* </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.
|
||||
*
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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 {
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user