diff --git a/config/src/main/java/org/springframework/security/config/Elements.java b/config/src/main/java/org/springframework/security/config/Elements.java index 6684c67c0d..8553acdb3b 100644 --- a/config/src/main/java/org/springframework/security/config/Elements.java +++ b/config/src/main/java/org/springframework/security/config/Elements.java @@ -74,5 +74,6 @@ public abstract class Elements { public static final String INTERCEPT_MESSAGE = "intercept-message"; public static final String OAUTH2_LOGIN = "oauth2-login"; + public static final String OAUTH2_CLIENT = "oauth2-client"; public static final String CLIENT_REGISTRATIONS = "client-registrations"; } diff --git a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java index 8152497c97..c2c7f53691 100644 --- a/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java @@ -15,8 +15,6 @@ */ package org.springframework.security.config.http; -import static org.springframework.security.config.http.SecurityFilters.*; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanMetadataElement; @@ -59,9 +57,27 @@ import org.w3c.dom.Element; import javax.servlet.http.HttpServletRequest; import java.security.SecureRandom; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.function.Function; +import static org.springframework.security.config.http.SecurityFilters.ANONYMOUS_FILTER; +import static org.springframework.security.config.http.SecurityFilters.BASIC_AUTH_FILTER; +import static org.springframework.security.config.http.SecurityFilters.EXCEPTION_TRANSLATION_FILTER; +import static org.springframework.security.config.http.SecurityFilters.FORM_LOGIN_FILTER; +import static org.springframework.security.config.http.SecurityFilters.LOGIN_PAGE_FILTER; +import static org.springframework.security.config.http.SecurityFilters.LOGOUT_FILTER; +import static org.springframework.security.config.http.SecurityFilters.LOGOUT_PAGE_FILTER; +import static org.springframework.security.config.http.SecurityFilters.OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER; +import static org.springframework.security.config.http.SecurityFilters.OAUTH2_AUTHORIZATION_REQUEST_FILTER; +import static org.springframework.security.config.http.SecurityFilters.OAUTH2_LOGIN_FILTER; +import static org.springframework.security.config.http.SecurityFilters.OPENID_FILTER; +import static org.springframework.security.config.http.SecurityFilters.PRE_AUTH_FILTER; +import static org.springframework.security.config.http.SecurityFilters.REMEMBER_ME_FILTER; +import static org.springframework.security.config.http.SecurityFilters.X509_FILTER; + /** * Handles creation of authentication mechanism filters and related beans for <http> * parsing. @@ -145,6 +161,10 @@ final class AuthenticationConfigBuilder { private BeanReference oauth2LoginOidcAuthenticationProviderRef; private BeanDefinition oauth2LoginLinks; + private BeanDefinition authorizationRequestRedirectFilter; + private BeanDefinition authorizationCodeGrantFilter; + private BeanReference authorizationCodeAuthenticationProviderRef; + AuthenticationConfigBuilder(Element element, boolean forceAutoConfig, ParserContext pc, SessionCreationPolicy sessionPolicy, BeanReference requestCache, BeanReference authenticationManager, @@ -166,6 +186,7 @@ final class AuthenticationConfigBuilder { createBasicFilter(authenticationManager); createFormLoginFilter(sessionStrategy, authenticationManager); createOAuth2LoginFilter(sessionStrategy, authenticationManager); + createOAuth2ClientFilter(requestCache, authenticationManager); createOpenIDLoginFilter(sessionStrategy, authenticationManager); createX509Filter(authenticationManager); createJeeFilter(authenticationManager); @@ -283,6 +304,36 @@ final class AuthenticationConfigBuilder { oauth2LoginOidcAuthenticationProviderRef = new RuntimeBeanReference(oauth2LoginOidcAuthProviderId); } + void createOAuth2ClientFilter(BeanReference requestCache, BeanReference authenticationManager) { + Element oauth2ClientElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OAUTH2_CLIENT); + if (oauth2ClientElt == null) { + return; + } + + OAuth2ClientBeanDefinitionParser parser = new OAuth2ClientBeanDefinitionParser( + requestCache, authenticationManager); + parser.parse(oauth2ClientElt, this.pc); + + this.authorizationRequestRedirectFilter = parser.getAuthorizationRequestRedirectFilter(); + String authorizationRequestRedirectFilterId = pc.getReaderContext() + .generateBeanName(this.authorizationRequestRedirectFilter); + this.pc.registerBeanComponent(new BeanComponentDefinition( + this.authorizationRequestRedirectFilter, authorizationRequestRedirectFilterId)); + + this.authorizationCodeGrantFilter = parser.getAuthorizationCodeGrantFilter(); + String authorizationCodeGrantFilterId = pc.getReaderContext() + .generateBeanName(this.authorizationCodeGrantFilter); + this.pc.registerBeanComponent(new BeanComponentDefinition( + this.authorizationCodeGrantFilter, authorizationCodeGrantFilterId)); + + BeanDefinition authorizationCodeAuthenticationProvider = parser.getAuthorizationCodeAuthenticationProvider(); + String authorizationCodeAuthenticationProviderId = pc.getReaderContext() + .generateBeanName(authorizationCodeAuthenticationProvider); + this.pc.registerBeanComponent(new BeanComponentDefinition( + authorizationCodeAuthenticationProvider, authorizationCodeAuthenticationProviderId)); + this.authorizationCodeAuthenticationProviderRef = new RuntimeBeanReference(authorizationCodeAuthenticationProviderId); + } + void createOpenIDLoginFilter(BeanReference sessionStrategy, BeanReference authManager) { Element openIDLoginElt = DomUtils.getChildElementByTagName(httpElt, Elements.OPENID_LOGIN); @@ -884,6 +935,11 @@ final class AuthenticationConfigBuilder { filters.add(new OrderDecorator(basicFilter, BASIC_AUTH_FILTER)); } + if (authorizationCodeGrantFilter != null) { + filters.add(new OrderDecorator(authorizationRequestRedirectFilter, OAUTH2_AUTHORIZATION_REQUEST_FILTER)); + filters.add(new OrderDecorator(authorizationCodeGrantFilter, OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER)); + } + filters.add(new OrderDecorator(etf, EXCEPTION_TRANSLATION_FILTER)); return filters; @@ -920,6 +976,10 @@ final class AuthenticationConfigBuilder { providers.add(oauth2LoginOidcAuthenticationProviderRef); } + if (authorizationCodeAuthenticationProviderRef != null) { + providers.add(authorizationCodeAuthenticationProviderRef); + } + return providers; } diff --git a/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java new file mode 100644 index 0000000000..269143ede0 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParser.java @@ -0,0 +1,172 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanReference; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationProvider; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; + +/** + * @author Joe Grandja + * @since 5.3 + */ +final class OAuth2ClientBeanDefinitionParser implements BeanDefinitionParser { + private static final String ELT_AUTHORIZATION_CODE_GRANT = "authorization-code-grant"; + private static final String ATT_CLIENT_REGISTRATION_REPOSITORY_REF = "client-registration-repository-ref"; + private static final String ATT_AUTHORIZED_CLIENT_REPOSITORY_REF = "authorized-client-repository-ref"; + private static final String ATT_AUTHORIZED_CLIENT_SERVICE_REF = "authorized-client-service-ref"; + private static final String ATT_AUTHORIZATION_REQUEST_REPOSITORY_REF = "authorization-request-repository-ref"; + private static final String ATT_AUTHORIZATION_REQUEST_RESOLVER_REF = "authorization-request-resolver-ref"; + private static final String ATT_ACCESS_TOKEN_RESPONSE_CLIENT_REF = "access-token-response-client-ref"; + private final BeanReference requestCache; + private final BeanReference authenticationManager; + private BeanDefinition authorizationRequestRedirectFilter; + private BeanDefinition authorizationCodeGrantFilter; + private BeanDefinition authorizationCodeAuthenticationProvider; + + OAuth2ClientBeanDefinitionParser(BeanReference requestCache, BeanReference authenticationManager) { + this.requestCache = requestCache; + this.authenticationManager = authenticationManager; + } + + @Override + public BeanDefinition parse(Element element, ParserContext parserContext) { + Element authorizationCodeGrantElt = DomUtils.getChildElementByTagName(element, ELT_AUTHORIZATION_CODE_GRANT); + + BeanMetadataElement clientRegistrationRepository = getClientRegistrationRepository(element); + BeanMetadataElement authorizedClientRepository = getAuthorizedClientRepository( + element, clientRegistrationRepository); + BeanMetadataElement authorizationRequestRepository = getAuthorizationRequestRepository( + authorizationCodeGrantElt); + + BeanDefinitionBuilder authorizationRequestRedirectFilterBuilder = BeanDefinitionBuilder + .rootBeanDefinition(OAuth2AuthorizationRequestRedirectFilter.class); + String authorizationRequestResolverRef = authorizationCodeGrantElt != null ? + authorizationCodeGrantElt.getAttribute(ATT_AUTHORIZATION_REQUEST_RESOLVER_REF) : null; + if (!StringUtils.isEmpty(authorizationRequestResolverRef)) { + authorizationRequestRedirectFilterBuilder.addConstructorArgReference(authorizationRequestResolverRef); + } else { + authorizationRequestRedirectFilterBuilder.addConstructorArgValue(clientRegistrationRepository); + } + this.authorizationRequestRedirectFilter = authorizationRequestRedirectFilterBuilder + .addPropertyValue("authorizationRequestRepository", authorizationRequestRepository) + .addPropertyValue("requestCache", this.requestCache) + .getBeanDefinition(); + + this.authorizationCodeGrantFilter = BeanDefinitionBuilder + .rootBeanDefinition(OAuth2AuthorizationCodeGrantFilter.class) + .addConstructorArgValue(clientRegistrationRepository) + .addConstructorArgValue(authorizedClientRepository) + .addConstructorArgValue(this.authenticationManager) + .addPropertyValue("authorizationRequestRepository", authorizationRequestRepository) + .getBeanDefinition(); + + BeanMetadataElement accessTokenResponseClient = getAccessTokenResponseClient(authorizationCodeGrantElt); + + this.authorizationCodeAuthenticationProvider = BeanDefinitionBuilder + .rootBeanDefinition(OAuth2AuthorizationCodeAuthenticationProvider.class) + .addConstructorArgValue(accessTokenResponseClient) + .getBeanDefinition(); + + return null; + } + + private BeanMetadataElement getClientRegistrationRepository(Element element) { + BeanMetadataElement clientRegistrationRepository; + String clientRegistrationRepositoryRef = element.getAttribute(ATT_CLIENT_REGISTRATION_REPOSITORY_REF); + if (!StringUtils.isEmpty(clientRegistrationRepositoryRef)) { + clientRegistrationRepository = new RuntimeBeanReference(clientRegistrationRepositoryRef); + } else { + clientRegistrationRepository = new RuntimeBeanReference(ClientRegistrationRepository.class); + } + return clientRegistrationRepository; + } + + private BeanMetadataElement getAuthorizedClientRepository(Element element, + BeanMetadataElement clientRegistrationRepository) { + BeanMetadataElement authorizedClientRepository; + String authorizedClientRepositoryRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_REPOSITORY_REF); + if (!StringUtils.isEmpty(authorizedClientRepositoryRef)) { + authorizedClientRepository = new RuntimeBeanReference(authorizedClientRepositoryRef); + } else { + BeanMetadataElement authorizedClientService; + String authorizedClientServiceRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_SERVICE_REF); + if (!StringUtils.isEmpty(authorizedClientServiceRef)) { + authorizedClientService = new RuntimeBeanReference(authorizedClientServiceRef); + } else { + authorizedClientService = BeanDefinitionBuilder + .rootBeanDefinition( + "org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService") + .addConstructorArgValue(clientRegistrationRepository).getBeanDefinition(); + } + authorizedClientRepository = BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository") + .addConstructorArgValue(authorizedClientService).getBeanDefinition(); + } + return authorizedClientRepository; + } + + private BeanMetadataElement getAuthorizationRequestRepository(Element element) { + BeanMetadataElement authorizationRequestRepository; + String authorizationRequestRepositoryRef = element != null ? + element.getAttribute(ATT_AUTHORIZATION_REQUEST_REPOSITORY_REF) : null; + if (!StringUtils.isEmpty(authorizationRequestRepositoryRef)) { + authorizationRequestRepository = new RuntimeBeanReference(authorizationRequestRepositoryRef); + } else { + authorizationRequestRepository = BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository") + .getBeanDefinition(); + } + return authorizationRequestRepository; + } + + private BeanMetadataElement getAccessTokenResponseClient(Element element) { + BeanMetadataElement accessTokenResponseClient; + String accessTokenResponseClientRef = element != null ? + element.getAttribute(ATT_ACCESS_TOKEN_RESPONSE_CLIENT_REF) : null; + if (!StringUtils.isEmpty(accessTokenResponseClientRef)) { + accessTokenResponseClient = new RuntimeBeanReference(accessTokenResponseClientRef); + } else { + accessTokenResponseClient = BeanDefinitionBuilder.rootBeanDefinition( + "org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient") + .getBeanDefinition(); + } + return accessTokenResponseClient; + } + + BeanDefinition getAuthorizationRequestRedirectFilter() { + return this.authorizationRequestRedirectFilter; + } + + BeanDefinition getAuthorizationCodeGrantFilter() { + return this.authorizationCodeGrantFilter; + } + + BeanDefinition getAuthorizationCodeAuthenticationProvider() { + return this.authorizationCodeAuthenticationProvider; + } +} diff --git a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java index 2ca1b76e0f..ee6f366d48 100644 --- a/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java +++ b/config/src/main/java/org/springframework/security/config/http/SecurityFilters.java @@ -51,6 +51,7 @@ enum SecurityFilters { JAAS_API_SUPPORT_FILTER, REMEMBER_ME_FILTER, ANONYMOUS_FILTER, + OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER, SESSION_MANAGEMENT_FILTER, EXCEPTION_TRANSLATION_FILTER, FILTER_SECURITY_INTERCEPTOR, diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.3.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.3.rnc index a3739b3eab..6bd7fee3d2 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.3.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.3.rnc @@ -296,7 +296,7 @@ http-firewall = http = ## Container element for HTTP security configuration. Multiple elements can now be defined, each with a specific pattern to which the enclosed security configuration applies. A pattern can also be configured to bypass Spring Security's filters completely by setting the "security" attribute to "none". - element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & oauth2-login? & openid-login? & x509? & jee? & http-basic? & logout? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf? & cors?) } + element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & oauth2-login? & oauth2-client? & openid-login? & x509? & jee? & http-basic? & logout? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers? & csrf? & cors?) } http.attlist &= ## The request URL pattern which will be mapped to the filter chain created by this element. If omitted, the filter chain will match all requests. attribute pattern {xsd:token}? @@ -483,6 +483,32 @@ oauth2-login.attlist &= ## Reference to the JwtDecoderFactory used by OidcAuthorizationCodeAuthenticationProvider attribute jwt-decoder-factory-ref {xsd:token}? +oauth2-client = + ## Configures OAuth 2.0 Client support. + element oauth2-client {oauth2-client.attlist, (authorization-code-grant?) } +oauth2-client.attlist &= + ## Reference to the ClientRegistrationRepository + attribute client-registration-repository-ref {xsd:token}? +oauth2-client.attlist &= + ## Reference to the OAuth2AuthorizedClientRepository + attribute authorized-client-repository-ref {xsd:token}? +oauth2-client.attlist &= + ## Reference to the OAuth2AuthorizedClientService + attribute authorized-client-service-ref {xsd:token}? + +authorization-code-grant = + ## Configures OAuth 2.0 Authorization Code Grant. + element authorization-code-grant {authorization-code-grant.attlist, empty} +authorization-code-grant.attlist &= + ## Reference to the AuthorizationRequestRepository + attribute authorization-request-repository-ref {xsd:token}? +authorization-code-grant.attlist &= + ## Reference to the OAuth2AuthorizationRequestResolver + attribute authorization-request-resolver-ref {xsd:token}? +authorization-code-grant.attlist &= + ## Reference to the OAuth2AccessTokenResponseClient + attribute access-token-response-client-ref {xsd:token}? + client-registrations = ## Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 Provider. element client-registrations {client-registration+, provider*} @@ -1024,4 +1050,4 @@ position = ## The explicit position at which the custom-filter should be placed in the chain. Use if you are replacing a standard filter. attribute position {named-security-filter} -named-security-filter = "FIRST" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "CSRF_FILTER" | "LOGOUT_FILTER" | "OAUTH2_AUTHORIZATION_REQUEST_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "OAUTH2_LOGIN_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BEARER_TOKEN_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" +named-security-filter = "FIRST" | "CHANNEL_FILTER" | "SECURITY_CONTEXT_FILTER" | "CONCURRENT_SESSION_FILTER" | "WEB_ASYNC_MANAGER_FILTER" | "HEADERS_FILTER" | "CORS_FILTER" | "CSRF_FILTER" | "LOGOUT_FILTER" | "OAUTH2_AUTHORIZATION_REQUEST_FILTER" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_FILTER" | "OAUTH2_LOGIN_FILTER" | "FORM_LOGIN_FILTER" | "OPENID_FILTER" | "LOGIN_PAGE_FILTER" |"LOGOUT_PAGE_FILTER" | "DIGEST_AUTH_FILTER" | "BEARER_TOKEN_AUTH_FILTER" | "BASIC_AUTH_FILTER" | "REQUEST_CACHE_FILTER" | "SERVLET_API_SUPPORT_FILTER" | "JAAS_API_SUPPORT_FILTER" | "REMEMBER_ME_FILTER" | "ANONYMOUS_FILTER" | "OAUTH2_AUTHORIZATION_CODE_GRANT_FILTER" | "SESSION_MANAGEMENT_FILTER" | "EXCEPTION_TRANSLATION_FILTER" | "FILTER_SECURITY_INTERCEPTOR" | "SWITCH_USER_FILTER" | "LAST" diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.3.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.3.xsd index 4d91099c8f..6fee0689b7 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.3.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.3.xsd @@ -956,6 +956,7 @@ + Sets up form login for authentication with an Open ID identity @@ -1546,6 +1547,67 @@ + + + Configures OAuth 2.0 Client support. + + + + + + + + + + + + + Reference to the ClientRegistrationRepository + + + + + + Reference to the OAuth2AuthorizedClientRepository + + + + + + Reference to the OAuth2AuthorizedClientService + + + + + + + Configures OAuth 2.0 Authorization Code Grant. + + + + + + + + + + Reference to the AuthorizationRequestRepository + + + + + + Reference to the OAuth2AuthorizationRequestResolver + + + + + + Reference to the OAuth2AccessTokenResponseClient + + + + Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0 @@ -3006,6 +3068,7 @@ + diff --git a/config/src/test/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests.java new file mode 100644 index 0000000000..6774cfcdf2 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests.java @@ -0,0 +1,219 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.config.http; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.oauth2.client.CommonOAuth2Provider; +import org.springframework.security.config.test.SpringTestRule; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses.accessTokenResponse; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link OAuth2ClientBeanDefinitionParser}. + * + * @author Joe Grandja + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SecurityTestExecutionListeners +public class OAuth2ClientBeanDefinitionParserTests { + private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests"; + + @Rule + public final SpringTestRule spring = new SpringTestRule(); + + @Autowired + private ClientRegistrationRepository clientRegistrationRepository; + + @Autowired(required = false) + private OAuth2AuthorizedClientRepository authorizedClientRepository; + + @Autowired(required = false) + private OAuth2AuthorizedClientService authorizedClientService; + + @Autowired(required = false) + private AuthorizationRequestRepository authorizationRequestRepository; + + @Autowired(required = false) + private OAuth2AuthorizationRequestResolver authorizationRequestResolver; + + @Autowired(required = false) + private OAuth2AccessTokenResponseClient accessTokenResponseClient; + + @Autowired + private MockMvc mvc; + + @Test + public void requestWhenAuthorizeThenRedirect() throws Exception { + this.spring.configLocations(xml("Minimal")).autowire(); + + MvcResult result = this.mvc.perform(get("/oauth2/authorization/google")) + .andExpect(status().is3xxRedirection()) + .andReturn(); + assertThat(result.getResponse().getRedirectedUrl()).matches( + "https://accounts.google.com/o/oauth2/v2/auth\\?" + + "response_type=code&client_id=google-client-id&" + + "scope=scope1%20scope2&state=.{15,}&redirect_uri=http://localhost/callback/google"); + } + + @Test + public void requestWhenCustomClientRegistrationRepositoryThenCalled() throws Exception { + this.spring.configLocations(xml("CustomClientRegistrationRepository")).autowire(); + + ClientRegistration clientRegistration = CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .redirectUriTemplate("http://localhost/callback/google") + .scope("scope1", "scope2") + .build(); + when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn(clientRegistration); + + MvcResult result = this.mvc.perform(get("/oauth2/authorization/google")) + .andExpect(status().is3xxRedirection()) + .andReturn(); + assertThat(result.getResponse().getRedirectedUrl()).matches( + "https://accounts.google.com/o/oauth2/v2/auth\\?" + + "response_type=code&client_id=google-client-id&" + + "scope=scope1%20scope2&state=.{15,}&redirect_uri=http://localhost/callback/google"); + + verify(this.clientRegistrationRepository).findByRegistrationId(any()); + } + + @Test + public void requestWhenCustomAuthorizationRequestResolverThenCalled() throws Exception { + this.spring.configLocations(xml("CustomConfiguration")).autowire(); + + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google"); + + OAuth2AuthorizationRequest authorizationRequest = createAuthorizationRequest(clientRegistration); + when(this.authorizationRequestResolver.resolve(any())).thenReturn(authorizationRequest); + + this.mvc.perform(get("/oauth2/authorization/google")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl( + "https://accounts.google.com/o/oauth2/v2/auth?" + + "response_type=code&client_id=google-client-id&" + + "scope=scope1%20scope2&state=state&redirect_uri=http://localhost/callback/google")); + + verify(this.authorizationRequestResolver).resolve(any()); + } + + @Test + public void requestWhenAuthorizationResponseMatchThenProcess() throws Exception { + this.spring.configLocations(xml("CustomConfiguration")).autowire(); + + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google"); + + OAuth2AuthorizationRequest authorizationRequest = createAuthorizationRequest(clientRegistration); + when(this.authorizationRequestRepository.loadAuthorizationRequest(any())) + .thenReturn(authorizationRequest); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())) + .thenReturn(authorizationRequest); + + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", authorizationRequest.getState()); + this.mvc.perform(get(authorizationRequest.getRedirectUri()).params(params)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl(authorizationRequest.getRedirectUri())); + + ArgumentCaptor authorizedClientCaptor = + ArgumentCaptor.forClass(OAuth2AuthorizedClient.class); + verify(this.authorizedClientRepository).saveAuthorizedClient( + authorizedClientCaptor.capture(), any(), any(), any()); + OAuth2AuthorizedClient authorizedClient = authorizedClientCaptor.getValue(); + assertThat(authorizedClient.getClientRegistration()).isEqualTo(clientRegistration); + assertThat(authorizedClient.getAccessToken()).isEqualTo(accessTokenResponse.getAccessToken()); + } + + @WithMockUser + @Test + public void requestWhenCustomAuthorizedClientServiceThenCalled() throws Exception { + this.spring.configLocations(xml("CustomAuthorizedClientService")).autowire(); + + ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId("google"); + + OAuth2AuthorizationRequest authorizationRequest = createAuthorizationRequest(clientRegistration); + when(this.authorizationRequestRepository.loadAuthorizationRequest(any())) + .thenReturn(authorizationRequest); + when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())) + .thenReturn(authorizationRequest); + + OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build(); + when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("code", "code123"); + params.add("state", authorizationRequest.getState()); + this.mvc.perform(get(authorizationRequest.getRedirectUri()).params(params)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl(authorizationRequest.getRedirectUri())); + + verify(this.authorizedClientService).saveAuthorizedClient(any(), any()); + } + + private static OAuth2AuthorizationRequest createAuthorizationRequest(ClientRegistration clientRegistration) { + Map attributes = new HashMap<>(); + attributes.put(OAuth2ParameterNames.REGISTRATION_ID, clientRegistration.getRegistrationId()); + return OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()) + .clientId(clientRegistration.getClientId()) + .redirectUri(clientRegistration.getRedirectUriTemplate()) + .scopes(clientRegistration.getScopes()) + .state("state") + .attributes(attributes) + .build(); + } + + private static String xml(String configName) { + return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml"; + } +} diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomAuthorizedClientService.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomAuthorizedClientService.xml new file mode 100644 index 0000000000..ceb72ca05c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomAuthorizedClientService.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomClientRegistrationRepository.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomClientRegistrationRepository.xml new file mode 100644 index 0000000000..55bb7e3b75 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomClientRegistrationRepository.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomConfiguration.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomConfiguration.xml new file mode 100644 index 0000000000..447dbf914d --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-CustomConfiguration.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-Minimal.xml b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-Minimal.xml new file mode 100644 index 0000000000..8938d4b50c --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/http/OAuth2ClientBeanDefinitionParserTests-Minimal.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc index ef36f9eaaf..aed960fee3 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc @@ -164,6 +164,7 @@ The default value is true. * <> * <> * <> +* <> * <> * <> * <> @@ -965,6 +966,71 @@ Reference to the `AuthenticationFailureHandler`. Reference to the `JwtDecoderFactory` used by `OidcAuthorizationCodeAuthenticationProvider`. +[[nsa-oauth2-client]] +==== +Configures <> support. + + +[[nsa-oauth2-client-parents]] +===== Parent Elements of + +* <> + +[[nsa-oauth2-client-attributes]] +===== Attributes + + +[[nsa-oauth2-client-client-registration-repository-ref]] +* **client-registration-repository-ref** +Reference to the `ClientRegistrationRepository`. + + +[[nsa-oauth2-client-authorized-client-repository-ref]] +* **authorized-client-repository-ref** +Reference to the `OAuth2AuthorizedClientRepository`. + + +[[nsa-oauth2-client-authorized-client-service-ref]] +* **authorized-client-service-ref** +Reference to the `OAuth2AuthorizedClientService`. + + +[[nsa-oauth2-client-children]] +===== Child Elements of + +* <> + + +[[nsa-authorization-code-grant]] +==== +Configures <>. + + +[[nsa-authorization-code-grant-parents]] +===== Parent Elements of + +* <> + + +[[nsa-authorization-code-grant-attributes]] +===== Attributes + + +[[nsa-authorization-code-grant-authorization-request-repository-ref]] +* **authorization-request-repository-ref** +Reference to the `AuthorizationRequestRepository`. + + +[[nsa-authorization-code-grant-authorization-request-resolver-ref]] +* **authorization-request-resolver-ref** +Reference to the `OAuth2AuthorizationRequestResolver`. + + +[[nsa-authorization-code-grant-access-token-response-client-ref]] +* **access-token-response-client-ref** +Reference to the `OAuth2AccessTokenResponseClient`. + + [[nsa-client-registrations]] ==== A container element for client(s) registered (<>) with an OAuth 2.0 or OpenID Connect 1.0 Provider.