Quellcode durchsuchen

Add XML namespace support for oauth2-login

Fixes gh-4557
Ruby Hartono vor 5 Jahren
Ursprung
Commit
71a5c9521c
35 geänderte Dateien mit 2729 neuen und 63 gelöschten Zeilen
  1. 1 1
      config/src/main/java/org/springframework/security/config/Elements.java
  2. 1 1
      config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java
  3. 81 8
      config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java
  4. 4 1
      config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java
  5. 456 4
      config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java
  6. 3 1
      config/src/main/java/org/springframework/security/config/http/SecurityFilters.java
  7. 204 3
      config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java
  8. 46 4
      config/src/main/resources/org/springframework/security/config/spring-security-5.3.rnc
  9. 93 3
      config/src/main/resources/org/springframework/security/config/spring-security-5.3.xsd
  10. 1 1
      config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java
  11. 591 8
      config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java
  12. 242 0
      config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java
  13. 1 1
      config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java
  14. 34 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration.xml
  15. 38 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationFailureHandler.xml
  16. 53 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationHandler.xml
  17. 38 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRequestResolver.xml
  18. 58 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomGrantedAuthorities.xml
  19. 34 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomLoginPage.xml
  20. 51 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomLoginProcessingUrl.xml
  21. 35 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithFormLogin.xml
  22. 54 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactory.xml
  23. 51 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactoryAndDefaultSuccessHandler.xml
  24. 57 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithTestConfiguration.xml
  25. 52 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithinSameFile.xml
  26. 3 27
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration.xml
  27. 49 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizationRequestRepository.xml
  28. 53 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientRepository.xml
  29. 53 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientService.xml
  30. 50 0
      config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomClientRegistrationRepository.xml
  31. 39 0
      config/src/test/resources/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests-FromIssuerUri.xml
  32. 27 0
      config/src/test/resources/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests-MultiClientRegistration.xml
  33. 59 0
      config/src/test/resources/org/springframework/security/config/oauth2/client/google-github-registration.xml
  34. 44 0
      config/src/test/resources/org/springframework/security/config/oauth2/client/google-registration.xml
  35. 73 0
      docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc

+ 1 - 1
config/src/main/java/org/springframework/security/config/Elements.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * 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.

+ 1 - 1
config/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2009-2019 the original author or authors.
+ * Copyright 2009-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.

+ 81 - 8
config/src/main/java/org/springframework/security/config/http/AuthenticationConfigBuilder.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * 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.
@@ -138,6 +138,15 @@ final class AuthenticationConfigBuilder {
 
 	private String openIDLoginPage;
 
+	private String oauth2LoginFilterId;
+	private String oauth2AuthorizationRequestRedirectFilterId;
+	private BeanDefinition oauth2AuthorizationRequestRedirectFilter;
+	private BeanDefinition oauth2LoginEntryPoint;
+	private BeanReference oauth2LoginAuthenticationProviderRef;
+	private BeanReference oauth2LoginOidcAuthenticationProviderRef;
+
+	private BeanDefinition oauth2LoginLinks;
+
 	AuthenticationConfigBuilder(Element element, boolean forceAutoConfig,
 			ParserContext pc, SessionCreationPolicy sessionPolicy,
 			BeanReference requestCache, BeanReference authenticationManager,
@@ -238,10 +247,42 @@ final class AuthenticationConfigBuilder {
 
 	void createOAuth2LoginFilter(BeanReference sessionStrategy, BeanReference authManager) {
 		Element oauth2LoginElt = DomUtils.getChildElementByTagName(this.httpElt, Elements.OAUTH2_LOGIN);
-		if (oauth2LoginElt != null || this.autoConfig) {
-			OAuth2LoginBeanDefinitionParser parser = new OAuth2LoginBeanDefinitionParser();
-			parser.parse(oauth2LoginElt, this.pc);
-			// TODO Implement
+		if (oauth2LoginElt != null) {
+			OAuth2LoginBeanDefinitionParser parser = new OAuth2LoginBeanDefinitionParser(requestCache, portMapper,
+					portResolver, sessionStrategy, allowSessionCreation);
+			BeanDefinition oauth2LoginFilterBean = parser.parse(oauth2LoginElt, this.pc);
+			oauth2LoginFilterBean.getPropertyValues().addPropertyValue("authenticationManager", authManager);
+
+			// retrieve the other bean result
+			BeanDefinition oauth2LoginAuthProvider = parser.getOAuth2LoginAuthenticationProvider();
+			oauth2AuthorizationRequestRedirectFilter = parser.getOAuth2AuthorizationRequestRedirectFilter();
+			oauth2LoginEntryPoint = parser.getOAuth2LoginAuthenticationEntryPoint();
+
+			// generate bean name to be registered
+			String oauth2LoginAuthenticationProviderId = pc.getReaderContext()
+					.generateBeanName(oauth2LoginAuthProvider);
+			oauth2LoginFilterId = pc.getReaderContext().generateBeanName(oauth2LoginFilterBean);
+			oauth2AuthorizationRequestRedirectFilterId = pc.getReaderContext()
+					.generateBeanName(oauth2AuthorizationRequestRedirectFilter);
+			oauth2LoginLinks = parser.getOAuth2LoginLinks();
+
+			// register the component
+			pc.registerBeanComponent(new BeanComponentDefinition(oauth2AuthorizationRequestRedirectFilter,
+					oauth2AuthorizationRequestRedirectFilterId));
+			pc.registerBeanComponent(new BeanComponentDefinition(oauth2LoginFilterBean, oauth2LoginFilterId));
+			pc.registerBeanComponent(
+					new BeanComponentDefinition(oauth2LoginAuthProvider, oauth2LoginAuthenticationProviderId));
+
+			oauth2LoginAuthenticationProviderRef = new RuntimeBeanReference(oauth2LoginAuthenticationProviderId);
+
+			// oidc provider
+			BeanDefinition oauth2LoginOidcAuthProvider = parser.getOAuth2LoginOidcAuthenticationProvider();
+			String oauth2LoginOidcAuthenticationProviderId = pc.getReaderContext()
+					.generateBeanName(oauth2LoginOidcAuthProvider);
+			pc.registerBeanComponent(
+					new BeanComponentDefinition(oauth2LoginOidcAuthProvider, oauth2LoginOidcAuthenticationProviderId));
+			oauth2LoginOidcAuthenticationProviderRef = new RuntimeBeanReference(
+					oauth2LoginOidcAuthenticationProviderId);
 		}
 	}
 
@@ -545,7 +586,7 @@ final class AuthenticationConfigBuilder {
 	}
 
 	void createLoginPageFilterIfNeeded() {
-		boolean needLoginPage = formFilterId != null || openIDFilterId != null;
+		boolean needLoginPage = formFilterId != null || openIDFilterId != null || oauth2LoginFilterId != null;
 
 		// If no login page has been defined, add in the default page generator.
 		if (needLoginPage && formLoginPage == null && openIDLoginPage == null) {
@@ -571,6 +612,12 @@ final class AuthenticationConfigBuilder {
 						openidLoginProcessingUrl);
 			}
 
+			if (oauth2LoginFilterId != null) {
+				loginPageFilter.addConstructorArgReference(oauth2LoginFilterId);
+				loginPageFilter.addPropertyValue("Oauth2LoginEnabled", true);
+				loginPageFilter.addPropertyValue("Oauth2AuthenticationUrlToClientName", oauth2LoginLinks);
+			}
+
 			loginPageGenerationFilter = loginPageFilter.getBeanDefinition();
 			this.logoutPageGenerationFilter = logoutPageFilter.getBeanDefinition();
 		}
@@ -732,7 +779,7 @@ final class AuthenticationConfigBuilder {
 		Element openIDLoginElt = DomUtils.getChildElementByTagName(httpElt,
 				Elements.OPENID_LOGIN);
 		// Basic takes precedence if explicit element is used and no others are configured
-		if (basicAuthElt != null && formLoginElt == null && openIDLoginElt == null) {
+		if (basicAuthElt != null && formLoginElt == null && openIDLoginElt == null && oauth2LoginEntryPoint == null) {
 			return basicEntryPoint;
 		}
 
@@ -747,7 +794,15 @@ final class AuthenticationConfigBuilder {
 		}
 
 		if (formFilterId != null && openIDLoginPage == null) {
-			return formEntryPoint;
+			// gh-6802
+			// If form login was enabled through element and Oauth2 login was enabled from element then use form login
+			if (formLoginElt != null && oauth2LoginEntryPoint != null) {
+				return formEntryPoint;
+			}
+			// If form login was enabled through auto-config, and Oauth2 login was not enabled then use form login
+			if (oauth2LoginEntryPoint == null) {
+				return formEntryPoint;
+			}
 		}
 
 		// Otherwise use OpenID if enabled
@@ -760,6 +815,11 @@ final class AuthenticationConfigBuilder {
 			return preAuthEntryPoint;
 		}
 
+		// OAuth2 entry point will not be null if only 1 client registration
+		if (oauth2LoginEntryPoint != null) {
+			return oauth2LoginEntryPoint;
+		}
+
 		pc.getReaderContext()
 				.error("No AuthenticationEntryPoint could be established. Please "
 						+ "make sure you have a login mechanism configured through the namespace (such as form-login) or "
@@ -808,6 +868,11 @@ final class AuthenticationConfigBuilder {
 					FORM_LOGIN_FILTER));
 		}
 
+		if (oauth2LoginFilterId != null) {
+			filters.add(new OrderDecorator(new RuntimeBeanReference(oauth2LoginFilterId), OAUTH2_LOGIN_FILTER));
+			filters.add(new OrderDecorator(oauth2AuthorizationRequestRedirectFilter, OAUTH2_REDIRECT_FILTER));
+		}
+
 		if (openIDFilterId != null) {
 			filters.add(new OrderDecorator(new RuntimeBeanReference(openIDFilterId),
 					OPENID_FILTER));
@@ -850,6 +915,14 @@ final class AuthenticationConfigBuilder {
 			providers.add(jeeProviderRef);
 		}
 
+		if (oauth2LoginAuthenticationProviderRef != null) {
+			providers.add(oauth2LoginAuthenticationProviderRef);
+		}
+
+		if (oauth2LoginOidcAuthenticationProviderRef != null) {
+			providers.add(oauth2LoginOidcAuthenticationProviderRef);
+		}
+
 		return providers;
 	}
 

+ 4 - 1
config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2012 the original author or authors.
+ * 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.
@@ -33,6 +33,7 @@ import org.springframework.beans.factory.xml.ParserContext;
 import org.springframework.core.OrderComparator;
 import org.springframework.core.Ordered;
 import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
 import org.springframework.security.authentication.ProviderManager;
 import org.springframework.security.config.BeanIds;
 import org.springframework.security.config.Elements;
@@ -294,6 +295,8 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
 					clearCredentials);
 		}
 
+		// gh-6009
+		authManager.addPropertyValue("authenticationEventPublisher", new RootBeanDefinition(DefaultAuthenticationEventPublisher.class));
 		authManager.getRawBeanDefinition().setSource(pc.extractSource(element));
 		BeanDefinition authMgrBean = authManager.getBeanDefinition();
 		String id = pc.getReaderContext().generateBeanName(authMgrBean);

+ 456 - 4
config/src/main/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParser.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * 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.
@@ -15,19 +15,471 @@
  */
 package org.springframework.security.config.http;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.BeanMetadataElement;
+import org.springframework.beans.BeansException;
 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.parsing.BeanComponentDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
 import org.springframework.beans.factory.xml.BeanDefinitionParser;
 import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.core.ResolvableType;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.config.Elements;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider;
+import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
+import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.oidc.OidcScopes;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
+import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
+import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
+import org.springframework.security.web.util.matcher.AndRequestMatcher;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
+import org.springframework.security.web.util.matcher.NegatedRequestMatcher;
+import org.springframework.security.web.util.matcher.OrRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
+import org.springframework.security.web.util.matcher.RequestMatcher;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.util.xml.DomUtils;
+import org.springframework.web.accept.ContentNegotiationStrategy;
+import org.springframework.web.accept.HeaderContentNegotiationStrategy;
 import org.w3c.dom.Element;
 
 /**
- * @author
+ * @author Ruby Hartono
+ * @since 5.3
  */
 final class OAuth2LoginBeanDefinitionParser implements BeanDefinitionParser {
 
+	private static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization";
+	private static final String DEFAULT_LOGIN_URI = DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL;
+
+	private static final String ELT_CLIENT_REGISTRATION = "client-registration";
+	private static final String ATT_REGISTRATION_ID = "registration-id";
+	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 static final String ATT_USER_AUTHORITIES_MAPPER_REF = "user-authorities-mapper-ref";
+	private static final String ATT_USER_SERVICE_REF = "user-service-ref";
+	private static final String ATT_OIDC_USER_SERVICE_REF = "oidc-user-service-ref";
+	private static final String ATT_LOGIN_PROCESSING_URL = "login-processing-url";
+	private static final String ATT_LOGIN_PAGE = "login-page";
+	private static final String ATT_AUTHENTICATION_SUCCESS_HANDLER_REF = "authentication-success-handler-ref";
+	private static final String ATT_AUTHENTICATION_FAILURE_HANDLER_REF = "authentication-failure-handler-ref";
+	private static final String ATT_JWT_DECODER_FACTORY_REF = "jwt-decoder-factory-ref";
+
+	private BeanReference requestCache;
+	private final BeanReference portMapper;
+	private final BeanReference portResolver;
+	private final BeanReference sessionStrategy;
+	private final boolean allowSessionCreation;
+
+	private BeanDefinition oauth2AuthorizationRequestRedirectFilter;
+
+	private BeanDefinition oauth2LoginAuthenticationEntryPoint;
+
+	private BeanDefinition oauth2LoginAuthenticationProvider;
+
+	private BeanDefinition oauth2LoginOidcAuthenticationProvider;
+
+	private BeanDefinition oauth2LoginLinks;
+
+	OAuth2LoginBeanDefinitionParser(BeanReference requestCache, BeanReference portMapper, BeanReference portResolver,
+			BeanReference sessionStrategy, boolean allowSessionCreation) {
+		this.requestCache = requestCache;
+		this.portMapper = portMapper;
+		this.portResolver = portResolver;
+		this.sessionStrategy = sessionStrategy;
+		this.allowSessionCreation = allowSessionCreation;
+	}
+
 	@Override
 	public BeanDefinition parse(Element element, ParserContext parserContext) {
-		// TODO Implement
-		return null;
+		// register magic bean
+		BeanDefinition oauth2LoginBeanConfig = BeanDefinitionBuilder.rootBeanDefinition(OAuth2LoginBeanConfig.class)
+				.getBeanDefinition();
+		String oauth2LoginBeanConfigId = parserContext.getReaderContext().generateBeanName(oauth2LoginBeanConfig);
+		parserContext
+				.registerBeanComponent(new BeanComponentDefinition(oauth2LoginBeanConfig, oauth2LoginBeanConfigId));
+
+		// configure filter
+		BeanMetadataElement clientRegistrationRepository = getClientRegistrationRepository(element);
+		BeanMetadataElement authorizedClientRepository = getAuthorizedClientRepository(element,
+				clientRegistrationRepository);
+		BeanMetadataElement accessTokenResponseClient = getAccessTokenResponseClient(element);
+		BeanMetadataElement oauth2UserService = getOAuth2UserService(element);
+		BeanMetadataElement oauth2AuthRequestRepository = getOAuth2AuthorizationRequestRepository(element);
+
+		BeanDefinitionBuilder oauth2LoginAuthenticationFilterBuilder = BeanDefinitionBuilder
+				.rootBeanDefinition(OAuth2LoginAuthenticationFilter.class)
+				.addConstructorArgValue(clientRegistrationRepository).addConstructorArgValue(authorizedClientRepository)
+				.addPropertyValue("authorizationRequestRepository", oauth2AuthRequestRepository);
+
+		if (sessionStrategy != null) {
+			oauth2LoginAuthenticationFilterBuilder.addPropertyValue("sessionAuthenticationStrategy", sessionStrategy);
+		}
+
+		Object source = parserContext.extractSource(element);
+		String loginProcessingUrl = element.getAttribute(ATT_LOGIN_PROCESSING_URL);
+		WebConfigUtils.validateHttpRedirect(loginProcessingUrl, parserContext, source);
+		if (!StringUtils.isEmpty(loginProcessingUrl)) {
+			oauth2LoginAuthenticationFilterBuilder.addConstructorArgValue(loginProcessingUrl);
+		} else {
+			oauth2LoginAuthenticationFilterBuilder
+					.addConstructorArgValue(OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI);
+		}
+
+		BeanDefinitionBuilder oauth2LoginAuthenticationProviderBuilder = BeanDefinitionBuilder
+				.rootBeanDefinition(OAuth2LoginAuthenticationProvider.class)
+				.addConstructorArgValue(accessTokenResponseClient).addConstructorArgValue(oauth2UserService);
+
+		String oauth2UserAuthMapperRef = element.getAttribute(ATT_USER_AUTHORITIES_MAPPER_REF);
+		if (!StringUtils.isEmpty(oauth2UserAuthMapperRef)) {
+			oauth2LoginAuthenticationProviderBuilder.addPropertyReference("authoritiesMapper", oauth2UserAuthMapperRef);
+		}
+
+		oauth2LoginAuthenticationProvider = oauth2LoginAuthenticationProviderBuilder.getBeanDefinition();
+
+		oauth2LoginOidcAuthenticationProvider = getOAuth2OidcAuthProvider(element, accessTokenResponseClient,
+				oauth2UserAuthMapperRef, parserContext);
+
+		BeanDefinitionBuilder oauth2AuthorizationRequestRedirectFilterBuilder = BeanDefinitionBuilder
+				.rootBeanDefinition(OAuth2AuthorizationRequestRedirectFilter.class);
+
+		String oauth2AuthorizationRequestResolverRef = element.getAttribute(ATT_AUTHORIZATION_REQUEST_RESOLVER_REF);
+		if (!StringUtils.isEmpty(oauth2AuthorizationRequestResolverRef)) {
+			oauth2AuthorizationRequestRedirectFilterBuilder
+					.addConstructorArgReference(oauth2AuthorizationRequestResolverRef);
+		} else {
+			oauth2AuthorizationRequestRedirectFilterBuilder.addConstructorArgValue(clientRegistrationRepository);
+		}
+
+		oauth2AuthorizationRequestRedirectFilterBuilder
+				.addPropertyValue("authorizationRequestRepository", oauth2AuthRequestRepository)
+				.addPropertyValue("requestCache", requestCache);
+		oauth2AuthorizationRequestRedirectFilter = oauth2AuthorizationRequestRedirectFilterBuilder.getBeanDefinition();
+
+		String authenticationSuccessHandlerRef = element.getAttribute(ATT_AUTHENTICATION_SUCCESS_HANDLER_REF);
+		if (!StringUtils.isEmpty(authenticationSuccessHandlerRef)) {
+			oauth2LoginAuthenticationFilterBuilder.addPropertyReference("authenticationSuccessHandler",
+					authenticationSuccessHandlerRef);
+		}
+
+		String loginPage = element.getAttribute(ATT_LOGIN_PAGE);
+		WebConfigUtils.validateHttpRedirect(loginPage, parserContext, source);
+		if (!StringUtils.isEmpty(loginPage)) {
+			oauth2LoginAuthenticationEntryPoint = BeanDefinitionBuilder
+					.rootBeanDefinition(LoginUrlAuthenticationEntryPoint.class).addConstructorArgValue(loginPage)
+					.addPropertyValue("portMapper", portMapper).addPropertyValue("portResolver", portResolver)
+					.getBeanDefinition();
+		} else {
+			Map<RequestMatcher, AuthenticationEntryPoint> entryPoint = getLoginEntryPoint(element, parserContext);
+
+			if (entryPoint != null) {
+				oauth2LoginAuthenticationEntryPoint = BeanDefinitionBuilder
+						.rootBeanDefinition(DelegatingAuthenticationEntryPoint.class).addConstructorArgValue(entryPoint)
+						.addPropertyValue("defaultEntryPoint", new LoginUrlAuthenticationEntryPoint(DEFAULT_LOGIN_URI))
+						.getBeanDefinition();
+			}
+		}
+
+		String authenticationFailureHandlerRef = element.getAttribute(ATT_AUTHENTICATION_FAILURE_HANDLER_REF);
+		if (!StringUtils.isEmpty(authenticationFailureHandlerRef)) {
+			oauth2LoginAuthenticationFilterBuilder.addPropertyReference("authenticationFailureHandler",
+					authenticationFailureHandlerRef);
+		} else {
+			BeanDefinitionBuilder failureHandlerBuilder = BeanDefinitionBuilder.rootBeanDefinition(
+					"org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler");
+			failureHandlerBuilder.addConstructorArgValue(
+					DEFAULT_LOGIN_URI + "?" + DefaultLoginPageGeneratingFilter.ERROR_PARAMETER_NAME);
+			failureHandlerBuilder.addPropertyValue("allowSessionCreation", allowSessionCreation);
+
+			oauth2LoginAuthenticationFilterBuilder.addPropertyValue("authenticationFailureHandler",
+					failureHandlerBuilder.getBeanDefinition());
+		}
+
+		// prepare loginlinks
+		oauth2LoginLinks = BeanDefinitionBuilder.rootBeanDefinition(Map.class)
+				.setFactoryMethodOnBean("getLoginLinks", oauth2LoginBeanConfigId).getBeanDefinition();
+
+		return oauth2LoginAuthenticationFilterBuilder.getBeanDefinition();
+	}
+
+	private BeanMetadataElement getOAuth2AuthorizationRequestRepository(Element element) {
+		BeanMetadataElement oauth2AuthRequestRepository = null;
+		String oauth2AuthRequestRepositoryRef = element.getAttribute(ATT_AUTHORIZATION_REQUEST_REPOSITORY_REF);
+		if (!StringUtils.isEmpty(oauth2AuthRequestRepositoryRef)) {
+			oauth2AuthRequestRepository = new RuntimeBeanReference(oauth2AuthRequestRepositoryRef);
+		} else {
+			oauth2AuthRequestRepository = BeanDefinitionBuilder.rootBeanDefinition(
+					"org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository")
+					.getBeanDefinition();
+		}
+		return oauth2AuthRequestRepository;
+	}
+
+	private BeanMetadataElement getAuthorizedClientRepository(Element element,
+			BeanMetadataElement clientRegistrationRepository) {
+		BeanMetadataElement authorizedClientRepository = null;
+
+		String authorizedClientRepositoryRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_REPOSITORY_REF);
+		if (!StringUtils.isEmpty(authorizedClientRepositoryRef)) {
+			authorizedClientRepository = new RuntimeBeanReference(authorizedClientRepositoryRef);
+		} else {
+			BeanMetadataElement oauth2AuthorizedClientService = null;
+			String authorizedClientServiceRef = element.getAttribute(ATT_AUTHORIZED_CLIENT_SERVICE_REF);
+			if (!StringUtils.isEmpty(authorizedClientServiceRef)) {
+				oauth2AuthorizedClientService = new RuntimeBeanReference(authorizedClientServiceRef);
+			} else {
+				oauth2AuthorizedClientService = BeanDefinitionBuilder
+						.rootBeanDefinition(
+								"org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService")
+						.addConstructorArgValue(clientRegistrationRepository).getBeanDefinition();
+			}
+
+			authorizedClientRepository = BeanDefinitionBuilder.rootBeanDefinition(
+					"org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository")
+					.addConstructorArgValue(oauth2AuthorizedClientService).getBeanDefinition();
+		}
+
+		return authorizedClientRepository;
+	}
+
+	private BeanMetadataElement getClientRegistrationRepository(Element element) {
+		BeanMetadataElement clientRegistrationRepository = null;
+
+		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 BeanDefinition getOAuth2OidcAuthProvider(Element element, BeanMetadataElement accessTokenResponseClient,
+			String oauth2UserAuthMapperRef, ParserContext parserContext) {
+		BeanDefinition oauth2OidcAuthProvider = null;
+		boolean oidcAuthenticationProviderEnabled = ClassUtils
+				.isPresent("org.springframework.security.oauth2.jwt.JwtDecoder", this.getClass().getClassLoader());
+
+		if (oidcAuthenticationProviderEnabled) {
+			BeanMetadataElement oidcUserService = getOAuth2OidcUserService(element);
+
+			BeanDefinitionBuilder oauth2OidcAuthProviderBuilder = BeanDefinitionBuilder.rootBeanDefinition(
+					"org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider")
+					.addConstructorArgValue(accessTokenResponseClient).addConstructorArgValue(oidcUserService);
+
+			if (!StringUtils.isEmpty(oauth2UserAuthMapperRef)) {
+				oauth2OidcAuthProviderBuilder.addPropertyReference("authoritiesMapper", oauth2UserAuthMapperRef);
+			}
+
+			String jwtDecoderFactoryRef = element.getAttribute(ATT_JWT_DECODER_FACTORY_REF);
+			if (!StringUtils.isEmpty(jwtDecoderFactoryRef)) {
+				oauth2OidcAuthProviderBuilder.addPropertyReference("jwtDecoderFactory", jwtDecoderFactoryRef);
+			}
+
+			oauth2OidcAuthProvider = oauth2OidcAuthProviderBuilder.getBeanDefinition();
+		} else {
+			oauth2OidcAuthProvider = BeanDefinitionBuilder.rootBeanDefinition(OidcAuthenticationRequestChecker.class)
+					.getBeanDefinition();
+		}
+
+		return oauth2OidcAuthProvider;
+	}
+
+	private BeanMetadataElement getOAuth2OidcUserService(Element element) {
+		BeanMetadataElement oauth2OidcUserService = null;
+		String oauth2UserServiceRef = element.getAttribute(ATT_OIDC_USER_SERVICE_REF);
+		if (!StringUtils.isEmpty(oauth2UserServiceRef)) {
+			oauth2OidcUserService = new RuntimeBeanReference(oauth2UserServiceRef);
+		} else {
+			oauth2OidcUserService = BeanDefinitionBuilder
+					.rootBeanDefinition("org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService")
+					.getBeanDefinition();
+		}
+		return oauth2OidcUserService;
+	}
+
+	private BeanMetadataElement getOAuth2UserService(Element element) {
+		BeanMetadataElement oauth2UserService = null;
+		String oauth2UserServiceRef = element.getAttribute(ATT_USER_SERVICE_REF);
+		if (!StringUtils.isEmpty(oauth2UserServiceRef)) {
+			oauth2UserService = new RuntimeBeanReference(oauth2UserServiceRef);
+		} else {
+			oauth2UserService = BeanDefinitionBuilder
+					.rootBeanDefinition("org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService")
+					.getBeanDefinition();
+		}
+		return oauth2UserService;
+	}
+
+	private BeanMetadataElement getAccessTokenResponseClient(Element element) {
+		BeanMetadataElement accessTokenResponseClient = null;
+
+		String accessTokenResponseClientRef = element.getAttribute(ATT_ACCESS_TOKEN_RESPONSE_CLIENT_REF);
+		if (!StringUtils.isEmpty(accessTokenResponseClientRef)) {
+			accessTokenResponseClient = new RuntimeBeanReference(accessTokenResponseClientRef);
+		} else {
+			accessTokenResponseClient = BeanDefinitionBuilder.rootBeanDefinition(
+					"org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient")
+					.getBeanDefinition();
+		}
+		return accessTokenResponseClient;
+	}
+
+	BeanDefinition getOAuth2AuthorizationRequestRedirectFilter() {
+		return oauth2AuthorizationRequestRedirectFilter;
+	}
+
+	BeanDefinition getOAuth2LoginAuthenticationEntryPoint() {
+		return oauth2LoginAuthenticationEntryPoint;
+	}
+
+	BeanDefinition getOAuth2LoginAuthenticationProvider() {
+		return oauth2LoginAuthenticationProvider;
+	}
+
+	BeanDefinition getOAuth2LoginOidcAuthenticationProvider() {
+		return oauth2LoginOidcAuthenticationProvider;
+	}
+
+	BeanDefinition getOAuth2LoginLinks() {
+		return oauth2LoginLinks;
+	}
+
+	private Map<RequestMatcher, AuthenticationEntryPoint> getLoginEntryPoint(Element element,
+			ParserContext parserContext) {
+		Map<RequestMatcher, AuthenticationEntryPoint> entryPoints = null;
+		Element clientRegsElt = DomUtils.getChildElementByTagName(element.getOwnerDocument().getDocumentElement(),
+				Elements.CLIENT_REGISTRATIONS);
+
+		if (clientRegsElt != null) {
+			List<Element> clientRegList = DomUtils.getChildElementsByTagName(clientRegsElt, ELT_CLIENT_REGISTRATION);
+
+			if (clientRegList.size() == 1) {
+				RequestMatcher loginPageMatcher = new AntPathRequestMatcher(DEFAULT_LOGIN_URI);
+				RequestMatcher faviconMatcher = new AntPathRequestMatcher("/favicon.ico");
+				RequestMatcher defaultEntryPointMatcher = this.getAuthenticationEntryPointMatcher();
+				RequestMatcher defaultLoginPageMatcher = new AndRequestMatcher(
+						new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher);
+
+				RequestMatcher notXRequestedWith = new NegatedRequestMatcher(
+						new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
+
+				Element clientRegElt = clientRegList.get(0);
+				entryPoints = new LinkedHashMap<>();
+				entryPoints.put(
+						new AndRequestMatcher(notXRequestedWith, new NegatedRequestMatcher(defaultLoginPageMatcher)),
+						new LoginUrlAuthenticationEntryPoint(DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/"
+								+ clientRegElt.getAttribute(ATT_REGISTRATION_ID)));
+
+			}
+		}
+
+		return entryPoints;
+	}
+
+	private RequestMatcher getAuthenticationEntryPointMatcher() {
+		ContentNegotiationStrategy contentNegotiationStrategy = new HeaderContentNegotiationStrategy();
+
+		MediaTypeRequestMatcher mediaMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy,
+				MediaType.APPLICATION_XHTML_XML, new MediaType("image", "*"), MediaType.TEXT_HTML,
+				MediaType.TEXT_PLAIN);
+		mediaMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
+
+		RequestMatcher notXRequestedWith = new NegatedRequestMatcher(
+				new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
+
+		return new AndRequestMatcher(Arrays.asList(notXRequestedWith, mediaMatcher));
+	}
+
+	private static class OidcAuthenticationRequestChecker implements AuthenticationProvider {
+
+		@Override
+		public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+			OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication;
+
+			// Section 3.1.2.1 Authentication Request -
+			// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
+			// scope
+			// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
+			if (authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest().getScopes()
+					.contains(OidcScopes.OPENID)) {
+
+				OAuth2Error oauth2Error = new OAuth2Error("oidc_provider_not_configured",
+						"An OpenID Connect Authentication Provider has not been configured. "
+								+ "Check to ensure you include the dependency 'spring-security-oauth2-jose'.",
+						null);
+				throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+			}
+
+			return null;
+		}
+
+		@Override
+		public boolean supports(Class<?> authentication) {
+			return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication);
+		}
+	}
+
+	/**
+	 * Wrapper bean class to provide configuration from applicationContext
+	 */
+	private static class OAuth2LoginBeanConfig implements ApplicationContextAware {
+
+		private ApplicationContext appContext;
+
+		@Override
+		public void setApplicationContext(ApplicationContext appContext) throws BeansException {
+			this.appContext = appContext;
+		}
+
+		@SuppressWarnings({ "unchecked", "unused" })
+		public Map<String, String> getLoginLinks() {
+			Iterable<ClientRegistration> clientRegistrations = null;
+			ClientRegistrationRepository clientRegistrationRepository = appContext
+					.getBean(ClientRegistrationRepository.class);
+			ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository).as(Iterable.class);
+			if (type != ResolvableType.NONE && ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
+				clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
+			}
+			if (clientRegistrations == null) {
+				return Collections.emptyMap();
+			}
+
+			String authorizationRequestBaseUri = DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
+			Map<String, String> loginUrlToClientName = new HashMap<>();
+			clientRegistrations.forEach(registration -> loginUrlToClientName.put(
+					authorizationRequestBaseUri + "/" + registration.getRegistrationId(),
+					registration.getClientName()));
+
+			return loginUrlToClientName;
+		}
+
 	}
 }

+ 3 - 1
config/src/main/java/org/springframework/security/config/http/SecurityFilters.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2016 the original author or authors.
+ * 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.
@@ -34,9 +34,11 @@ enum SecurityFilters {
 	HEADERS_FILTER, CORS_FILTER,
 	CSRF_FILTER,
 	LOGOUT_FILTER,
+	OAUTH2_REDIRECT_FILTER,
 	X509_FILTER,
 	PRE_AUTH_FILTER,
 	CAS_FILTER,
+	OAUTH2_LOGIN_FILTER,
 	FORM_LOGIN_FILTER,
 	OPENID_FILTER,
 	LOGIN_PAGE_FILTER,

+ 204 - 3
config/src/main/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParser.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * 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.
@@ -15,19 +15,220 @@
  */
 package org.springframework.security.config.oauth2.client;
 
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
 import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.parsing.BeanComponentDefinition;
+import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
+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.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrations;
+import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
+import org.springframework.security.oauth2.core.AuthenticationMethod;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.util.StringUtils;
+import org.springframework.util.xml.DomUtils;
 import org.w3c.dom.Element;
 
 /**
- * @author
+ * @author Ruby Hartono
+ * @since 5.3
  */
 public final class ClientRegistrationsBeanDefinitionParser implements BeanDefinitionParser {
 
+	private static final String ELT_CLIENT_REGISTRATION = "client-registration";
+	private static final String ELT_PROVIDER = "provider";
+	private static final String ATT_REGISTRATION_ID = "registration-id";
+	private static final String ATT_CLIENT_ID = "client-id";
+	private static final String ATT_CLIENT_SECRET = "client-secret";
+	private static final String ATT_CLIENT_AUTHENTICATION_METHOD = "client-authentication-method";
+	private static final String ATT_AUTHORIZATION_GRANT_TYPE = "authorization-grant-type";
+	private static final String ATT_REDIRECT_URI = "redirect-uri";
+	private static final String ATT_SCOPE = "scope";
+	private static final String ATT_CLIENT_NAME = "client-name";
+	private static final String ATT_PROVIDER_ID = "provider-id";
+	private static final String ATT_AUTHORIZATION_URI = "authorization-uri";
+	private static final String ATT_TOKEN_URI = "token-uri";
+	private static final String ATT_USERINFO_URI = "userinfo-uri";
+	private static final String ATT_USERINFO_AUTHENTICATION_METHOD = "userinfo-authentication-method";
+	private static final String ATT_USERNAME_ATTRIBUTE_NAME = "username-attribute-name";
+	private static final String ATT_JWKSET_URI = "jwkset-uri";
+	private static final String ATT_ISSUER_URI = "issuer-uri";
+
 	@Override
 	public BeanDefinition parse(Element element, ParserContext parserContext) {
-		// TODO Implement
+		CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(),
+				parserContext.extractSource(element));
+		parserContext.pushContainingComponent(compositeDef);
+
+		Map<String, Map<String, String>> providerDetailMap = getProviders(element);
+
+		List<ClientRegistration> clientRegs = getClientRegistrations(element, parserContext, providerDetailMap);
+
+		BeanDefinition inMemClientRegRepoBeanDef = BeanDefinitionBuilder
+				.rootBeanDefinition(InMemoryClientRegistrationRepository.class).addConstructorArgValue(clientRegs)
+				.getBeanDefinition();
+		String beanName = parserContext.getReaderContext().generateBeanName(inMemClientRegRepoBeanDef);
+		parserContext.registerBeanComponent(new BeanComponentDefinition(inMemClientRegRepoBeanDef, beanName));
+
+		parserContext.popAndRegisterContainingComponent();
 		return null;
 	}
+
+	private List<ClientRegistration> getClientRegistrations(Element element, ParserContext parserContext,
+			Map<String, Map<String, String>> providerDetailMap) {
+		List<Element> clientRegElts = DomUtils.getChildElementsByTagName(element, ELT_CLIENT_REGISTRATION);
+		List<ClientRegistration> clientRegs = new ArrayList<>();
+
+		for (Element clientRegElt : clientRegElts) {
+			String regId = clientRegElt.getAttribute(ATT_REGISTRATION_ID);
+			String clientId = clientRegElt.getAttribute(ATT_CLIENT_ID);
+			String clientSecret = clientRegElt.getAttribute(ATT_CLIENT_SECRET);
+			String clientAuthMethod = clientRegElt.getAttribute(ATT_CLIENT_AUTHENTICATION_METHOD);
+			String authGrantType = clientRegElt.getAttribute(ATT_AUTHORIZATION_GRANT_TYPE);
+			String redirectUri = clientRegElt.getAttribute(ATT_REDIRECT_URI);
+			String scope = clientRegElt.getAttribute(ATT_SCOPE);
+			String clientName = clientRegElt.getAttribute(ATT_CLIENT_NAME);
+			String providerId = clientRegElt.getAttribute(ATT_PROVIDER_ID);
+
+			Set<String> scopes = StringUtils.commaDelimitedListToSet(scope);
+			ClientRegistration.Builder builder = getBuilderFromIssuerIfPossible(regId, providerId, providerDetailMap);
+			if (builder == null) {
+				builder = getBuilder(regId, providerId, providerDetailMap);
+				if (builder == null) {
+					Object source = parserContext.extractSource(element);
+					parserContext.getReaderContext().error(getErrorMessage(providerId, regId), source);
+					// error on the config skip to next element
+					break;
+				}
+			}
+
+			ClientRegistration clientReg = builder.clientId(clientId)
+					.clientSecret(clientSecret)
+					.clientAuthenticationMethod(new ClientAuthenticationMethod(clientAuthMethod))
+					.authorizationGrantType(new AuthorizationGrantType(authGrantType))
+					.redirectUriTemplate(redirectUri)
+					.scope(scopes)
+					.clientName(clientName)
+					.build();
+			clientRegs.add(clientReg);
+		}
+		return clientRegs;
+	}
+
+	private Map<String, Map<String, String>> getProviders(Element element) {
+		List<Element> providerRegElts = DomUtils.getChildElementsByTagName(element, ELT_PROVIDER);
+		Map<String, Map<String, String>> providerDetailMap = new HashMap<>();
+		for (Element providerRegElt : providerRegElts) {
+			Map<String, String> detail = new HashMap<String, String>();
+			String providerId = providerRegElt.getAttribute(ATT_PROVIDER_ID);
+			detail.put(ATT_PROVIDER_ID, providerId);
+			detail.put(ATT_AUTHORIZATION_URI, providerRegElt.getAttribute(ATT_AUTHORIZATION_URI));
+			detail.put(ATT_TOKEN_URI, providerRegElt.getAttribute(ATT_TOKEN_URI));
+			detail.put(ATT_USERINFO_URI, providerRegElt.getAttribute(ATT_USERINFO_URI));
+			detail.put(ATT_USERINFO_AUTHENTICATION_METHOD,
+					providerRegElt.getAttribute(ATT_USERINFO_AUTHENTICATION_METHOD));
+			detail.put(ATT_USERNAME_ATTRIBUTE_NAME, providerRegElt.getAttribute(ATT_USERNAME_ATTRIBUTE_NAME));
+			detail.put(ATT_JWKSET_URI, providerRegElt.getAttribute(ATT_JWKSET_URI));
+			detail.put(ATT_ISSUER_URI, providerRegElt.getAttribute(ATT_ISSUER_URI));
+
+			providerDetailMap.put(providerId, detail);
+		}
+		return providerDetailMap;
+	}
+
+	private static ClientRegistration.Builder getBuilderFromIssuerIfPossible(String registrationId,
+			String configuredProviderId, Map<String, Map<String, String>> providers) {
+		String providerId = (configuredProviderId != null) ? configuredProviderId : registrationId;
+		if (providers.containsKey(providerId)) {
+			Map<String, String> provider = providers.get(providerId);
+			String issuer = provider.get(ATT_ISSUER_URI);
+			if (!StringUtils.isEmpty(issuer)) {
+				ClientRegistration.Builder builder = ClientRegistrations.fromIssuerLocation(issuer)
+						.registrationId(registrationId);
+				return getBuilder(builder, provider);
+			}
+		}
+		return null;
+	}
+
+	private static ClientRegistration.Builder getBuilder(String registrationId, String configuredProviderId,
+			Map<String, Map<String, String>> providers) {
+		String providerId = (configuredProviderId != null) ? configuredProviderId : registrationId;
+		CommonOAuth2Provider provider = getCommonProvider(providerId);
+		if (provider == null && !providers.containsKey(providerId)) {
+			return null;
+		}
+		ClientRegistration.Builder builder = (provider != null) ? provider.getBuilder(registrationId)
+				: ClientRegistration.withRegistrationId(registrationId);
+		if (providers.containsKey(providerId)) {
+			return getBuilder(builder, providers.get(providerId));
+		}
+		return builder;
+	}
+
+	private static ClientRegistration.Builder getBuilder(ClientRegistration.Builder builder,
+			Map<String, String> provider) {
+		getOptionalIfNotEmpty(provider.get(ATT_AUTHORIZATION_URI)).ifPresent(builder::authorizationUri);
+		getOptionalIfNotEmpty(provider.get(ATT_TOKEN_URI)).ifPresent(builder::tokenUri);
+		getOptionalIfNotEmpty(provider.get(ATT_USERINFO_URI)).ifPresent(builder::userInfoUri);
+		getOptionalIfNotEmpty(provider.get(ATT_USERINFO_AUTHENTICATION_METHOD)).map(AuthenticationMethod::new)
+				.ifPresent(builder::userInfoAuthenticationMethod);
+		getOptionalIfNotEmpty(provider.get(ATT_JWKSET_URI)).ifPresent(builder::jwkSetUri);
+		getOptionalIfNotEmpty(provider.get(ATT_USERNAME_ATTRIBUTE_NAME)).ifPresent(builder::userNameAttributeName);
+		return builder;
+	}
+
+	private static Optional<String> getOptionalIfNotEmpty(String str) {
+		return Optional.ofNullable(str).filter(s -> !s.isEmpty());
+	}
+
+	private static CommonOAuth2Provider getCommonProvider(String providerId) {
+		try {
+			String value = providerId.toString().trim();
+			if (value.isEmpty()) {
+				return null;
+			}
+			try {
+				return CommonOAuth2Provider.valueOf(value);
+			} catch (Exception ex) {
+				return findEnum(value);
+			}
+		} catch (Exception ex) {
+			return null;
+		}
+	}
+
+	private static CommonOAuth2Provider findEnum(String value) {
+		String name = getCanonicalName(value);
+		for (CommonOAuth2Provider candidate : EnumSet.allOf(CommonOAuth2Provider.class)) {
+			String candidateName = getCanonicalName(candidate.name());
+			if (name.equals(candidateName)) {
+				return candidate;
+			}
+		}
+		throw new IllegalArgumentException(
+				"No enum constant " + CommonOAuth2Provider.class.getCanonicalName() + "." + value);
+	}
+
+	private static String getCanonicalName(String name) {
+		StringBuilder canonicalName = new StringBuilder(name.length());
+		name.chars().filter(Character::isLetterOrDigit).map(Character::toLowerCase)
+				.forEach((c) -> canonicalName.append((char) c));
+		return canonicalName.toString();
+	}
+
+	private static String getErrorMessage(String configuredProviderId, String registrationId) {
+		return ((configuredProviderId != null) ? "Unknown provider ID '" + configuredProviderId + "'"
+				: "Provider ID must be specified for client registration '" + registrationId + "'");
+	}
 }

+ 46 - 4
config/src/main/resources/org/springframework/security/config/spring-security-5.3.rnc

@@ -439,11 +439,53 @@ form-login.attlist &=
 
 oauth2-login =
 	## Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider.
-	element oauth2-login {empty}
+	element oauth2-login {oauth2-login.attlist}
+oauth2-login.attlist &=
+	## Reference to ClientRegistrationRepository
+	attribute client-registration-repository-ref {xsd:token}?
+oauth2-login.attlist &=
+	## Reference to OAuth2AuthorizedClientRepository
+	attribute authorized-client-repository-ref {xsd:token}?
+oauth2-login.attlist &=
+	## Reference to OAuth2AuthorizedClientService
+	attribute authorized-client-service-ref {xsd:token}?
+oauth2-login.attlist &=
+	## Reference to AuthorizationRequestRepository
+	attribute authorization-request-repository-ref {xsd:token}?
+oauth2-login.attlist &=
+	## Reference to OAuth2AuthorizationRequestResolver
+	attribute authorization-request-resolver-ref {xsd:token}?
+oauth2-login.attlist &=
+	## Reference to OAuth2AccessTokenResponseClient
+	attribute access-token-response-client-ref {xsd:token}?
+oauth2-login.attlist &=
+	## Reference to GrantedAuthoritiesMapper
+	attribute user-authorities-mapper-ref {xsd:token}?
+oauth2-login.attlist &=
+	## Reference to OAuth2UserService
+	attribute user-service-ref {xsd:token}?
+oauth2-login.attlist &=
+	## Reference to OidcUserService
+	attribute oidc-user-service-ref {xsd:token}?
+oauth2-login.attlist &=
+	## Specifies the URL to validate the credentials.
+	attribute login-processing-url {xsd:token}?
+oauth2-login.attlist &=
+	## Specifies the URL to send users to if login is required
+	attribute login-page {xsd:token}?
+oauth2-login.attlist &=
+	## Specifies authentication success handler
+	attribute authentication-success-handler-ref {xsd:token}?
+oauth2-login.attlist &=
+	## Specifies authentication failure handler
+	attribute authentication-failure-handler-ref {xsd:token}?
+oauth2-login.attlist &=
+	## Specifies JWT decoder factory for OidcAuthorizationCodeAuthenticationProvider
+	attribute jwt-decoder-factory-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?}
+	element client-registrations {client-registration+, provider*}
 
 client-registration =
 	## Represents a client registered with an OAuth 2.0 or OpenID Connect 1.0 Provider.
@@ -487,7 +529,7 @@ provider.attlist &=
 	attribute authorization-uri {xsd:token}?
 provider.attlist &=
 	## The Token Endpoint URI for the Authorization Server.
-	attribute token-uri {xsd:token}
+	attribute token-uri {xsd:token}?
 provider.attlist &=
 	## The UserInfo Endpoint URI used to access the claims/attributes of the authenticated end-user.
 	attribute userinfo-uri {xsd:token}?
@@ -982,4 +1024,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" | "X509_FILTER" | "PRE_AUTH_FILTER" | "CAS_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_REDIRECT_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"

+ 93 - 3
config/src/main/resources/org/springframework/security/config/spring-security-5.3.xsd

@@ -1456,8 +1456,96 @@
          <xs:documentation>Configures authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider.
                 </xs:documentation>
       </xs:annotation>
-      <xs:complexType/>
+      <xs:complexType>
+         <xs:attributeGroup ref="security:oauth2-login.attlist"/>
+      </xs:complexType>
    </xs:element>
+  <xs:attributeGroup name="oauth2-login.attlist">
+      <xs:attribute name="client-registration-repository-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to ClientRegistrationRepository
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="authorized-client-repository-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to OAuth2AuthorizedClientRepository
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="authorized-client-service-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to OAuth2AuthorizedClientService
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="authorization-request-repository-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to AuthorizationRequestRepository
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="authorization-request-resolver-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to OAuth2AuthorizationRequestResolver
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="access-token-response-client-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to OAuth2AccessTokenResponseClient
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="user-authorities-mapper-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to GrantedAuthoritiesMapper
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="user-service-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to OAuth2UserService
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="oidc-user-service-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Reference to OidcUserService
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="login-processing-url" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Specifies the URL to validate the credentials.
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="login-page" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Specifies the URL to send users to if login is required
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="authentication-success-handler-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Specifies authentication success handler
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="authentication-failure-handler-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Specifies authentication failure handler
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="jwt-decoder-factory-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>Specifies JWT decoder factory for OidcAuthorizationCodeAuthenticationProvider
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+  </xs:attributeGroup>
   <xs:element name="client-registrations">
       <xs:annotation>
          <xs:documentation>Container element for client(s) registered with an OAuth 2.0 or OpenID Connect 1.0
@@ -1467,7 +1555,7 @@
       <xs:complexType>
          <xs:sequence>
             <xs:element maxOccurs="unbounded" ref="security:client-registration"/>
-            <xs:element minOccurs="0" ref="security:provider"/>
+            <xs:element minOccurs="0" maxOccurs="unbounded" ref="security:provider"/>
          </xs:sequence>
       </xs:complexType>
    </xs:element>
@@ -1580,7 +1668,7 @@
                 </xs:documentation>
          </xs:annotation>
       </xs:attribute>
-      <xs:attribute name="token-uri" use="required" type="xs:token">
+      <xs:attribute name="token-uri" type="xs:token">
          <xs:annotation>
             <xs:documentation>The Token Endpoint URI for the Authorization Server.
                 </xs:documentation>
@@ -2901,9 +2989,11 @@
          <xs:enumeration value="CORS_FILTER"/>
          <xs:enumeration value="CSRF_FILTER"/>
          <xs:enumeration value="LOGOUT_FILTER"/>
+         <xs:enumeration value="OAUTH2_REDIRECT_FILTER"/>
          <xs:enumeration value="X509_FILTER"/>
          <xs:enumeration value="PRE_AUTH_FILTER"/>
          <xs:enumeration value="CAS_FILTER"/>
+         <xs:enumeration value="OAUTH2_LOGIN_FILTER"/>
          <xs:enumeration value="FORM_LOGIN_FILTER"/>
          <xs:enumeration value="OPENID_FILTER"/>
          <xs:enumeration value="LOGIN_PAGE_FILTER"/>

+ 1 - 1
config/src/test/java/org/springframework/security/config/doc/XsdDocumentedTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * 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.

+ 591 - 8
config/src/test/java/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2019 the original author or authors.
+ * 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.
@@ -17,39 +17,622 @@ package org.springframework.security.config.http;
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.internal.util.MockUtil;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationListener;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
 import org.springframework.security.config.test.SpringTestRule;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
+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.registration.TestClientRegistrations;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
+import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
+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.client.web.OAuth2LoginAuthenticationFilter;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+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.oauth2.core.endpoint.TestOAuth2AuthorizationRequests;
+import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.oauth2.core.user.TestOAuth2Users;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
+import org.springframework.security.oauth2.jwt.TestJwts;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.context.SecurityContextRepository;
 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 static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+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.redirectedUrlPattern;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
 import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 
+import java.lang.reflect.Field;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
 /**
  * Tests for {@link OAuth2LoginBeanDefinitionParser}.
  *
- * @author
+ * @author Ruby Hartono
  */
 public class OAuth2LoginBeanDefinitionParserTests {
-	private static final String CONFIG_LOCATION_PREFIX =
-			"classpath:org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests";
+	private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests";
 
 	@Rule
 	public final SpringTestRule spring = new SpringTestRule();
 
+	@Autowired
+	private ClientRegistrationRepository clientRegistrationRepository;
+
+	@Autowired
+	private OAuth2LoginAuthenticationFilter oauth2LoginAuthenticationFilter;
+
+	@Autowired(required = false)
+	private OAuth2AuthorizedClientRepository oauth2AuthorizedClientRepository;
+
+	@Autowired(required = false)
+	private OAuth2AuthorizedClientService oauth2AuthorizedClientService;
+
+	@Autowired
+	SecurityContextRepository securityContextRepository;
+
+	@Autowired(required = false)
+	private ApplicationListener<AuthenticationSuccessEvent> authenticationSuccessListener;
+
+	@Autowired(required = false)
+	private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;
+
+	@Autowired(required = false)
+	private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
+
+	@Autowired(required = false)
+	private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService;
+
+	@Autowired(required = false)
+	private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory;
+
+	@Autowired(required = false)
+	private OAuth2AuthorizationRequestResolver oauth2AuthorizationRequestResolver;
+
+	@Autowired(required = false)
+	private GrantedAuthoritiesMapper grantedAuthoritiesMapper;
+
+	@Autowired(required = false)
+	private AuthenticationFailureHandler authenticationFailureHandler;
+
+	@Autowired(required = false)
+	private AuthenticationSuccessHandler authenticationSuccessHandler;
+
 	@Autowired
 	private MockMvc mvc;
 
+	@Test
+	public void requestLoginWhenMultiClientRegistrationThenReturnLoginPageWithOauth2Login() throws Exception {
+		this.spring.configLocations(this.xml("MultiClientRegistration")).autowire();
+
+		MvcResult result = this.mvc.perform(get("/login")).andExpect(status().is2xxSuccessful()).andReturn();
+
+		assertThat(result.getResponse().getContentAsString())
+				.contains("<a href=\"/oauth2/authorization/google-login\">Google</a>");
+		assertThat(result.getResponse().getContentAsString())
+				.contains("<a href=\"/oauth2/authorization/github-login\">Github</a>");
+	}
+
+	// gh-5347
 	@Test
 	public void requestWhenSingleClientRegistrationThenAutoRedirect() throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration-WithinSameFile")).autowire();
+
+		this.mvc.perform(get("/")).andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("http://localhost/oauth2/authorization/google-login"));
+	}
+
+	// gh-5347
+	@Test
+	public void requestWhenSingleClientRegistrationAndRequestFaviconNotAuthenticatedThenRedirectDefaultLoginPage()
+			throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration")).autowire();
+
+		this.mvc.perform(get("/favicon.ico").accept(new MediaType("image", "*"))).andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("http://localhost/login"));
+	}
+
+	// gh-6812
+	@Test
+	public void requestWhenSingleClientRegistrationAndRequestXHRNotAuthenticatedThenDoesNotRedirectForAuthorization()
+			throws Exception {
 		this.spring.configLocations(this.xml("SingleClientRegistration")).autowire();
 
-		this.mvc.perform(get("/"))
-				.andExpect(status().is3xxRedirection())
-				.andExpect(redirectedUrlPattern("/oauth2/authorization/google-login"));
+		this.mvc.perform(get("/").header("X-Requested-With", "XMLHttpRequest")).andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("http://localhost/login"));
+	}
+
+	@Test
+	public void requestWhenAuthorizationRequestNotFoundThenThrowAuthenticationException() throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomAuthenticationFailureHandler"))
+				.autowire();
+
+		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+		params.add("code", "code123");
+		params.add("state", "state123");
+		this.mvc.perform(get("/login/oauth2/code/google").params(params));
+
+		// assertions
+		ArgumentCaptor<AuthenticationException> exceptionCaptor = ArgumentCaptor
+				.forClass(AuthenticationException.class);
+		verify(authenticationFailureHandler).onAuthenticationFailure(any(), any(), exceptionCaptor.capture());
+		AuthenticationException excValue = exceptionCaptor.getValue();
+		assertThat(excValue).isInstanceOf(OAuth2AuthenticationException.class);
+		assertThat(((OAuth2AuthenticationException) excValue).getError().getErrorCode())
+				.isEqualTo("authorization_request_not_found");
+	}
+
+	@Test
+	public void requestWhenAuthorizationResponseValidThenAuthenticate() throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration-WithTestConfiguration")).autowire();
+		OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		OAuth2User oauth2User = TestOAuth2Users.create();
+		when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User);
+
+		Map<String, Object> attributes = new HashMap<>();
+		attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login");
+		OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes)
+				.build();
+		when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest);
+
+		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+		params.add("code", "code123");
+		params.add("state", authRequest.getState());
+		this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().is2xxSuccessful());
+
+		ArgumentCaptor<Authentication> authenticationCaptor = ArgumentCaptor.forClass(Authentication.class);
+		verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture());
+		Authentication authenticationValue = authenticationCaptor.getValue();
+		assertThat(authenticationValue.getAuthorities()).hasSize(1);
+		assertThat(authenticationValue.getAuthorities()).first().isInstanceOf(SimpleGrantedAuthority.class)
+				.hasToString("ROLE_USER");
+	}
+
+	// gh-6009
+	@Test
+	public void requestWhenAuthorizationResponseValidThenAuthenticationSuccessEventPublished() throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration-WithTestConfiguration")).autowire();
+		OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		OAuth2User oauth2User = TestOAuth2Users.create();
+		when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User);
+
+		Map<String, Object> attributes = new HashMap<>();
+		attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login");
+		OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes)
+				.build();
+		when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest);
+
+		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+		params.add("code", "code123");
+		params.add("state", authRequest.getState());
+		this.mvc.perform(get("/login/oauth2/code/google").params(params));
+
+		// assertions
+		verify(authenticationSuccessListener).onApplicationEvent(any(AuthenticationSuccessEvent.class));
+	}
+
+	@Test
+	public void requestWhenOidcAuthenticationResponseValidThenJwtDecoderFactoryCalled() throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration-WithJwtDecoderFactoryAndDefaultSuccessHandler"))
+				.autowire();
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put(OidcParameterNames.ID_TOKEN, "token123");
+		OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().additionalParameters(additionalParameters)
+				.build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		Jwt jwt = TestJwts.user();
+		when(this.jwtDecoderFactory.createDecoder(any())).thenReturn(token -> jwt);
+
+		Map<String, Object> attributes = new HashMap<>();
+		attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login");
+		OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes)
+				.scope("openid").build();
+		when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest);
+
+		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+		params.add("code", "code123");
+		params.add("state", authRequest.getState());
+		this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("/"));
+	}
+
+	@SuppressWarnings({ "unchecked", "rawtypes" })
+	@Test
+	public void requestWhenCustomGrantedAuthoritiesMapperThenCalled() throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomGrantedAuthorities")).autowire();
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put(OidcParameterNames.ID_TOKEN, "token123");
+		OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().additionalParameters(additionalParameters)
+				.build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		OAuth2User oauth2User = TestOAuth2Users.create();
+		when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User);
+
+		GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_OAUTH2_USER");
+		when(this.grantedAuthoritiesMapper.mapAuthorities(any()))
+				.thenReturn((Collection) Collections.singletonList(grantedAuthority));
+
+		Map<String, Object> attributes = new HashMap<>();
+		attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login");
+		OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes)
+				.build();
+		when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest);
+
+		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+		params.add("code", "code123");
+		params.add("state", authRequest.getState());
+		this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().is2xxSuccessful());
+
+		ArgumentCaptor<Authentication> authenticationCaptor = ArgumentCaptor.forClass(Authentication.class);
+		verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture());
+		Authentication authenticationValue = authenticationCaptor.getValue();
+		assertThat(authenticationValue.getAuthorities()).hasSize(1);
+		assertThat(authenticationValue.getAuthorities()).first().isInstanceOf(SimpleGrantedAuthority.class)
+				.hasToString("ROLE_OAUTH2_USER");
+
+		// re-setup for OIDC test
+		Jwt jwt = TestJwts.user();
+		when(this.jwtDecoderFactory.createDecoder(any())).thenReturn(token -> jwt);
+
+		grantedAuthority = new SimpleGrantedAuthority("ROLE_OIDC_USER");
+		when(this.grantedAuthoritiesMapper.mapAuthorities(any()))
+				.thenReturn((Collection) Collections.singletonList(grantedAuthority));
+
+		authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes).scope("openid").build();
+		when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest);
+
+		this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().is2xxSuccessful());
+
+		authenticationCaptor = ArgumentCaptor.forClass(Authentication.class);
+		verify(authenticationSuccessHandler, times(2)).onAuthenticationSuccess(any(), any(),
+				authenticationCaptor.capture());
+		authenticationValue = authenticationCaptor.getValue();
+		assertThat(authenticationValue.getAuthorities()).hasSize(1);
+		assertThat(authenticationValue.getAuthorities()).first().isInstanceOf(SimpleGrantedAuthority.class)
+				.hasToString("ROLE_OIDC_USER");
+	}
+
+	@SuppressWarnings({ "unchecked", "rawtypes" })
+	@Test
+	public void successOidcLoginWhenSingleClientRegistrationAndCustomAuthoritiesThenReturnSuccessWithCorrectAuthorities()
+			throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration-WithJwtDecoderFactory")).autowire();
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put(OidcParameterNames.ID_TOKEN, "token123");
+		OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().additionalParameters(additionalParameters)
+				.build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		Jwt jwt = TestJwts.user();
+		when(this.jwtDecoderFactory.createDecoder(any())).thenReturn(token -> jwt);
+
+		GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_OIDC_USER");
+		when(this.grantedAuthoritiesMapper.mapAuthorities(any()))
+				.thenReturn((Collection) Collections.singletonList(grantedAuthority));
+
+		Map<String, Object> attributes = new HashMap<>();
+		attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login");
+		OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes)
+				.scope("openid").build();
+		when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest);
+
+		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+		params.add("code", "code123");
+		params.add("state", authRequest.getState());
+		this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().is2xxSuccessful());
+
+		ArgumentCaptor<Authentication> authenticationCaptor = ArgumentCaptor.forClass(Authentication.class);
+		verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture());
+		Authentication authenticationValue = authenticationCaptor.getValue();
+		assertThat(authenticationValue.getAuthorities()).hasSize(1);
+		assertThat(authenticationValue.getAuthorities()).first().isInstanceOf(SimpleGrantedAuthority.class)
+				.hasToString("ROLE_OIDC_USER");
+	}
+
+	// gh-5488
+	@Test
+	public void requestWhenCustomLoginProcessingUrlThenProcessAuthentication() throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomLoginProcessingUrl")).autowire();
+		OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		OAuth2User oauth2User = TestOAuth2Users.create();
+		when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User);
+
+		Map<String, Object> attributes = new HashMap<>();
+		attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login");
+		OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes)
+				.build();
+		when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest);
+
+		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+		params.add("code", "code123");
+		params.add("state", authRequest.getState());
+		this.mvc.perform(get("/login/oauth2/google").params(params)).andExpect(status().is2xxSuccessful());
+
+		ArgumentCaptor<Authentication> authenticationCaptor = ArgumentCaptor.forClass(Authentication.class);
+		verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture());
+		Authentication authenticationValue = authenticationCaptor.getValue();
+		assertThat(authenticationValue.getAuthorities()).hasSize(1);
+		assertThat(authenticationValue.getAuthorities()).first().isInstanceOf(SimpleGrantedAuthority.class)
+				.hasToString("ROLE_USER");
+	}
+
+	// gh-5521
+	@Test
+	public void requestWhenCustomAuthorizationRequestResolverThenCalled() throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomAuthorizationRequestResolver"))
+				.autowire();
+
+		this.mvc.perform(get("/oauth2/authorization/google")).andExpect(status().is3xxRedirection());
+
+		verify(oauth2AuthorizationRequestResolver).resolve(any());
+	}
+
+	// gh-5347
+	@Test
+	public void requestWhenMultipleClientsConfiguredThenRedirectDefaultLoginPage() throws Exception {
+		this.spring.configLocations(this.xml("MultiClientRegistration")).autowire();
+
+		this.mvc.perform(get("/")).andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("http://localhost/login"));
+	}
+
+	@Test
+	public void requestWhenCustomLoginPageThenRedirectCustomLoginPage() throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomLoginPage")).autowire();
+
+		this.mvc.perform(get("/")).andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("http://localhost/custom-login"));
+	}
+
+	// gh-6802
+	@Test
+	public void requestWhenSingleClientRegistrationAndFormLoginConfiguredThenRedirectDefaultLoginPage()
+			throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration-WithFormLogin")).autowire();
+
+		this.mvc.perform(get("/")).andExpect(status().is3xxRedirection())
+				.andExpect(redirectedUrl("http://localhost/login"));
+	}
+
+	@Test
+	public void requestWhenCustomClientRegistrationRepositoryThenCalled() throws Exception {
+		this.spring.configLocations(this.xml("WithCustomClientRegistrationRepository")).autowire();
+
+		OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		OAuth2User oauth2User = TestOAuth2Users.create();
+		when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User);
+
+		Map<String, Object> attributes = new HashMap<>();
+		attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login");
+		OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes)
+				.build();
+		when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest);
+
+		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+		params.add("code", "code123");
+		params.add("state", authRequest.getState());
+		this.mvc.perform(get("/login/oauth2/code/google").params(params));
+
+		assertThat(MockUtil.isMock(clientRegistrationRepository)).isTrue();
+		verify(clientRegistrationRepository).findByRegistrationId("google-login");
+
+		Field field = oauth2LoginAuthenticationFilter.getClass().getDeclaredField("clientRegistrationRepository");
+		field.setAccessible(true);
+		Object fieldVal = field.get(oauth2LoginAuthenticationFilter);
+		assertThat(MockUtil.isMock(fieldVal)).isTrue();
+		assertThat(fieldVal).isSameAs(clientRegistrationRepository);
+	}
+
+	@Test
+	public void requestWhenCustomAuthorizedClientRepositoryThenCalled() throws Exception {
+		this.spring.configLocations(this.xml("WithCustomAuthorizedClientRepository")).autowire();
+
+		ClientRegistration clientReg = TestClientRegistrations.clientRegistration().build();
+		when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn(clientReg);
+
+		OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		OAuth2User oauth2User = TestOAuth2Users.create();
+		when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User);
+
+		Map<String, Object> attributes = new HashMap<>();
+		attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login");
+		OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes)
+				.build();
+		when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest);
+
+		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+		params.add("code", "code123");
+		params.add("state", authRequest.getState());
+		this.mvc.perform(get("/login/oauth2/code/registration-id").params(params));
+
+		assertThat(MockUtil.isMock(oauth2AuthorizedClientRepository)).isTrue();
+		verify(oauth2AuthorizedClientRepository).saveAuthorizedClient(any(), any(), any(), any());
+
+		Field field = oauth2LoginAuthenticationFilter.getClass().getDeclaredField("authorizedClientRepository");
+		field.setAccessible(true);
+		Object fieldVal = field.get(oauth2LoginAuthenticationFilter);
+		assertThat(MockUtil.isMock(fieldVal)).isTrue();
+		assertThat(fieldVal).isSameAs(oauth2AuthorizedClientRepository);
+	}
+
+	@Test
+	public void requestWhenCustomAuthorizedClientServiceThenCalled() throws Exception {
+		this.spring.configLocations(this.xml("WithCustomAuthorizedClientService")).autowire();
+
+		ClientRegistration clientReg = TestClientRegistrations.clientRegistration().build();
+		when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn(clientReg);
+
+		OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		OAuth2User oauth2User = TestOAuth2Users.create();
+		when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User);
+
+		Map<String, Object> attributes = new HashMap<>();
+		attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login");
+		OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes)
+				.build();
+		when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest);
+
+		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+		params.add("code", "code123");
+		params.add("state", authRequest.getState());
+		this.mvc.perform(get("/login/oauth2/code/registration-id").params(params));
+
+		assertThat(MockUtil.isMock(oauth2AuthorizedClientService)).isTrue();
+		verify(oauth2AuthorizedClientService).saveAuthorizedClient(any(), any());
+
+		Field authorizedClientRepositoryField = oauth2LoginAuthenticationFilter.getClass()
+				.getDeclaredField("authorizedClientRepository");
+		authorizedClientRepositoryField.setAccessible(true);
+		Object authorizedClientRepositoryFieldVal = authorizedClientRepositoryField
+				.get(oauth2LoginAuthenticationFilter);
+		assertThat(authorizedClientRepositoryFieldVal)
+				.isInstanceOf(AuthenticatedPrincipalOAuth2AuthorizedClientRepository.class);
+
+		Field authorizedClientServiceField = authorizedClientRepositoryFieldVal.getClass()
+				.getDeclaredField("authorizedClientService");
+		authorizedClientServiceField.setAccessible(true);
+		Object authorizedClientServiceFieldVal = authorizedClientServiceField.get(authorizedClientRepositoryFieldVal);
+		assertThat(MockUtil.isMock(authorizedClientServiceFieldVal)).isTrue();
+		assertThat(authorizedClientServiceFieldVal).isSameAs(oauth2AuthorizedClientService);
+	}
+
+	@Test
+	public void requestWhenCustomAuthorizationRequestRepositoryThenCalled() throws Exception {
+		this.spring.configLocations(this.xml("WithCustomAuthorizationRequestRepository")).autowire();
+
+		ClientRegistration clientReg = TestClientRegistrations.clientRegistration().build();
+		when(this.clientRegistrationRepository.findByRegistrationId(any())).thenReturn(clientReg);
+
+		OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		OAuth2User oauth2User = TestOAuth2Users.create();
+		when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User);
+
+		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+		params.add("code", "code123");
+		params.add("state", "state");
+		this.mvc.perform(get("/login/oauth2/code/registration-id").params(params));
+
+		assertThat(MockUtil.isMock(authorizationRequestRepository)).isTrue();
+		verify(authorizationRequestRepository).removeAuthorizationRequest(any(), any());
+
+		Field authorizationRequestRepositoryField = oauth2LoginAuthenticationFilter.getClass()
+				.getDeclaredField("authorizationRequestRepository");
+		authorizationRequestRepositoryField.setAccessible(true);
+		Object authorizationRequestRepositoryFieldVal = authorizationRequestRepositoryField
+				.get(oauth2LoginAuthenticationFilter);
+		assertThat(MockUtil.isMock(authorizationRequestRepositoryFieldVal)).isTrue();
+		assertThat(authorizationRequestRepositoryFieldVal).isSameAs(authorizationRequestRepository);
+	}
+
+	@Test
+	public void requestWhenCustomAuthenticationSuccessHandlerThenCalled() throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomAuthenticationHandler")).autowire();
+		OAuth2AccessTokenResponse accessTokenResponse = accessTokenResponse().build();
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		OAuth2User oauth2User = TestOAuth2Users.create();
+		when(this.oauth2UserService.loadUser(any())).thenReturn(oauth2User);
+
+		Map<String, Object> attributes = new HashMap<>();
+		attributes.put(OAuth2ParameterNames.REGISTRATION_ID, "google-login");
+		OAuth2AuthorizationRequest authRequest = TestOAuth2AuthorizationRequests.request().attributes(attributes)
+				.build();
+		when(this.authorizationRequestRepository.removeAuthorizationRequest(any(), any())).thenReturn(authRequest);
+
+		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+		params.add("code", "code123");
+		params.add("state", authRequest.getState());
+		this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().is2xxSuccessful());
+
+		ArgumentCaptor<Authentication> authenticationCaptor = ArgumentCaptor.forClass(Authentication.class);
+		verify(authenticationSuccessHandler).onAuthenticationSuccess(any(), any(), authenticationCaptor.capture());
+		Authentication authenticationValue = authenticationCaptor.getValue();
+		assertThat(authenticationValue.getAuthorities()).hasSize(1);
+		assertThat(authenticationValue.getAuthorities()).first().isInstanceOf(SimpleGrantedAuthority.class)
+				.hasToString("ROLE_USER");
+
+	}
+
+	@Test
+	public void requestWhenCustomAuthenticationFailureHandlerThenCalled() throws Exception {
+		this.spring.configLocations(this.xml("SingleClientRegistration-WithCustomAuthenticationHandler")).autowire();
+
+		MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
+		params.add("code", "code123");
+		params.add("state", "state123");
+		this.mvc.perform(get("/login/oauth2/code/google").params(params)).andExpect(status().isIAmATeapot());
 	}
 
 	private String xml(String configName) {
 		return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml";
 	}
+
+	public static class TeapotAuthenticationHandler
+			implements AuthenticationSuccessHandler, AuthenticationFailureHandler {
+
+		@Override
+		public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
+				AuthenticationException exception) {
+			response.setStatus(HttpStatus.I_AM_A_TEAPOT.value());
+		}
+
+		@Override
+		public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
+				Authentication authentication) {
+			response.setStatus(HttpStatus.I_AM_A_TEAPOT.value());
+		}
+	}
+
 }

+ 242 - 0
config/src/test/java/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests.java

@@ -0,0 +1,242 @@
+/*
+ * 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.oauth2.client;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.security.config.test.SpringTestRule;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
+import org.springframework.security.oauth2.core.AuthenticationMethod;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.util.StringUtils;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import okhttp3.mockwebserver.Dispatcher;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+
+/**
+ * Tests for {@link ClientRegistrationsBeanDefinitionParser}.
+ *
+ * @author Ruby Hartono
+ */
+public class ClientRegistrationsBeanDefinitionParserTests {
+	private static final String CONFIG_LOCATION_PREFIX = "classpath:org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests";
+
+	@Rule
+	public final SpringTestRule spring = new SpringTestRule();
+
+	@Autowired
+	private ClientRegistrationRepository clientRegistrationRepository;
+
+	private static final String DEFAULT_RESPONSE =
+			"{\n"
+			+ "    \"authorization_endpoint\": \"https://example.com/o/oauth2/v2/auth\", \n"
+			+ "    \"claims_supported\": [\n"
+			+ "        \"aud\", \n"
+			+ "        \"email\", \n"
+			+ "        \"email_verified\", \n"
+			+ "        \"exp\", \n"
+			+ "        \"family_name\", \n"
+			+ "        \"given_name\", \n"
+			+ "        \"iat\", \n"
+			+ "        \"iss\", \n"
+			+ "        \"locale\", \n"
+			+ "        \"name\", \n"
+			+ "        \"picture\", \n"
+			+ "        \"sub\"\n"
+			+ "    ], \n"
+			+ "    \"code_challenge_methods_supported\": [\n"
+			+ "        \"plain\", \n"
+			+ "        \"S256\"\n"
+			+ "    ], \n"
+			+ "    \"id_token_signing_alg_values_supported\": [\n"
+			+ "        \"RS256\"\n"
+			+ "    ], \n"
+			+ "    \"issuer\": \"http://localhost:49259\", \n"
+			+ "    \"jwks_uri\": \"https://example.com/oauth2/v3/certs\", \n"
+			+ "    \"response_types_supported\": [\n"
+			+ "        \"code\", \n"
+			+ "        \"token\", \n"
+			+ "        \"id_token\", \n"
+			+ "        \"code token\", \n"
+			+ "        \"code id_token\", \n"
+			+ "        \"token id_token\", \n"
+			+ "        \"code token id_token\", \n"
+			+ "        \"none\"\n"
+			+ "    ], \n"
+			+ "    \"revocation_endpoint\": \"https://example.com/o/oauth2/revoke\", \n"
+			+ "    \"scopes_supported\": [\n"
+			+ "        \"openid\", \n"
+			+ "        \"email\", \n"
+			+ "        \"profile\"\n"
+			+ "    ], \n"
+			+ "    \"subject_types_supported\": [\n"
+			+ "        \"public\"\n"
+			+ "    ], \n"
+			+ "    \"grant_types_supported\" : [\"authorization_code\"], \n"
+			+ "    \"token_endpoint\": \"https://example.com/oauth2/v4/token\", \n"
+			+ "    \"token_endpoint_auth_methods_supported\": [\n"
+			+ "        \"client_secret_post\", \n"
+			+ "        \"client_secret_basic\", \n"
+			+ "        \"none\"\n"
+			+ "    ], \n"
+			+ "    \"userinfo_endpoint\": \"https://example.com/oauth2/v3/userinfo\"\n"
+			+ "}";
+
+	@Test
+	public void parseWhenIssuerUriConfiguredThenRequestConfigFromIssuer() throws Exception {
+		MockWebServer server = new MockWebServer();
+		ObjectMapper mapper = new ObjectMapper();
+		server.start(49259);
+		Map<String, Object> response = mapper.readValue(DEFAULT_RESPONSE, new TypeReference<Map<String, Object>>() {
+		});
+		final String responseBody = mapper.writeValueAsString(response);
+
+		final Dispatcher oidcDispatcher = new Dispatcher() {
+			@Override
+			public MockResponse dispatch(RecordedRequest request) {
+				switch (request.getPath()) {
+				case "/issuer1/.well-known/openid-configuration":
+				case "/.well-known/openid-configuration":
+					return new MockResponse().setResponseCode(200).setBody(responseBody)
+							.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+				}
+				return new MockResponse().setResponseCode(404);
+			}
+		};
+
+		final Dispatcher oauthDispatcher = new Dispatcher() {
+			@Override
+			public MockResponse dispatch(RecordedRequest request) {
+				switch (request.getPath()) {
+				case "/.well-known/oauth-authorization-server/issuer1":
+				case "/.well-known/oauth-authorization-server":
+					return new MockResponse().setResponseCode(200).setBody(responseBody)
+							.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+				}
+				return new MockResponse().setResponseCode(404);
+			}
+		};
+
+		server.setDispatcher(oidcDispatcher);
+
+		this.spring.configLocations(this.xml("FromIssuerUri")).autowire();
+
+		assertThat(clientRegistrationRepository).isInstanceOf(InMemoryClientRegistrationRepository.class);
+
+		testIssuerUriResponse(clientRegistrationRepository);
+
+		// test oauth
+		server.setDispatcher(oauthDispatcher);
+		this.spring.configLocations(this.xml("FromIssuerUri")).autowire();
+		testIssuerUriResponse(clientRegistrationRepository);
+
+		server.shutdown();
+		server.close();
+	}
+
+	private void testIssuerUriResponse(ClientRegistrationRepository clientRegistrationRepository) {
+		ClientRegistration googleLogin = clientRegistrationRepository.findByRegistrationId("google-login");
+		assertThat(googleLogin).isNotNull();
+		assertThat(googleLogin.getRegistrationId()).isEqualTo("google-login");
+		assertThat(googleLogin.getClientId()).isEqualTo("google-client-id");
+		assertThat(googleLogin.getClientSecret()).isEqualTo("google-client-secret");
+		assertThat(googleLogin.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
+		assertThat(googleLogin.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
+		assertThat(googleLogin.getRedirectUriTemplate()).isEqualTo("{baseUrl}/login/oauth2/code/{registrationId}");
+		assertThat(googleLogin.getScopes()).isEqualTo(StringUtils.commaDelimitedListToSet("openid,profile,email"));
+		assertThat(googleLogin.getClientName()).isEqualTo("Google");
+
+		ProviderDetails googleProviderDetails = googleLogin.getProviderDetails();
+		assertThat(googleProviderDetails).isNotNull();
+		assertThat(googleProviderDetails.getAuthorizationUri()).isEqualTo("https://example.com/o/oauth2/v2/auth");
+		assertThat(googleProviderDetails.getTokenUri()).isEqualTo("https://example.com/oauth2/v4/token");
+		assertThat(googleProviderDetails.getUserInfoEndpoint().getUri())
+				.isEqualTo("https://example.com/oauth2/v3/userinfo");
+		assertThat(googleProviderDetails.getUserInfoEndpoint().getAuthenticationMethod())
+				.isEqualTo(AuthenticationMethod.HEADER);
+		assertThat(googleProviderDetails.getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo("sub");
+		assertThat(googleProviderDetails.getJwkSetUri()).isEqualTo("https://example.com/oauth2/v3/certs");
+	}
+
+	@Test
+	public void parseWhenMultipleClientsConfiguredThenAvailableInRepository() throws Exception {
+		this.spring.configLocations(this.xml("MultiClientRegistration")).autowire();
+
+		assertThat(clientRegistrationRepository).isInstanceOf(InMemoryClientRegistrationRepository.class);
+
+		ClientRegistration googleLogin = clientRegistrationRepository.findByRegistrationId("google-login");
+		assertThat(googleLogin).isNotNull();
+		assertThat(googleLogin.getRegistrationId()).isEqualTo("google-login");
+		assertThat(googleLogin.getClientId()).isEqualTo("google-client-id");
+		assertThat(googleLogin.getClientSecret()).isEqualTo("google-client-secret");
+		assertThat(googleLogin.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
+		assertThat(googleLogin.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
+		assertThat(googleLogin.getRedirectUriTemplate()).isEqualTo("{baseUrl}/login/oauth2/code/{registrationId}");
+		assertThat(googleLogin.getScopes()).isEqualTo(StringUtils.commaDelimitedListToSet("openid,profile,email"));
+		assertThat(googleLogin.getClientName()).isEqualTo("Google");
+
+		ProviderDetails googleProviderDetails = googleLogin.getProviderDetails();
+		assertThat(googleProviderDetails).isNotNull();
+		assertThat(googleProviderDetails.getAuthorizationUri())
+				.isEqualTo("https://accounts.google.com/o/oauth2/v2/auth");
+		assertThat(googleProviderDetails.getTokenUri()).isEqualTo("https://www.googleapis.com/oauth2/v4/token");
+		assertThat(googleProviderDetails.getUserInfoEndpoint().getUri())
+				.isEqualTo("https://www.googleapis.com/oauth2/v3/userinfo");
+		assertThat(googleProviderDetails.getUserInfoEndpoint().getAuthenticationMethod())
+				.isEqualTo(AuthenticationMethod.HEADER);
+		assertThat(googleProviderDetails.getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo("sub");
+		assertThat(googleProviderDetails.getJwkSetUri()).isEqualTo("https://www.googleapis.com/oauth2/v3/certs");
+
+		ClientRegistration githubLogin = clientRegistrationRepository.findByRegistrationId("github-login");
+		assertThat(githubLogin).isNotNull();
+		assertThat(githubLogin.getRegistrationId()).isEqualTo("github-login");
+		assertThat(githubLogin.getClientId()).isEqualTo("github-client-id");
+		assertThat(githubLogin.getClientSecret()).isEqualTo("github-client-secret");
+		assertThat(githubLogin.getClientAuthenticationMethod()).isEqualTo(ClientAuthenticationMethod.BASIC);
+		assertThat(githubLogin.getAuthorizationGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
+		assertThat(githubLogin.getRedirectUriTemplate()).isEqualTo("{baseUrl}/login/oauth2/code/{registrationId}");
+		assertThat(googleLogin.getScopes()).isEqualTo(StringUtils.commaDelimitedListToSet("openid,profile,email"));
+		assertThat(githubLogin.getClientName()).isEqualTo("Github");
+
+		ProviderDetails githubProviderDetails = githubLogin.getProviderDetails();
+		assertThat(githubProviderDetails).isNotNull();
+		assertThat(githubProviderDetails.getAuthorizationUri()).isEqualTo("https://github.com/login/oauth/authorize");
+		assertThat(githubProviderDetails.getTokenUri()).isEqualTo("https://github.com/login/oauth/access_token");
+		assertThat(githubProviderDetails.getUserInfoEndpoint().getUri()).isEqualTo("https://api.github.com/user");
+		assertThat(githubProviderDetails.getUserInfoEndpoint().getAuthenticationMethod())
+				.isEqualTo(AuthenticationMethod.HEADER);
+		assertThat(githubProviderDetails.getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo("id");
+	}
+
+	private String xml(String configName) {
+		return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml";
+	}
+}

+ 1 - 1
config/src/test/java/org/springframework/security/config/util/InMemoryXmlApplicationContext.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2009-2019 the original author or authors.
+ * Copyright 2009-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.

+ 34 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-MultiClientRegistration.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login/>
+	</http>
+
+	<b:import resource="../oauth2/client/google-github-registration.xml"/>
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 38 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationFailureHandler.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login authentication-failure-handler-ref="authenticationFailureHandler"/>
+	</http>
+
+	<b:bean name="authenticationFailureHandler" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.web.authentication.AuthenticationFailureHandler"/>
+	</b:bean>
+
+	<b:import resource="../oauth2/client/google-registration.xml"/>
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 53 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthenticationHandler.xml

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login access-token-response-client-ref="accessTokenResponseClient"
+						user-service-ref="oauth2UserService"
+						authorization-request-repository-ref="authorizationRequestRepository"
+						authentication-success-handler-ref="authenticationSuccessHandler"
+						authentication-failure-handler-ref="testHandler"/>
+	</http>
+
+	<b:bean name="accessTokenResponseClient" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient"/>
+	</b:bean>
+	<b:bean name="oauth2UserService" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.userinfo.OAuth2UserService"/>
+	</b:bean>
+	<b:bean name="authorizationRequestRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.web.AuthorizationRequestRepository"/>
+	</b:bean>
+	<b:bean name="authenticationSuccessHandler" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.web.authentication.AuthenticationSuccessHandler"/>
+	</b:bean>
+
+	<b:bean id="testHandler" class="org.springframework.security.config.http.OAuth2LoginBeanDefinitionParserTests.TeapotAuthenticationHandler"/>
+
+	<b:import resource="../oauth2/client/google-registration.xml"/>
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 38 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomAuthorizationRequestResolver.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login authorization-request-resolver-ref="oauth2AuthorizationRequestResolver"/>
+	</http>
+
+	<b:bean name="oauth2AuthorizationRequestResolver" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver"/>
+	</b:bean>
+
+	<b:import resource="../oauth2/client/google-registration.xml"/>
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 58 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomGrantedAuthorities.xml

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login access-token-response-client-ref="accessTokenResponseClient"
+						user-service-ref="oauth2UserService"
+						user-authorities-mapper-ref="grantedAuthoritiesMapper"
+						jwt-decoder-factory-ref="jwtDecoderFactory"
+						authorization-request-repository-ref="authorizationRequestRepository"
+						authentication-success-handler-ref="authenticationSuccessHandler"/>
+	</http>
+	
+	<b:bean name="accessTokenResponseClient" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient"/>
+	</b:bean>
+	<b:bean name="oauth2UserService" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.userinfo.OAuth2UserService"/>
+	</b:bean>
+	<b:bean name="grantedAuthoritiesMapper" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper"/>
+	</b:bean>
+	<b:bean name="authorizationRequestRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.web.AuthorizationRequestRepository"/>
+	</b:bean>
+	<b:bean name="authenticationSuccessHandler" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.web.authentication.AuthenticationSuccessHandler"/>
+	</b:bean>
+	<b:bean name="jwtDecoderFactory" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.jwt.JwtDecoderFactory"/>
+	</b:bean>
+
+	<b:import resource="../oauth2/client/google-registration.xml"/>
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 34 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomLoginPage.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login  login-page="/custom-login"/>
+	</http>
+
+	<b:import resource="../oauth2/client/google-registration.xml"/>
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 51 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithCustomLoginProcessingUrl.xml

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login access-token-response-client-ref="accessTokenResponseClient"
+						user-service-ref="oauth2UserService"
+						login-processing-url="/login/oauth2/*"
+						authorization-request-repository-ref="authorizationRequestRepository"
+						authentication-success-handler-ref="authenticationSuccessHandler" />
+	</http>
+	
+	<b:bean name="accessTokenResponseClient" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient"/>
+	</b:bean>
+	<b:bean name="oauth2UserService" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.userinfo.OAuth2UserService"/>
+	</b:bean>
+	<b:bean name="authorizationRequestRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.web.AuthorizationRequestRepository"/>
+	</b:bean>
+	<b:bean name="authenticationSuccessHandler" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.web.authentication.AuthenticationSuccessHandler"/>
+	</b:bean>
+
+	<b:import resource="../oauth2/client/google-registration.xml"/>
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 35 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithFormLogin.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login/>
+		<form-login/>
+	</http>
+
+	<b:import resource="../oauth2/client/google-registration.xml"/>
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 54 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactory.xml

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login access-token-response-client-ref="accessTokenResponseClient"
+						jwt-decoder-factory-ref="jwtDecoderFactory"
+						user-authorities-mapper-ref="grantedAuthoritiesMapper"
+						authorization-request-repository-ref="authorizationRequestRepository"
+						authentication-success-handler-ref="authenticationSuccessHandler"/>
+	</http>
+	
+	<b:bean name="accessTokenResponseClient" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient"/>
+	</b:bean>
+	<b:bean name="jwtDecoderFactory" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.jwt.JwtDecoderFactory"/>
+	</b:bean>
+	<b:bean name="grantedAuthoritiesMapper" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper"/>
+	</b:bean>
+	<b:bean name="authorizationRequestRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.web.AuthorizationRequestRepository"/>
+	</b:bean>
+	<b:bean name="authenticationSuccessHandler" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.web.authentication.AuthenticationSuccessHandler"/>
+	</b:bean>
+
+	<b:import resource="../oauth2/client/google-registration.xml"/>
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 51 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithJwtDecoderFactoryAndDefaultSuccessHandler.xml

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login access-token-response-client-ref="accessTokenResponseClient"
+						jwt-decoder-factory-ref="jwtDecoderFactory"
+						user-authorities-mapper-ref="grantedAuthoritiesMapper"
+						authorization-request-repository-ref="authorizationRequestRepository"
+						/>
+	</http>
+	
+	<b:bean name="accessTokenResponseClient" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient"/>
+	</b:bean>
+	<b:bean name="jwtDecoderFactory" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.jwt.JwtDecoderFactory"/>
+	</b:bean>
+	<b:bean name="grantedAuthoritiesMapper" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper"/>
+	</b:bean>
+	<b:bean name="authorizationRequestRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.web.AuthorizationRequestRepository"/>
+	</b:bean>
+
+	<b:import resource="../oauth2/client/google-registration.xml"/>
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 57 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithTestConfiguration.xml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login access-token-response-client-ref="accessTokenResponseClient"
+						user-service-ref="oauth2UserService"
+						oidc-user-service-ref="oauth2UserService"
+						authorization-request-repository-ref="authorizationRequestRepository"
+						authentication-success-handler-ref="authenticationSuccessHandler"/>
+	</http>
+	
+	<b:bean name="accessTokenResponseClient" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient"/>
+	</b:bean>
+	<b:bean name="oauth2UserService" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.userinfo.OAuth2UserService"/>
+	</b:bean>
+	<b:bean name="jwtDecoderFactory" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.jwt.JwtDecoderFactory"/>
+	</b:bean>
+	<b:bean name="authenticationSuccessListener" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.context.ApplicationListener"/>
+	</b:bean>
+	<b:bean name="authorizationRequestRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.web.AuthorizationRequestRepository"/>
+	</b:bean>
+	<b:bean name="authenticationSuccessHandler" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.web.authentication.AuthenticationSuccessHandler"/>
+	</b:bean>
+
+	<b:import resource="../oauth2/client/google-registration.xml"/>
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 52 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration-WithinSameFile.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login/>
+	</http>
+
+	<client-registrations>
+		<client-registration registration-id="google-login"
+							 client-id="google-client-id"
+							 client-secret="google-client-secret"
+							 client-authentication-method="basic"
+							 authorization-grant-type="authorization_code"
+							 redirect-uri="{baseUrl}/login/oauth2/code/{registrationId}"
+							 scope="openid,profile,email"
+							 client-name="Google"
+							 provider-id="google"/>
+		<provider provider-id="google"
+				  authorization-uri="https://accounts.google.com/o/oauth2/v2/auth"
+				  token-uri="https://www.googleapis.com/oauth2/v4/token"
+				  userinfo-uri="https://www.googleapis.com/oauth2/v3/userinfo"
+				  userinfo-authentication-method="header"
+				  username-attribute-name="sub"
+				  jwkset-uri="https://www.googleapis.com/oauth2/v3/certs"/>
+	</client-registrations>
+
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 3 - 27
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-SingleClientRegistration.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!--
-  ~ Copyright 2002-2019 the original author or authors.
+  ~ 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.
@@ -25,34 +25,10 @@
 			https://www.springframework.org/schema/beans/spring-beans.xsd">
 
 	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
 		<oauth2-login/>
 	</http>
 
-	<client-registrations>
-		<client-registration registration-id="google-login"
-							 client-id="google-client-id"
-							 client-secret="google-client-secret"
-							 client-authentication-method="basic"
-							 authorization-grant-type="authorization_code"
-							 redirect-uri="{baseUrl}/login/oauth2/code/{registrationId}"
-							 scope="openid,profile,email"
-							 client-name="Google"
-							 provider-id="google"/>
-		<provider provider-id="google"
-				  authorization-uri="https://accounts.google.com/o/oauth2/v2/auth"
-				  token-uri="https://www.googleapis.com/oauth2/v4/token"
-				  userinfo-uri="https://www.googleapis.com/oauth2/v3/userinfo"
-				  userinfo-authentication-method="header"
-				  username-attribute-name="sub"
-				  jwkset-uri="https://www.googleapis.com/oauth2/v3/certs"/>
-	</client-registrations>
-
-	<!--
-		NOTE:
-		This import should be removed after `OAuth2LoginBeanDefinitionParser` is implemented.
-		Without this import the `AbstractXmlApplicationContext` will fail to load.
-		This is simply a placeholder to force a successful load.
-	-->
+	<b:import resource="../oauth2/client/google-registration.xml"/>
 	<b:import resource="userservice.xml"/>
-
 </b:beans>

+ 49 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizationRequestRepository.xml

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login client-registration-repository-ref="clientRegistrationRepository"
+						access-token-response-client-ref="accessTokenResponseClient"
+						user-service-ref="oauth2UserService"
+						authorization-request-repository-ref="authorizationRequestRepository"/>
+	</http>
+
+	<b:bean name="accessTokenResponseClient" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient"/>
+	</b:bean>
+	<b:bean name="oauth2UserService" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.userinfo.OAuth2UserService"/>
+	</b:bean>
+	<b:bean name="clientRegistrationRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.registration.ClientRegistrationRepository"/>
+	</b:bean>
+	<b:bean name="authorizationRequestRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.web.AuthorizationRequestRepository"/>
+	</b:bean>
+
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 53 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientRepository.xml

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login client-registration-repository-ref="clientRegistrationRepository"
+						authorized-client-repository-ref="authorizedClientRepository"
+						access-token-response-client-ref="accessTokenResponseClient"
+						user-service-ref="oauth2UserService"
+						authorization-request-repository-ref="authorizationRequestRepository"/>
+	</http>
+
+	<b:bean name="accessTokenResponseClient" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient"/>
+	</b:bean>
+	<b:bean name="oauth2UserService" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.userinfo.OAuth2UserService"/>
+	</b:bean>
+	<b:bean name="authorizationRequestRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.web.AuthorizationRequestRepository"/>
+	</b:bean>
+	<b:bean name="clientRegistrationRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.registration.ClientRegistrationRepository"/>
+	</b:bean>
+	<b:bean name="authorizedClientRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository"/>
+	</b:bean>
+
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 53 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomAuthorizedClientService.xml

@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login client-registration-repository-ref="clientRegistrationRepository"
+						access-token-response-client-ref="accessTokenResponseClient"
+						user-service-ref="oauth2UserService"
+						authorization-request-repository-ref="authorizationRequestRepository"
+						authorized-client-service-ref="authorizedClientService"/>
+	</http>
+
+	<b:bean name="accessTokenResponseClient" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient"/>
+	</b:bean>
+	<b:bean name="oauth2UserService" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.userinfo.OAuth2UserService"/>
+	</b:bean>
+	<b:bean name="authorizationRequestRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.web.AuthorizationRequestRepository"/>
+	</b:bean>
+	<b:bean name="clientRegistrationRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.registration.ClientRegistrationRepository"/>
+	</b:bean>
+	<b:bean name="authorizedClientService" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.OAuth2AuthorizedClientService"/>
+	</b:bean>
+
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 50 - 0
config/src/test/resources/org/springframework/security/config/http/OAuth2LoginBeanDefinitionParserTests-WithCustomClientRegistrationRepository.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<http auto-config="true">
+		<intercept-url pattern="/**" access="authenticated"/>
+		<oauth2-login client-registration-repository-ref="clientRegistrationRepository"
+						access-token-response-client-ref="accessTokenResponseClient"
+						user-service-ref="oauth2UserService"
+						authorization-request-repository-ref="authorizationRequestRepository"/>
+	</http>
+
+	<b:bean name="accessTokenResponseClient" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient"/>
+	</b:bean>
+	<b:bean name="oauth2UserService" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.userinfo.OAuth2UserService"/>
+	</b:bean>
+	<b:bean name="authorizationRequestRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.web.AuthorizationRequestRepository"/>
+	</b:bean>
+	<b:bean name="clientRegistrationRepository" class="org.mockito.Mockito" factory-method="mock">
+			<b:constructor-arg value="org.springframework.security.oauth2.client.registration.ClientRegistrationRepository"/>
+	</b:bean>
+	
+
+	<b:import resource="userservice.xml"/>
+</b:beans>

+ 39 - 0
config/src/test/resources/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests-FromIssuerUri.xml

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+	<client-registrations>
+		<client-registration registration-id="google-login"
+							 client-id="google-client-id"
+							 client-secret="google-client-secret"
+							 client-authentication-method="basic"
+							 authorization-grant-type="authorization_code"
+							 redirect-uri="{baseUrl}/login/oauth2/code/{registrationId}"
+							 scope="openid,profile,email"
+							 client-name="Google"
+							 provider-id="google"/>
+		<provider 	provider-id="google"
+					issuer-uri="http://localhost:49259"/>
+	</client-registrations>
+</b:beans>

+ 27 - 0
config/src/test/resources/org/springframework/security/config/oauth2/client/ClientRegistrationsBeanDefinitionParserTests-MultiClientRegistration.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+	<b:import resource="google-github-registration.xml"/>
+</b:beans>

+ 59 - 0
config/src/test/resources/org/springframework/security/config/oauth2/client/google-github-registration.xml

@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+	<client-registrations>
+		<client-registration registration-id="google-login"
+							 client-id="google-client-id"
+							 client-secret="google-client-secret"
+							 client-authentication-method="basic"
+							 authorization-grant-type="authorization_code"
+							 redirect-uri="{baseUrl}/login/oauth2/code/{registrationId}"
+							 scope="openid,profile,email"
+							 client-name="Google"
+							 provider-id="google"/>
+		<client-registration registration-id="github-login"
+							 client-id="github-client-id"
+							 client-secret="github-client-secret"
+							 client-authentication-method="basic"
+							 authorization-grant-type="authorization_code"
+							 redirect-uri="{baseUrl}/login/oauth2/code/{registrationId}"
+							 scope="openid,profile,email"
+							 client-name="Github"
+							 provider-id="github"/>
+		<provider provider-id="google"
+				  authorization-uri="https://accounts.google.com/o/oauth2/v2/auth"
+				  token-uri="https://www.googleapis.com/oauth2/v4/token"
+				  userinfo-uri="https://www.googleapis.com/oauth2/v3/userinfo"
+				  userinfo-authentication-method="header"
+				  username-attribute-name="sub"
+				  jwkset-uri="https://www.googleapis.com/oauth2/v3/certs"/>
+		<provider provider-id="github"
+				  authorization-uri="https://github.com/login/oauth/authorize"
+				  token-uri="https://github.com/login/oauth/access_token"
+				  userinfo-uri="https://api.github.com/user"
+				  userinfo-authentication-method="header"
+				  username-attribute-name="id"/>
+	</client-registrations>
+</b:beans>

+ 44 - 0
config/src/test/resources/org/springframework/security/config/oauth2/client/google-registration.xml

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+
+<b:beans xmlns:b="http://www.springframework.org/schema/beans"
+		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+		xmlns="http://www.springframework.org/schema/security"
+		xsi:schemaLocation="
+			http://www.springframework.org/schema/security
+			https://www.springframework.org/schema/security/spring-security.xsd
+			http://www.springframework.org/schema/beans
+			https://www.springframework.org/schema/beans/spring-beans.xsd">
+	<client-registrations>
+		<client-registration registration-id="google-login"
+							 client-id="google-client-id"
+							 client-secret="google-client-secret"
+							 client-authentication-method="basic"
+							 authorization-grant-type="authorization_code"
+							 redirect-uri="{baseUrl}/login/oauth2/code/{registrationId}"
+							 scope="openid,profile,email"
+							 client-name="Google"
+							 provider-id="google"/>
+		<provider provider-id="google"
+				  authorization-uri="https://accounts.google.com/o/oauth2/v2/auth"
+				  token-uri="https://www.googleapis.com/oauth2/v4/token"
+				  userinfo-uri="https://www.googleapis.com/oauth2/v3/userinfo"
+				  userinfo-authentication-method="header"
+				  username-attribute-name="sub"
+				  jwkset-uri="https://www.googleapis.com/oauth2/v3/certs"/>
+	</client-registrations>
+</b:beans>

+ 73 - 0
docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc

@@ -891,6 +891,79 @@ The <<oauth2login,OAuth 2.0 Login>> feature configures authentication support us
 
 * <<nsa-http,http>>
 
+[[nsa-oauth2-login-attributes]]
+===== <oauth2-login> Attributes
+
+
+[[nsa-oauth2-login-client-registration-repository-ref]]
+* **client-registration-repository-ref**
+Reference to `ClientRegistrationRepository`.
+
+
+[[nsa-oauth2-login-authorized-client-repository-ref]]
+* **authorized-client-repository-ref**
+Reference to `OAuth2AuthorizedClientRepository`.
+
+
+[[nsa-oauth2-login-authorized-client-service-ref]]
+* **authorized-client-service-ref**
+Reference to `OAuth2AuthorizedClientService`.
+
+
+[[nsa-oauth2-login-authorization-request-repository-ref]]
+* **authorization-request-repository-ref**
+Reference to `AuthorizationRequestRepository`.
+
+
+[[nsa-oauth2-login-authorization-request-resolver-ref]]
+* **authorization-request-resolver-ref**
+Reference to `OAuth2AuthorizationRequestResolver`.
+
+
+[[nsa-oauth2-login-access-token-response-client-ref]]
+* **access-token-response-client-ref**
+Reference to `OAuth2AccessTokenResponseClient`.
+
+
+[[nsa-oauth2-login-user-authorities-mapper-ref]]
+* **user-authorities-mapper-ref**
+Reference to `GrantedAuthoritiesMapper`.
+
+
+[[nsa-oauth2-login-user-service-ref]]
+* **user-service-ref**
+Reference to `OAuth2UserService`.
+
+
+[[nsa-oauth2-login-oidc-user-service-ref]]
+* **oidc-user-service-ref**
+Reference to `OidcUserService`.
+
+
+[[nsa-oauth2-login-login-processing-url]]
+* **login-processing-url**
+Specifies the URL to validate the credentials.
+
+
+[[nsa-oauth2-login-login-page]]
+* **login-page**
+Specifies the URL to send users to if login is required
+
+
+[[nsa-oauth2-login-authentication-success-handler-ref]]
+* **authentication-success-handler-ref**
+Specifies authentication success handler
+
+
+[[nsa-oauth2-login-authentication-failure-handler-ref]]
+* **authentication-failure-handler-ref**
+Specifies authentication failure handler
+
+
+[[nsa-oauth2-login-jwt-decoder-factory-ref]]
+* **jwt-decoder-factory-ref**
+Specifies JWT decoder factory for OidcAuthorizationCodeAuthenticationProvider
+
 
 [[nsa-client-registrations]]
 ==== <client-registrations>