瀏覽代碼

Add support for authorization_code grant

Fixes gh-4928
Joe Grandja 7 年之前
父節點
當前提交
982fc360b2
共有 33 個文件被更改,包括 2531 次插入187 次删除
  1. 4 0
      config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java
  2. 22 10
      config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java
  3. 52 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java
  4. 1 15
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/ImplicitGrantConfigurer.java
  5. 295 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java
  6. 74 0
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java
  7. 5 41
      config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java
  8. 5 5
      config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java
  9. 171 0
      config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java
  10. 5 5
      config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java
  11. 60 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientAuthorizationRequiredException.java
  12. 44 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2ClientException.java
  13. 88 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java
  14. 114 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java
  15. 54 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationExchangeValidator.java
  16. 2 25
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java
  17. 209 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java
  18. 97 6
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java
  19. 72 0
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java
  20. 3 38
      oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java
  21. 141 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java
  22. 115 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationTokenTests.java
  23. 304 0
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java
  24. 119 12
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java
  25. 13 30
      oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java
  26. 64 0
      samples/boot/oauth2/authcodegrant/README.adoc
  27. 16 0
      samples/boot/oauth2/authcodegrant/spring-security-samples-boot-oauth2-authcodegrant.gradle
  28. 165 0
      samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java
  29. 30 0
      samples/boot/oauth2/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java
  30. 55 0
      samples/boot/oauth2/authcodegrant/src/main/java/sample/config/SecurityConfig.java
  31. 81 0
      samples/boot/oauth2/authcodegrant/src/main/java/sample/web/MainController.java
  32. 23 0
      samples/boot/oauth2/authcodegrant/src/main/resources/application.yml
  33. 28 0
      samples/boot/oauth2/authcodegrant/src/main/resources/templates/github-repos.html

+ 4 - 0
config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterComparator.java

@@ -117,6 +117,10 @@ final class FilterComparator implements Comparator<Filter>, Serializable {
 		order += STEP;
 		order += STEP;
 		put(AnonymousAuthenticationFilter.class, order);
 		put(AnonymousAuthenticationFilter.class, order);
 		order += STEP;
 		order += STEP;
+		filterToOrder.put(
+			"org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter",
+			order);
+		order += STEP;
 		put(SessionManagementFilter.class, order);
 		put(SessionManagementFilter.class, order);
 		order += STEP;
 		order += STEP;
 		put(ExceptionTranslationFilter.class, order);
 		put(ExceptionTranslationFilter.class, order);

+ 22 - 10
config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2016 the original author or authors.
+ * Copyright 2002-2018 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -15,14 +15,6 @@
  */
  */
 package org.springframework.security.config.annotation.web.builders;
 package org.springframework.security.config.annotation.web.builders;
 
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import javax.servlet.Filter;
-import javax.servlet.http.HttpServletRequest;
-
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContext;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpMethod;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationManager;
@@ -56,12 +48,13 @@ import org.springframework.security.config.annotation.web.configurers.SecurityCo
 import org.springframework.security.config.annotation.web.configurers.ServletApiConfigurer;
 import org.springframework.security.config.annotation.web.configurers.ServletApiConfigurer;
 import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
 import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
 import org.springframework.security.config.annotation.web.configurers.X509Configurer;
 import org.springframework.security.config.annotation.web.configurers.X509Configurer;
+import org.springframework.security.config.annotation.web.configurers.oauth2.OAuth2Configurer;
+import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
 import org.springframework.security.config.annotation.web.configurers.openid.OpenIDLoginConfigurer;
 import org.springframework.security.config.annotation.web.configurers.openid.OpenIDLoginConfigurer;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UserDetailsService;
-import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
 import org.springframework.security.web.DefaultSecurityFilterChain;
 import org.springframework.security.web.DefaultSecurityFilterChain;
 import org.springframework.security.web.PortMapper;
 import org.springframework.security.web.PortMapper;
 import org.springframework.security.web.PortMapperImpl;
 import org.springframework.security.web.PortMapperImpl;
@@ -79,6 +72,13 @@ import org.springframework.web.cors.CorsConfiguration;
 import org.springframework.web.filter.CorsFilter;
 import org.springframework.web.filter.CorsFilter;
 import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
 import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
 
 
+import javax.servlet.Filter;
+import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
 /**
 /**
  * A {@link HttpSecurity} is similar to Spring Security's XML &lt;http&gt; element in the
  * A {@link HttpSecurity} is similar to Spring Security's XML &lt;http&gt; element in the
  * namespace configuration. It allows configuring web based security for specific http
  * namespace configuration. It allows configuring web based security for specific http
@@ -991,6 +991,18 @@ public final class HttpSecurity extends
 		return getOrApply(new OAuth2LoginConfigurer<>());
 		return getOrApply(new OAuth2LoginConfigurer<>());
 	}
 	}
 
 
+	/**
+	 * Configures support for the <a target="_blank" href="https://tools.ietf.org/html/rfc6749">OAuth 2.0 Authorization Framework</a>.
+	 *
+	 * @author Joe Grandja
+	 * @since 5.1
+	 * @return the {@link OAuth2Configurer} for further customizations
+	 * @throws Exception
+	 */
+	public OAuth2Configurer oauth2() throws Exception {
+		return getOrApply(new OAuth2Configurer());
+	}
+
 	/**
 	/**
 	 * Configures channel security. In order for this configuration to be useful at least
 	 * Configures channel security. In order for this configuration to be useful at least
 	 * one mapping to a required channel must be provided.
 	 * one mapping to a required channel must be provided.

+ 52 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/OAuth2Configurer.java

@@ -0,0 +1,52 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.annotation.web.configurers.oauth2;
+
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer;
+
+/**
+ * An {@link AbstractHttpConfigurer} that provides support for the
+ * <a target="_blank" href="https://tools.ietf.org/html/rfc6749">OAuth 2.0 Authorization Framework</a>.
+ *
+ * @author Joe Grandja
+ * @since 5.1
+ * @see HttpSecurity#oauth2()
+ * @see OAuth2ClientConfigurer
+ * @see AbstractHttpConfigurer
+ */
+public final class OAuth2Configurer extends AbstractHttpConfigurer<OAuth2Configurer, HttpSecurity> {
+
+	/**
+	 * Returns the {@link OAuth2ClientConfigurer} for configuring OAuth 2.0 Client support.
+	 *
+	 * @return the {@link OAuth2ClientConfigurer}
+	 * @throws Exception
+	 */
+	public OAuth2ClientConfigurer<HttpSecurity> client() throws Exception {
+		return this.getOrApply(new OAuth2ClientConfigurer<>());
+	}
+
+	@SuppressWarnings("unchecked")
+	private <C extends AbstractHttpConfigurer<C, HttpSecurity>> C getOrApply(C configurer) throws Exception {
+		C existingConfigurer = (C) this.getBuilder().getConfigurer(configurer.getClass());
+		if (existingConfigurer != null) {
+			return existingConfigurer;
+		}
+		return this.getBuilder().apply(configurer);
+	}
+}

+ 1 - 15
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/ImplicitGrantConfigurer.java

@@ -15,7 +15,6 @@
  */
  */
 package org.springframework.security.config.annotation.web.configurers.oauth2.client;
 package org.springframework.security.config.annotation.web.configurers.oauth2.client;
 
 
-import org.springframework.context.ApplicationContext;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
 import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
 import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
 import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
@@ -86,7 +85,7 @@ public final class ImplicitGrantConfigurer<B extends HttpSecurityBuilder<B>> ext
 	@Override
 	@Override
 	public void configure(B http) throws Exception {
 	public void configure(B http) throws Exception {
 		OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(
 		OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(
-			this.getClientRegistrationRepository(), this.getAuthorizationRequestBaseUri());
+			OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), this.getAuthorizationRequestBaseUri());
 		http.addFilter(this.postProcess(authorizationRequestFilter));
 		http.addFilter(this.postProcess(authorizationRequestFilter));
 	}
 	}
 
 
@@ -95,17 +94,4 @@ public final class ImplicitGrantConfigurer<B extends HttpSecurityBuilder<B>> ext
 			this.authorizationRequestBaseUri :
 			this.authorizationRequestBaseUri :
 			OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
 			OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
 	}
 	}
-
-	private ClientRegistrationRepository getClientRegistrationRepository() {
-		ClientRegistrationRepository clientRegistrationRepository = this.getBuilder().getSharedObject(ClientRegistrationRepository.class);
-		if (clientRegistrationRepository == null) {
-			clientRegistrationRepository = this.getClientRegistrationRepositoryBean();
-			this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
-		}
-		return clientRegistrationRepository;
-	}
-
-	private ClientRegistrationRepository getClientRegistrationRepositoryBean() {
-		return this.getBuilder().getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class);
-	}
 }
 }

+ 295 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java

@@ -0,0 +1,295 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.annotation.web.configurers.oauth2.client;
+
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
+import org.springframework.security.oauth2.client.endpoint.NimbusAuthorizationCodeTokenResponseClient;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.util.Assert;
+
+/**
+ * An {@link AbstractHttpConfigurer} for OAuth 2.0 Client support.
+ *
+ * <p>
+ * The following configuration options are available:
+ *
+ * <ul>
+ * <li>{@link #authorizationCodeGrant()} - enables the OAuth 2.0 Authorization Code Grant</li>
+ * </ul>
+ *
+ * <p>
+ * Defaults are provided for all configuration options with the only required configuration
+ * being {@link #clientRegistrationRepository(ClientRegistrationRepository)}.
+ * Alternatively, a {@link ClientRegistrationRepository} {@code @Bean} may be registered instead.
+ *
+ * <h2>Security Filters</h2>
+ *
+ * The following {@code Filter}'s are populated when {@link #authorizationCodeGrant()} is configured:
+ *
+ * <ul>
+ * <li>{@link OAuth2AuthorizationRequestRedirectFilter}</li>
+ * <li>{@link OAuth2AuthorizationCodeGrantFilter}</li>
+ * </ul>
+ *
+ * <h2>Shared Objects Created</h2>
+ *
+ * The following shared objects are populated:
+ *
+ * <ul>
+ * <li>{@link ClientRegistrationRepository} (required)</li>
+ * <li>{@link OAuth2AuthorizedClientService} (optional)</li>
+ * </ul>
+ *
+ * <h2>Shared Objects Used</h2>
+ *
+ * The following shared objects are used:
+ *
+ * <ul>
+ * <li>{@link ClientRegistrationRepository}</li>
+ * <li>{@link OAuth2AuthorizedClientService}</li>
+ * </ul>
+ *
+ * @author Joe Grandja
+ * @since 5.1
+ * @see OAuth2AuthorizationRequestRedirectFilter
+ * @see OAuth2AuthorizationCodeGrantFilter
+ * @see ClientRegistrationRepository
+ * @see OAuth2AuthorizedClientService
+ * @see AbstractHttpConfigurer
+ */
+public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>> extends
+	AbstractHttpConfigurer<OAuth2ClientConfigurer<B>, B> {
+
+	private AuthorizationCodeGrantConfigurer authorizationCodeGrantConfigurer;
+
+	/**
+	 * Sets the repository of client registrations.
+	 *
+	 * @param clientRegistrationRepository the repository of client registrations
+	 * @return the {@link OAuth2ClientConfigurer} for further configuration
+	 */
+	public OAuth2ClientConfigurer<B> clientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) {
+		Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
+		this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
+		return this;
+	}
+
+	/**
+	 * Sets the service for authorized client(s).
+	 *
+	 * @param authorizedClientService the authorized client service
+	 * @return the {@link OAuth2ClientConfigurer} for further configuration
+	 */
+	public OAuth2ClientConfigurer<B> authorizedClientService(OAuth2AuthorizedClientService authorizedClientService) {
+		Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
+		this.getBuilder().setSharedObject(OAuth2AuthorizedClientService.class, authorizedClientService);
+		return this;
+	}
+
+	/**
+	 * Returns the {@link AuthorizationCodeGrantConfigurer} for configuring the OAuth 2.0 Authorization Code Grant.
+	 *
+	 * @return the {@link AuthorizationCodeGrantConfigurer}
+	 */
+	public AuthorizationCodeGrantConfigurer authorizationCodeGrant() {
+		if (this.authorizationCodeGrantConfigurer == null) {
+			this.authorizationCodeGrantConfigurer = new AuthorizationCodeGrantConfigurer();
+		}
+		return this.authorizationCodeGrantConfigurer;
+	}
+
+	/**
+	 * Configuration options for the OAuth 2.0 Authorization Code Grant.
+	 */
+	public class AuthorizationCodeGrantConfigurer {
+		private final AuthorizationEndpointConfig authorizationEndpointConfig = new AuthorizationEndpointConfig();
+		private final TokenEndpointConfig tokenEndpointConfig = new TokenEndpointConfig();
+
+		private AuthorizationCodeGrantConfigurer() {
+		}
+
+		/**
+		 * Returns the {@link AuthorizationEndpointConfig} for configuring the Authorization Server's Authorization Endpoint.
+		 *
+		 * @return the {@link AuthorizationEndpointConfig}
+		 */
+		public AuthorizationEndpointConfig authorizationEndpoint() {
+			return this.authorizationEndpointConfig;
+		}
+
+		/**
+		 * Configuration options for the Authorization Server's Authorization Endpoint.
+		 */
+		public class AuthorizationEndpointConfig {
+			private String authorizationRequestBaseUri;
+			private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;
+
+			private AuthorizationEndpointConfig() {
+			}
+
+			/**
+			 * Sets the base {@code URI} used for authorization requests.
+			 *
+			 * @param authorizationRequestBaseUri the base {@code URI} used for authorization requests
+			 * @return the {@link AuthorizationEndpointConfig} for further configuration
+			 */
+			public AuthorizationEndpointConfig baseUri(String authorizationRequestBaseUri) {
+				Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty");
+				this.authorizationRequestBaseUri = authorizationRequestBaseUri;
+				return this;
+			}
+
+			/**
+			 * Sets the repository used for storing {@link OAuth2AuthorizationRequest}'s.
+			 *
+			 * @param authorizationRequestRepository the repository used for storing {@link OAuth2AuthorizationRequest}'s
+			 * @return the {@link AuthorizationEndpointConfig} for further configuration
+			 */
+			public AuthorizationEndpointConfig authorizationRequestRepository(
+				AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository) {
+
+				Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null");
+				this.authorizationRequestRepository = authorizationRequestRepository;
+				return this;
+			}
+
+			/**
+			 * Returns the {@link AuthorizationCodeGrantConfigurer} for further configuration.
+			 *
+			 * @return the {@link AuthorizationCodeGrantConfigurer}
+			 */
+			public AuthorizationCodeGrantConfigurer and() {
+				return AuthorizationCodeGrantConfigurer.this;
+			}
+		}
+
+		/**
+		 * Returns the {@link TokenEndpointConfig} for configuring the Authorization Server's Token Endpoint.
+		 *
+		 * @return the {@link TokenEndpointConfig}
+		 */
+		public TokenEndpointConfig tokenEndpoint() {
+			return this.tokenEndpointConfig;
+		}
+
+		/**
+		 * Configuration options for the Authorization Server's Token Endpoint.
+		 */
+		public class TokenEndpointConfig {
+			private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
+
+			private TokenEndpointConfig() {
+			}
+
+			/**
+			 * Sets the client used for requesting the access token credential from the Token Endpoint.
+			 *
+			 * @param accessTokenResponseClient the client used for requesting the access token credential from the Token Endpoint
+			 * @return the {@link TokenEndpointConfig} for further configuration
+			 */
+			public TokenEndpointConfig accessTokenResponseClient(
+				OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient) {
+
+				Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
+				this.accessTokenResponseClient = accessTokenResponseClient;
+				return this;
+			}
+
+			/**
+			 * Returns the {@link AuthorizationCodeGrantConfigurer} for further configuration.
+			 *
+			 * @return the {@link AuthorizationCodeGrantConfigurer}
+			 */
+			public AuthorizationCodeGrantConfigurer and() {
+				return AuthorizationCodeGrantConfigurer.this;
+			}
+		}
+
+		/**
+		 * Returns the {@link OAuth2ClientConfigurer} for further configuration.
+		 *
+		 * @return the {@link OAuth2ClientConfigurer}
+		 */
+		public OAuth2ClientConfigurer<B> and() {
+			return OAuth2ClientConfigurer.this;
+		}
+	}
+
+	@Override
+	public void init(B builder) throws Exception {
+		if (this.authorizationCodeGrantConfigurer != null) {
+			this.init(builder, this.authorizationCodeGrantConfigurer);
+		}
+	}
+
+	@Override
+	public void configure(B builder) throws Exception {
+		if (this.authorizationCodeGrantConfigurer != null) {
+			this.configure(builder, this.authorizationCodeGrantConfigurer);
+		}
+	}
+
+	private void init(B builder, AuthorizationCodeGrantConfigurer authorizationCodeGrantConfigurer) throws Exception {
+		OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient =
+			authorizationCodeGrantConfigurer.tokenEndpointConfig.accessTokenResponseClient;
+		if (accessTokenResponseClient == null) {
+			accessTokenResponseClient = new NimbusAuthorizationCodeTokenResponseClient();
+		}
+
+		OAuth2AuthorizationCodeAuthenticationProvider authorizationCodeAuthenticationProvider =
+			new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient);
+		builder.authenticationProvider(this.postProcess(authorizationCodeAuthenticationProvider));
+	}
+
+	private void configure(B builder, AuthorizationCodeGrantConfigurer authorizationCodeGrantConfigurer) throws Exception {
+		String authorizationRequestBaseUri = authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestBaseUri;
+		if (authorizationRequestBaseUri == null) {
+			authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
+		}
+
+		OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(
+			OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder), authorizationRequestBaseUri);
+
+		if (authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository != null) {
+			authorizationRequestFilter.setAuthorizationRequestRepository(
+				authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository);
+		}
+		builder.addFilter(this.postProcess(authorizationRequestFilter));
+
+		AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
+
+		OAuth2AuthorizationCodeGrantFilter authorizationCodeGrantFilter = new OAuth2AuthorizationCodeGrantFilter(
+			OAuth2ClientConfigurerUtils.getClientRegistrationRepository(builder),
+			OAuth2ClientConfigurerUtils.getAuthorizedClientService(builder),
+			authenticationManager);
+
+		if (authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository != null) {
+			authorizationCodeGrantFilter.setAuthorizationRequestRepository(
+				authorizationCodeGrantConfigurer.authorizationEndpointConfig.authorizationRequestRepository);
+		}
+		builder.addFilter(this.postProcess(authorizationCodeGrantFilter));
+	}
+}

+ 74 - 0
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerUtils.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.annotation.web.configurers.oauth2.client;
+
+import org.springframework.beans.factory.BeanFactoryUtils;
+import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+
+import java.util.Map;
+
+/**
+ * Utility methods for the OAuth 2.0 Client {@link AbstractHttpConfigurer}'s.
+ *
+ * @author Joe Grandja
+ * @since 5.1
+ */
+final class OAuth2ClientConfigurerUtils {
+
+	private OAuth2ClientConfigurerUtils() {
+	}
+
+	static <B extends HttpSecurityBuilder<B>> ClientRegistrationRepository getClientRegistrationRepository(B builder) {
+		ClientRegistrationRepository clientRegistrationRepository = builder.getSharedObject(ClientRegistrationRepository.class);
+		if (clientRegistrationRepository == null) {
+			clientRegistrationRepository = getClientRegistrationRepositoryBean(builder);
+			builder.setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
+		}
+		return clientRegistrationRepository;
+	}
+
+	private static <B extends HttpSecurityBuilder<B>> ClientRegistrationRepository getClientRegistrationRepositoryBean(B builder) {
+		return builder.getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class);
+	}
+
+	static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizedClientService getAuthorizedClientService(B builder) {
+		OAuth2AuthorizedClientService authorizedClientService = builder.getSharedObject(OAuth2AuthorizedClientService.class);
+		if (authorizedClientService == null) {
+			authorizedClientService = getAuthorizedClientServiceBean(builder);
+			if (authorizedClientService == null) {
+				authorizedClientService = new InMemoryOAuth2AuthorizedClientService(getClientRegistrationRepository(builder));
+			}
+			builder.setSharedObject(OAuth2AuthorizedClientService.class, authorizedClientService);
+		}
+		return authorizedClientService;
+	}
+
+	private static <B extends HttpSecurityBuilder<B>> OAuth2AuthorizedClientService getAuthorizedClientServiceBean(B builder) {
+		Map<String, OAuth2AuthorizedClientService> authorizedClientServiceMap = BeanFactoryUtils.beansOfTypeIncludingAncestors(
+				builder.getSharedObject(ApplicationContext.class), OAuth2AuthorizedClientService.class);
+		if (authorizedClientServiceMap.size() > 1) {
+			throw new NoUniqueBeanDefinitionException(OAuth2AuthorizedClientService.class, authorizedClientServiceMap.size(),
+				"Only one matching @Bean of type " + OAuth2AuthorizedClientService.class.getName() + " should be registered.");
+		}
+		return (!authorizedClientServiceMap.isEmpty() ? authorizedClientServiceMap.values().iterator().next() : null);
+	}
+}

+ 5 - 41
config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java

@@ -26,7 +26,6 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
 import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
-import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
 import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
 import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
 import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider;
 import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider;
 import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
 import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
@@ -376,8 +375,8 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>> exten
 	public void init(B http) throws Exception {
 	public void init(B http) throws Exception {
 		OAuth2LoginAuthenticationFilter authenticationFilter =
 		OAuth2LoginAuthenticationFilter authenticationFilter =
 			new OAuth2LoginAuthenticationFilter(
 			new OAuth2LoginAuthenticationFilter(
-				this.getClientRegistrationRepository(),
-				this.getAuthorizedClientService(),
+				OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()),
+				OAuth2ClientConfigurerUtils.getAuthorizedClientService(this.getBuilder()),
 				OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI);
 				OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI);
 		this.setAuthenticationFilter(authenticationFilter);
 		this.setAuthenticationFilter(authenticationFilter);
 		this.loginProcessingUrl(OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI);
 		this.loginProcessingUrl(OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI);
@@ -442,7 +441,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>> exten
 		}
 		}
 
 
 		OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(
 		OAuth2AuthorizationRequestRedirectFilter authorizationRequestFilter = new OAuth2AuthorizationRequestRedirectFilter(
-			this.getClientRegistrationRepository(), authorizationRequestBaseUri);
+			OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder()), authorizationRequestBaseUri);
 
 
 		if (this.authorizationEndpointConfig.authorizationRequestRepository != null) {
 		if (this.authorizationEndpointConfig.authorizationRequestRepository != null) {
 			authorizationRequestFilter.setAuthorizationRequestRepository(
 			authorizationRequestFilter.setAuthorizationRequestRepository(
@@ -466,41 +465,6 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>> exten
 		return new AntPathRequestMatcher(loginProcessingUrl);
 		return new AntPathRequestMatcher(loginProcessingUrl);
 	}
 	}
 
 
-	private ClientRegistrationRepository getClientRegistrationRepository() {
-		ClientRegistrationRepository clientRegistrationRepository =
-			this.getBuilder().getSharedObject(ClientRegistrationRepository.class);
-		if (clientRegistrationRepository == null) {
-			clientRegistrationRepository = this.getClientRegistrationRepositoryBean();
-			this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
-		}
-		return clientRegistrationRepository;
-	}
-
-	private ClientRegistrationRepository getClientRegistrationRepositoryBean() {
-		return this.getBuilder().getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class);
-	}
-
-	private OAuth2AuthorizedClientService getAuthorizedClientService() {
-		OAuth2AuthorizedClientService authorizedClientService =
-			this.getBuilder().getSharedObject(OAuth2AuthorizedClientService.class);
-		if (authorizedClientService == null) {
-			authorizedClientService = this.getAuthorizedClientServiceBean();
-			if (authorizedClientService == null) {
-				authorizedClientService = new InMemoryOAuth2AuthorizedClientService(this.getClientRegistrationRepository());
-			}
-			this.getBuilder().setSharedObject(OAuth2AuthorizedClientService.class, authorizedClientService);
-		}
-		return authorizedClientService;
-	}
-
-	private OAuth2AuthorizedClientService getAuthorizedClientServiceBean() {
-		Map<String, OAuth2AuthorizedClientService> authorizedClientServiceMap =
-			BeanFactoryUtils.beansOfTypeIncludingAncestors(
-				this.getBuilder().getSharedObject(ApplicationContext.class),
-				OAuth2AuthorizedClientService.class);
-		return (!authorizedClientServiceMap.isEmpty() ? authorizedClientServiceMap.values().iterator().next() : null);
-	}
-
 	private GrantedAuthoritiesMapper getGrantedAuthoritiesMapper() {
 	private GrantedAuthoritiesMapper getGrantedAuthoritiesMapper() {
 		GrantedAuthoritiesMapper grantedAuthoritiesMapper =
 		GrantedAuthoritiesMapper grantedAuthoritiesMapper =
 				this.getBuilder().getSharedObject(GrantedAuthoritiesMapper.class);
 				this.getBuilder().getSharedObject(GrantedAuthoritiesMapper.class);
@@ -528,7 +492,8 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>> exten
 		}
 		}
 
 
 		Iterable<ClientRegistration> clientRegistrations = null;
 		Iterable<ClientRegistration> clientRegistrations = null;
-		ClientRegistrationRepository clientRegistrationRepository = this.getClientRegistrationRepository();
+		ClientRegistrationRepository clientRegistrationRepository =
+			OAuth2ClientConfigurerUtils.getClientRegistrationRepository(this.getBuilder());
 		ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository).as(Iterable.class);
 		ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository).as(Iterable.class);
 		if (type != ResolvableType.NONE && ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
 		if (type != ResolvableType.NONE && ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
 			clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
 			clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
@@ -580,5 +545,4 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>> exten
 			return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication);
 			return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication);
 		}
 		}
 	}
 	}
-
 }
 }

+ 5 - 5
config/src/main/java/org/springframework/security/config/oauth2/client/CommonOAuth2Provider.java

@@ -36,7 +36,7 @@ public enum CommonOAuth2Provider {
 		@Override
 		@Override
 		public Builder getBuilder(String registrationId) {
 		public Builder getBuilder(String registrationId) {
 			ClientRegistration.Builder builder = getBuilder(registrationId,
 			ClientRegistration.Builder builder = getBuilder(registrationId,
-					ClientAuthenticationMethod.BASIC, DEFAULT_LOGIN_REDIRECT_URL);
+					ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
 			builder.scope("openid", "profile", "email");
 			builder.scope("openid", "profile", "email");
 			builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
 			builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
 			builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
 			builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
@@ -53,7 +53,7 @@ public enum CommonOAuth2Provider {
 		@Override
 		@Override
 		public Builder getBuilder(String registrationId) {
 		public Builder getBuilder(String registrationId) {
 			ClientRegistration.Builder builder = getBuilder(registrationId,
 			ClientRegistration.Builder builder = getBuilder(registrationId,
-					ClientAuthenticationMethod.BASIC, DEFAULT_LOGIN_REDIRECT_URL);
+					ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
 			builder.scope("read:user");
 			builder.scope("read:user");
 			builder.authorizationUri("https://github.com/login/oauth/authorize");
 			builder.authorizationUri("https://github.com/login/oauth/authorize");
 			builder.tokenUri("https://github.com/login/oauth/access_token");
 			builder.tokenUri("https://github.com/login/oauth/access_token");
@@ -69,7 +69,7 @@ public enum CommonOAuth2Provider {
 		@Override
 		@Override
 		public Builder getBuilder(String registrationId) {
 		public Builder getBuilder(String registrationId) {
 			ClientRegistration.Builder builder = getBuilder(registrationId,
 			ClientRegistration.Builder builder = getBuilder(registrationId,
-					ClientAuthenticationMethod.POST, DEFAULT_LOGIN_REDIRECT_URL);
+					ClientAuthenticationMethod.POST, DEFAULT_REDIRECT_URL);
 			builder.scope("public_profile", "email");
 			builder.scope("public_profile", "email");
 			builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth");
 			builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth");
 			builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token");
 			builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token");
@@ -85,7 +85,7 @@ public enum CommonOAuth2Provider {
 		@Override
 		@Override
 		public Builder getBuilder(String registrationId) {
 		public Builder getBuilder(String registrationId) {
 			ClientRegistration.Builder builder = getBuilder(registrationId,
 			ClientRegistration.Builder builder = getBuilder(registrationId,
-					ClientAuthenticationMethod.BASIC, DEFAULT_LOGIN_REDIRECT_URL);
+					ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
 			builder.scope("openid", "profile", "email", "address", "phone");
 			builder.scope("openid", "profile", "email", "address", "phone");
 			builder.userNameAttributeName(IdTokenClaimNames.SUB);
 			builder.userNameAttributeName(IdTokenClaimNames.SUB);
 			builder.clientName("Okta");
 			builder.clientName("Okta");
@@ -93,7 +93,7 @@ public enum CommonOAuth2Provider {
 		}
 		}
 	};
 	};
 
 
-	private static final String DEFAULT_LOGIN_REDIRECT_URL = "{baseUrl}/login/oauth2/code/{registrationId}";
+	private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}";
 
 
 	protected final ClientRegistration.Builder getBuilder(String registrationId,
 	protected final ClientRegistration.Builder getBuilder(String registrationId,
 															ClientAuthenticationMethod method, String redirectUri) {
 															ClientAuthenticationMethod method, String redirectUri) {

+ 171 - 0
config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurerTests.java

@@ -0,0 +1,171 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.annotation.web.configurers.oauth2.client;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.mock.web.MockHttpSession;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.test.SpringTestRule;
+import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
+import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+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.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Tests for {@link OAuth2ClientConfigurer}.
+ *
+ * @author Joe Grandja
+ */
+public class OAuth2ClientConfigurerTests {
+	private static ClientRegistrationRepository clientRegistrationRepository;
+
+	private static OAuth2AuthorizedClientService authorizedClientService;
+
+	private static OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
+
+	@Rule
+	public final SpringTestRule spring = new SpringTestRule();
+
+	@Autowired
+	private MockMvc mockMvc;
+
+	private ClientRegistration registration1;
+
+	@Before
+	public void setup() {
+		this.registration1 = ClientRegistration.withRegistrationId("registration-1")
+			.clientId("client-1")
+			.clientSecret("secret")
+			.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
+			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+			.redirectUriTemplate("{baseUrl}/client-1")
+			.scope("user")
+			.authorizationUri("https://provider.com/oauth2/authorize")
+			.tokenUri("https://provider.com/oauth2/token")
+			.userInfoUri("https://provider.com/oauth2/user")
+			.userNameAttributeName("id")
+			.clientName("client-1")
+			.build();
+		clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1);
+		authorizedClientService = new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
+
+		OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("access-token-1234")
+				.tokenType(OAuth2AccessToken.TokenType.BEARER)
+				.expiresIn(300)
+				.build();
+		accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class);
+		when(accessTokenResponseClient.getTokenResponse(any(OAuth2AuthorizationCodeGrantRequest.class))).thenReturn(accessTokenResponse);
+	}
+
+	@Test
+	public void configureWhenAuthorizationCodeRequestThenRedirectForAuthorization() throws Exception {
+		this.spring.register(OAuth2ClientConfig.class).autowire();
+
+		MvcResult mvcResult = this.mockMvc.perform(get("/oauth2/authorization/registration-1"))
+			.andExpect(status().is3xxRedirection())
+			.andReturn();
+		assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/client-1");
+	}
+
+	@Test
+	public void configureWhenAuthorizationCodeResponseSuccessThenAuthorizedClientSaved() throws Exception {
+		this.spring.register(OAuth2ClientConfig.class).autowire();
+
+		// Setup the Authorization Request in the session
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, this.registration1.getRegistrationId());
+		OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
+				.authorizationUri(this.registration1.getProviderDetails().getAuthorizationUri())
+				.clientId(this.registration1.getClientId())
+				.redirectUri("http://localhost/client-1")
+				.state("state")
+				.additionalParameters(additionalParameters)
+				.build();
+
+		AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository =
+				new HttpSessionOAuth2AuthorizationRequestRepository();
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
+
+		MockHttpSession session = (MockHttpSession) request.getSession();
+
+		String principalName = "user1";
+
+		this.mockMvc.perform(get("/client-1")
+			.param(OAuth2ParameterNames.CODE, "code")
+			.param(OAuth2ParameterNames.STATE, "state")
+			.with(user(principalName))
+			.session(session))
+			.andExpect(status().is3xxRedirection())
+			.andExpect(redirectedUrl("http://localhost/client-1"));
+
+		OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(
+			this.registration1.getRegistrationId(), principalName);
+		assertThat(authorizedClient).isNotNull();
+	}
+
+	@EnableWebSecurity
+	static class OAuth2ClientConfig extends WebSecurityConfigurerAdapter {
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			http
+				.authorizeRequests()
+					.anyRequest().authenticated()
+					.and()
+				.oauth2()
+					.client()
+						.clientRegistrationRepository(clientRegistrationRepository)
+						.authorizedClientService(authorizedClientService)
+						.authorizationCodeGrant()
+							.tokenEndpoint()
+								.accessTokenResponseClient(accessTokenResponseClient);
+		}
+	}
+}

+ 5 - 5
config/src/test/java/org/springframework/security/config/oauth2/client/CommonOAuth2ProviderTests.java

@@ -31,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat;
  */
  */
 public class CommonOAuth2ProviderTests {
 public class CommonOAuth2ProviderTests {
 
 
-	private static final String DEFAULT_LOGIN_REDIRECT_URL = "{baseUrl}/login/oauth2/code/{registrationId}";
+	private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}";
 
 
 	@Test
 	@Test
 	public void getBuilderWhenGoogleShouldHaveGoogleSettings() throws Exception {
 	public void getBuilderWhenGoogleShouldHaveGoogleSettings() throws Exception {
@@ -51,7 +51,7 @@ public class CommonOAuth2ProviderTests {
 			.isEqualTo(ClientAuthenticationMethod.BASIC);
 			.isEqualTo(ClientAuthenticationMethod.BASIC);
 		assertThat(registration.getAuthorizationGrantType())
 		assertThat(registration.getAuthorizationGrantType())
 			.isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
 			.isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
-		assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_LOGIN_REDIRECT_URL);
+		assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_REDIRECT_URL);
 		assertThat(registration.getScopes()).containsOnly("openid", "profile", "email");
 		assertThat(registration.getScopes()).containsOnly("openid", "profile", "email");
 		assertThat(registration.getClientName()).isEqualTo("Google");
 		assertThat(registration.getClientName()).isEqualTo("Google");
 		assertThat(registration.getRegistrationId()).isEqualTo("123");
 		assertThat(registration.getRegistrationId()).isEqualTo("123");
@@ -74,7 +74,7 @@ public class CommonOAuth2ProviderTests {
 			.isEqualTo(ClientAuthenticationMethod.BASIC);
 			.isEqualTo(ClientAuthenticationMethod.BASIC);
 		assertThat(registration.getAuthorizationGrantType())
 		assertThat(registration.getAuthorizationGrantType())
 			.isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
 			.isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
-		assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_LOGIN_REDIRECT_URL);
+		assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_REDIRECT_URL);
 		assertThat(registration.getScopes()).containsOnly("read:user");
 		assertThat(registration.getScopes()).containsOnly("read:user");
 		assertThat(registration.getClientName()).isEqualTo("GitHub");
 		assertThat(registration.getClientName()).isEqualTo("GitHub");
 		assertThat(registration.getRegistrationId()).isEqualTo("123");
 		assertThat(registration.getRegistrationId()).isEqualTo("123");
@@ -97,7 +97,7 @@ public class CommonOAuth2ProviderTests {
 			.isEqualTo(ClientAuthenticationMethod.POST);
 			.isEqualTo(ClientAuthenticationMethod.POST);
 		assertThat(registration.getAuthorizationGrantType())
 		assertThat(registration.getAuthorizationGrantType())
 			.isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
 			.isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
-		assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_LOGIN_REDIRECT_URL);
+		assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_REDIRECT_URL);
 		assertThat(registration.getScopes()).containsOnly("public_profile", "email");
 		assertThat(registration.getScopes()).containsOnly("public_profile", "email");
 		assertThat(registration.getClientName()).isEqualTo("Facebook");
 		assertThat(registration.getClientName()).isEqualTo("Facebook");
 		assertThat(registration.getRegistrationId()).isEqualTo("123");
 		assertThat(registration.getRegistrationId()).isEqualTo("123");
@@ -122,7 +122,7 @@ public class CommonOAuth2ProviderTests {
 			.isEqualTo(ClientAuthenticationMethod.BASIC);
 			.isEqualTo(ClientAuthenticationMethod.BASIC);
 		assertThat(registration.getAuthorizationGrantType())
 		assertThat(registration.getAuthorizationGrantType())
 			.isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
 			.isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE);
-		assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_LOGIN_REDIRECT_URL);
+		assertThat(registration.getRedirectUriTemplate()).isEqualTo(DEFAULT_REDIRECT_URL);
 		assertThat(registration.getScopes()).containsOnly("openid", "profile", "email",
 		assertThat(registration.getScopes()).containsOnly("openid", "profile", "email",
 			"address", "phone");
 			"address", "phone");
 		assertThat(registration.getClientName()).isEqualTo("Okta");
 		assertThat(registration.getClientName()).isEqualTo("Okta");

+ 60 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/ClientAuthorizationRequiredException.java

@@ -0,0 +1,60 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.oauth2.client;
+
+import org.springframework.util.Assert;
+
+/**
+ * This exception is thrown when an OAuth 2.0 Client is required
+ * to obtain authorization from the Resource Owner.
+ *
+ * @author Joe Grandja
+ * @since 5.1
+ * @see OAuth2AuthorizedClient
+ */
+public class ClientAuthorizationRequiredException extends OAuth2ClientException {
+	private final String clientRegistrationId;
+
+	/**
+	 * Constructs a {@code ClientAuthorizationRequiredException} using the provided parameters.
+	 *
+	 * @param clientRegistrationId the identifier for the client's registration
+	 */
+	public ClientAuthorizationRequiredException(String clientRegistrationId) {
+		this(clientRegistrationId, "Authorization required for Client Registration Id: " + clientRegistrationId);
+	}
+
+	/**
+	 * Constructs a {@code ClientAuthorizationRequiredException} using the provided parameters.
+	 *
+	 * @param clientRegistrationId the identifier for the client's registration
+	 * @param message the detail message
+	 */
+	public ClientAuthorizationRequiredException(String clientRegistrationId, String message) {
+		super(message);
+		Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
+		this.clientRegistrationId = clientRegistrationId;
+	}
+
+	/**
+	 * Returns the identifier for the client's registration.
+	 *
+	 * @return the identifier for the client's registration
+	 */
+	public String getClientRegistrationId() {
+		return this.clientRegistrationId;
+	}
+}

+ 44 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/OAuth2ClientException.java

@@ -0,0 +1,44 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.oauth2.client;
+
+/**
+ * Base exception for OAuth 2.0 Client related errors.
+ *
+ * @author Joe Grandja
+ * @since 5.1
+ */
+public class OAuth2ClientException extends RuntimeException {
+
+	/**
+	 * Constructs an {@code OAuth2ClientException} using the provided parameters.
+	 *
+	 * @param message the detail message
+	 */
+	public OAuth2ClientException(String message) {
+		super(message);
+	}
+
+	/**
+	 * Constructs an {@code OAuth2ClientException} using the provided parameters.
+	 *
+	 * @param message the detail message
+	 * @param cause the root cause
+	 */
+	public OAuth2ClientException(String message, Throwable cause) {
+		super(message, cause);
+	}
+}

+ 88 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProvider.java

@@ -0,0 +1,88 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.oauth2.client.authentication;
+
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.util.Assert;
+
+/**
+ * An implementation of an {@link AuthenticationProvider} for the OAuth 2.0 Authorization Code Grant.
+ *
+ * <p>
+ * This {@link AuthenticationProvider} is responsible for authenticating
+ * an Authorization Code credential with the Authorization Server's Token Endpoint
+ * and if valid, exchanging it for an Access Token credential.
+ *
+ * @author Joe Grandja
+ * @since 5.1
+ * @see OAuth2AuthorizationCodeAuthenticationToken
+ * @see OAuth2AccessTokenResponseClient
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
+ */
+public class OAuth2AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
+	private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeAuthenticationProvider} using the provided parameters.
+	 *
+	 * @param accessTokenResponseClient the client used for requesting the access token credential from the Token Endpoint
+	 */
+	public OAuth2AuthorizationCodeAuthenticationProvider(
+		OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient) {
+
+		Assert.notNull(accessTokenResponseClient, "accessTokenResponseClient cannot be null");
+		this.accessTokenResponseClient = accessTokenResponseClient;
+	}
+
+	@Override
+	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+		OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =
+			(OAuth2AuthorizationCodeAuthenticationToken) authentication;
+
+		OAuth2AuthorizationExchangeValidator.validate(
+			authorizationCodeAuthentication.getAuthorizationExchange());
+
+		OAuth2AccessTokenResponse accessTokenResponse =
+			this.accessTokenResponseClient.getTokenResponse(
+				new OAuth2AuthorizationCodeGrantRequest(
+					authorizationCodeAuthentication.getClientRegistration(),
+					authorizationCodeAuthentication.getAuthorizationExchange()));
+
+		OAuth2AccessToken accessToken = accessTokenResponse.getAccessToken();
+
+		OAuth2AuthorizationCodeAuthenticationToken authenticationResult =
+			new OAuth2AuthorizationCodeAuthenticationToken(
+				authorizationCodeAuthentication.getClientRegistration(),
+				authorizationCodeAuthentication.getAuthorizationExchange(),
+				accessToken);
+		authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
+
+		return authenticationResult;
+	}
+
+	@Override
+	public boolean supports(Class<?> authentication) {
+		return OAuth2AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+}

+ 114 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationToken.java

@@ -0,0 +1,114 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.oauth2.client.authentication;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.SpringSecurityCoreVersion;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
+import org.springframework.util.Assert;
+
+import java.util.Collections;
+
+/**
+ * An {@link AbstractAuthenticationToken} for the OAuth 2.0 Authorization Code Grant.
+ *
+ * @author Joe Grandja
+ * @since 5.1
+ * @see AbstractAuthenticationToken
+ * @see ClientRegistration
+ * @see OAuth2AuthorizationExchange
+ * @see OAuth2AccessToken
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant Flow</a>
+ */
+public class OAuth2AuthorizationCodeAuthenticationToken extends AbstractAuthenticationToken {
+	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
+	private ClientRegistration clientRegistration;
+	private OAuth2AuthorizationExchange authorizationExchange;
+	private OAuth2AccessToken accessToken;
+
+	/**
+	 * This constructor should be used when the Authorization Request/Response is complete.
+	 *
+	 * @param clientRegistration the client registration
+	 * @param authorizationExchange the authorization exchange
+	 */
+	public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration,
+														OAuth2AuthorizationExchange authorizationExchange) {
+		super(Collections.emptyList());
+		Assert.notNull(clientRegistration, "clientRegistration cannot be null");
+		Assert.notNull(authorizationExchange, "authorizationExchange cannot be null");
+		this.clientRegistration = clientRegistration;
+		this.authorizationExchange = authorizationExchange;
+	}
+
+	/**
+	 * This constructor should be used when the Access Token Request/Response is complete,
+	 * which indicates that the Authorization Code Grant flow has fully completed.
+	 *
+	 * @param clientRegistration the client registration
+	 * @param authorizationExchange the authorization exchange
+	 * @param accessToken the access token credential
+	 */
+	public OAuth2AuthorizationCodeAuthenticationToken(ClientRegistration clientRegistration,
+														OAuth2AuthorizationExchange authorizationExchange,
+														OAuth2AccessToken accessToken) {
+		this(clientRegistration, authorizationExchange);
+		Assert.notNull(accessToken, "accessToken cannot be null");
+		this.accessToken = accessToken;
+		this.setAuthenticated(true);
+	}
+
+	@Override
+	public Object getPrincipal() {
+		return this.clientRegistration.getClientId();
+	}
+
+	@Override
+	public Object getCredentials() {
+		return this.accessToken != null ?
+			this.accessToken.getTokenValue() :
+			this.authorizationExchange.getAuthorizationResponse().getCode();
+	}
+
+	/**
+	 * Returns the {@link ClientRegistration client registration}.
+	 *
+	 * @return the {@link ClientRegistration}
+	 */
+	public ClientRegistration getClientRegistration() {
+		return this.clientRegistration;
+	}
+
+	/**
+	 * Returns the {@link OAuth2AuthorizationExchange authorization exchange}.
+	 *
+	 * @return the {@link OAuth2AuthorizationExchange}
+	 */
+	public OAuth2AuthorizationExchange getAuthorizationExchange() {
+		return this.authorizationExchange;
+	}
+
+	/**
+	 * Returns the {@link OAuth2AccessToken access token}.
+	 *
+	 * @return the {@link OAuth2AccessToken}
+	 */
+	public OAuth2AccessToken getAccessToken() {
+		return this.accessToken;
+	}
+}

+ 54 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationExchangeValidator.java

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.oauth2.client.authentication;
+
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
+
+/**
+ * A validator for an &quot;exchange&quot; of an OAuth 2.0 Authorization Request and Response.
+ *
+ * @author Joe Grandja
+ * @since 5.1
+ * @see OAuth2AuthorizationExchange
+ */
+final class OAuth2AuthorizationExchangeValidator {
+	private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
+	private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
+
+	static void validate(OAuth2AuthorizationExchange authorizationExchange) {
+		OAuth2AuthorizationRequest authorizationRequest = authorizationExchange.getAuthorizationRequest();
+		OAuth2AuthorizationResponse authorizationResponse = authorizationExchange.getAuthorizationResponse();
+
+		if (authorizationResponse.statusError()) {
+			throw new OAuth2AuthenticationException(
+				authorizationResponse.getError(), authorizationResponse.getError().toString());
+		}
+
+		if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
+			OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
+			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+		}
+
+		if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
+			OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
+			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+		}
+	}
+}

+ 2 - 25
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/authentication/OAuth2LoginAuthenticationProvider.java

@@ -25,11 +25,7 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCo
 import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
 import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
 import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
 import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
-import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
-import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
-import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
-import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
 import org.springframework.security.oauth2.core.user.OAuth2User;
 import org.springframework.security.oauth2.core.user.OAuth2User;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 
 
@@ -60,8 +56,6 @@ import java.util.Collection;
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
  * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response</a>
  */
  */
 public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
 public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider {
-	private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
-	private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
 	private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
 	private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
 	private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
 	private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;
 	private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
 	private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
@@ -97,25 +91,8 @@ public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider
 			return null;
 			return null;
 		}
 		}
 
 
-		OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication
-			.getAuthorizationExchange().getAuthorizationRequest();
-		OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication
-			.getAuthorizationExchange().getAuthorizationResponse();
-
-		if (authorizationResponse.statusError()) {
-			throw new OAuth2AuthenticationException(
-				authorizationResponse.getError(), authorizationResponse.getError().toString());
-		}
-
-		if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
-			OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
-			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
-		}
-
-		if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
-			OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
-			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
-		}
+		OAuth2AuthorizationExchangeValidator.validate(
+			authorizationCodeAuthentication.getAuthorizationExchange());
 
 
 		OAuth2AccessTokenResponse accessTokenResponse =
 		OAuth2AccessTokenResponse accessTokenResponse =
 			this.accessTokenResponseClient.getTokenResponse(
 			this.accessTokenResponseClient.getTokenResponse(

+ 209 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilter.java

@@ -0,0 +1,209 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.oauth2.client.web;
+
+import org.springframework.security.authentication.AuthenticationDetailsSource;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.web.DefaultRedirectStrategy;
+import org.springframework.security.web.RedirectStrategy;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
+import org.springframework.security.web.savedrequest.RequestCache;
+import org.springframework.security.web.savedrequest.SavedRequest;
+import org.springframework.security.web.util.UrlUtils;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * A {@code Filter} for the OAuth 2.0 Authorization Code Grant,
+ * which handles the processing of the OAuth 2.0 Authorization Response.
+ *
+ * <p>
+ * The OAuth 2.0 Authorization Response is processed as follows:
+ *
+ * <ul>
+ * <li>
+ *	Assuming the End-User (Resource Owner) has granted access to the Client, the Authorization Server will append the
+ *	{@link OAuth2ParameterNames#CODE code} and {@link OAuth2ParameterNames#STATE state} parameters
+ *	to the {@link OAuth2ParameterNames#REDIRECT_URI redirect_uri} (provided in the Authorization Request)
+ *	and redirect the End-User's user-agent back to this {@code Filter} (the Client).
+ * </li>
+ * <li>
+ *  This {@code Filter} will then create an {@link OAuth2AuthorizationCodeAuthenticationToken} with
+ *  the {@link OAuth2ParameterNames#CODE code} received and
+ *  delegate it to the {@link AuthenticationManager} to authenticate.
+ * </li>
+ * <li>
+ *  Upon a successful authentication, an {@link OAuth2AuthorizedClient Authorized Client} is created by associating the
+ *  {@link OAuth2AuthorizationCodeAuthenticationToken#getClientRegistration() client} to the
+ *  {@link OAuth2AuthorizationCodeAuthenticationToken#getAccessToken() access token} and current {@code Principal}
+ *  and saving it via the {@link OAuth2AuthorizedClientService}.
+ * </li>
+ * </ul>
+ *
+ * @author Joe Grandja
+ * @since 5.1
+ * @see OAuth2AuthorizationCodeAuthenticationToken
+ * @see OAuth2AuthorizationCodeAuthenticationProvider
+ * @see OAuth2AuthorizationRequest
+ * @see OAuth2AuthorizationResponse
+ * @see AuthorizationRequestRepository
+ * @see OAuth2AuthorizationRequestRedirectFilter
+ * @see ClientRegistrationRepository
+ * @see OAuth2AuthorizedClient
+ * @see OAuth2AuthorizedClientService
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">Section 4.1 Authorization Code Grant</a>
+ * @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
+ */
+public class OAuth2AuthorizationCodeGrantFilter extends OncePerRequestFilter {
+	private final ClientRegistrationRepository clientRegistrationRepository;
+	private final OAuth2AuthorizedClientService authorizedClientService;
+	private final AuthenticationManager authenticationManager;
+	private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository =
+		new HttpSessionOAuth2AuthorizationRequestRepository();
+	private final AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
+	private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
+	private final RequestCache requestCache = new HttpSessionRequestCache();
+
+	/**
+	 * Constructs an {@code OAuth2AuthorizationCodeGrantFilter} using the provided parameters.
+	 *
+	 * @param clientRegistrationRepository the repository of client registrations
+	 * @param authorizedClientService the authorized client service
+	 * @param authenticationManager the authentication manager
+	 */
+	public OAuth2AuthorizationCodeGrantFilter(ClientRegistrationRepository clientRegistrationRepository,
+												OAuth2AuthorizedClientService authorizedClientService,
+												AuthenticationManager authenticationManager) {
+		Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
+		Assert.notNull(authorizedClientService, "authorizedClientService cannot be null");
+		Assert.notNull(authenticationManager, "authenticationManager cannot be null");
+		this.clientRegistrationRepository = clientRegistrationRepository;
+		this.authorizedClientService = authorizedClientService;
+		this.authenticationManager = authenticationManager;
+	}
+
+	/**
+	 * Sets the repository for stored {@link OAuth2AuthorizationRequest}'s.
+	 *
+	 * @param authorizationRequestRepository the repository for stored {@link OAuth2AuthorizationRequest}'s
+	 */
+	public final void setAuthorizationRequestRepository(AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository) {
+		Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null");
+		this.authorizationRequestRepository = authorizationRequestRepository;
+	}
+
+	@Override
+	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+		throws ServletException, IOException {
+
+		if (this.shouldProcessAuthorizationResponse(request)) {
+			this.processAuthorizationResponse(request, response);
+			return;
+		}
+
+		filterChain.doFilter(request, response);
+	}
+
+	private boolean shouldProcessAuthorizationResponse(HttpServletRequest request) {
+		OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request);
+		if (authorizationRequest == null) {
+			return false;
+		}
+		String requestUrl = UrlUtils.buildFullRequestUrl(request.getScheme(), request.getServerName(),
+				request.getServerPort(), request.getRequestURI(), null);
+		if (requestUrl.equals(authorizationRequest.getRedirectUri()) &&
+				OAuth2AuthorizationResponseUtils.isAuthorizationResponse(request)) {
+			return true;
+		}
+		return false;
+	}
+
+	private void processAuthorizationResponse(HttpServletRequest request, HttpServletResponse response)
+		throws ServletException, IOException {
+
+		OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request);
+
+		String registrationId = (String) authorizationRequest.getAdditionalParameters().get(OAuth2ParameterNames.REGISTRATION_ID);
+		ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
+
+		OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(request);
+
+		OAuth2AuthorizationCodeAuthenticationToken authenticationRequest = new OAuth2AuthorizationCodeAuthenticationToken(
+			clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
+		authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
+
+		OAuth2AuthorizationCodeAuthenticationToken authenticationResult;
+
+		try {
+			authenticationResult = (OAuth2AuthorizationCodeAuthenticationToken)
+				this.authenticationManager.authenticate(authenticationRequest);
+		} catch (OAuth2AuthenticationException ex) {
+			OAuth2Error error = ex.getError();
+			UriComponentsBuilder uriBuilder = UriComponentsBuilder
+				.fromUriString(authorizationResponse.getRedirectUri())
+				.queryParam(OAuth2ParameterNames.ERROR, error.getErrorCode());
+			if (!StringUtils.isEmpty(error.getDescription())) {
+				uriBuilder.queryParam(OAuth2ParameterNames.ERROR_DESCRIPTION, error.getDescription());
+			}
+			if (!StringUtils.isEmpty(error.getUri())) {
+				uriBuilder.queryParam(OAuth2ParameterNames.ERROR_URI, error.getUri());
+			}
+			this.redirectStrategy.sendRedirect(request, response, uriBuilder.build().encode().toString());
+			return;
+		}
+
+		Authentication currentAuthentication = SecurityContextHolder.getContext().getAuthentication();
+
+		OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
+			authenticationResult.getClientRegistration(),
+			currentAuthentication.getName(),
+			authenticationResult.getAccessToken());
+
+		this.authorizedClientService.saveAuthorizedClient(authorizedClient, currentAuthentication);
+
+		String redirectUrl = authorizationResponse.getRedirectUri();
+		SavedRequest savedRequest = this.requestCache.getRequest(request, response);
+		if (savedRequest != null) {
+			redirectUrl = savedRequest.getRedirectUrl();
+			this.requestCache.removeRequest(request, response);
+		}
+
+		this.redirectStrategy.sendRedirect(request, response, redirectUrl);
+	}
+}

+ 97 - 6
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilter.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2018 the original author or authors.
  *
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client.web;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.HttpStatus;
 import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
 import org.springframework.security.crypto.keygen.Base64StringKeyGenerator;
 import org.springframework.security.crypto.keygen.StringKeyGenerator;
 import org.springframework.security.crypto.keygen.StringKeyGenerator;
+import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException;
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
@@ -25,6 +26,9 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequ
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.web.DefaultRedirectStrategy;
 import org.springframework.security.web.DefaultRedirectStrategy;
 import org.springframework.security.web.RedirectStrategy;
 import org.springframework.security.web.RedirectStrategy;
+import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
+import org.springframework.security.web.savedrequest.RequestCache;
+import org.springframework.security.web.util.ThrowableAnalyzer;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
 import org.springframework.web.filter.OncePerRequestFilter;
 import org.springframework.web.filter.OncePerRequestFilter;
@@ -51,6 +55,17 @@ import java.util.Map;
  * response type, and a redirection URI which the authorization server will send the user-agent back to
  * response type, and a redirection URI which the authorization server will send the user-agent back to
  * once access is granted (or denied) by the End-User (Resource Owner).
  * once access is granted (or denied) by the End-User (Resource Owner).
  *
  *
+ * <p>
+ * By default, this {@code Filter} responds to authorization requests
+ * at the {@code URI} {@code /oauth2/authorization/{registrationId}}.
+ * The {@code URI} template variable {@code {registrationId}} represents the
+ * {@link ClientRegistration#getRegistrationId() registration identifier} of the client
+ * that is used for initiating the OAuth 2.0 Authorization Request.
+ *
+ * <p>
+ * <b>NOTE:</b> The default base {@code URI} {@code /oauth2/authorization} may be overridden
+ * via it's constructor {@link #OAuth2AuthorizationRequestRedirectFilter(ClientRegistrationRepository, String)}.
+
  * @author Joe Grandja
  * @author Joe Grandja
  * @author Rob Winch
  * @author Rob Winch
  * @since 5.0
  * @since 5.0
@@ -69,6 +84,8 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
 	 */
 	 */
 	public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization";
 	public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization";
 	private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";
 	private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";
+	private static final String AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME =
+			ClientAuthorizationRequiredException.class.getName() + ".AUTHORIZATION_REQUIRED_EXCEPTION";
 	private final AntPathRequestMatcher authorizationRequestMatcher;
 	private final AntPathRequestMatcher authorizationRequestMatcher;
 	private final ClientRegistrationRepository clientRegistrationRepository;
 	private final ClientRegistrationRepository clientRegistrationRepository;
 	private final OAuth2AuthorizationRequestUriBuilder authorizationRequestUriBuilder = new OAuth2AuthorizationRequestUriBuilder();
 	private final OAuth2AuthorizationRequestUriBuilder authorizationRequestUriBuilder = new OAuth2AuthorizationRequestUriBuilder();
@@ -76,6 +93,8 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
 	private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
 	private final StringKeyGenerator stateGenerator = new Base64StringKeyGenerator(Base64.getUrlEncoder());
 	private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository =
 	private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository =
 		new HttpSessionOAuth2AuthorizationRequestRepository();
 		new HttpSessionOAuth2AuthorizationRequestRepository();
+	private final ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();
+	private final RequestCache requestCache = new HttpSessionRequestCache();
 
 
 	/**
 	/**
 	 * Constructs an {@code OAuth2AuthorizationRequestRedirectFilter} using the provided parameters.
 	 * Constructs an {@code OAuth2AuthorizationRequestRedirectFilter} using the provided parameters.
@@ -125,7 +144,36 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
 			return;
 			return;
 		}
 		}
 
 
-		filterChain.doFilter(request, response);
+		try {
+			filterChain.doFilter(request, response);
+		} catch (IOException ex) {
+			throw ex;
+		} catch (Exception ex) {
+			// Check to see if we need to handle ClientAuthorizationRequiredException
+			Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
+			ClientAuthorizationRequiredException authzEx = (ClientAuthorizationRequiredException) this.throwableAnalyzer
+				.getFirstThrowableOfType(ClientAuthorizationRequiredException.class, causeChain);
+			if (authzEx != null) {
+				try {
+					request.setAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME, authzEx);
+					this.sendRedirectForAuthorization(request, response, authzEx.getClientRegistrationId());
+					this.requestCache.saveRequest(request, response);
+				} catch (Exception failed) {
+					this.unsuccessfulRedirectForAuthorization(request, response, failed);
+				} finally {
+					request.removeAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME);
+				}
+				return;
+			}
+
+			if (ex instanceof ServletException) {
+				throw (ServletException) ex;
+			} else if (ex instanceof RuntimeException) {
+				throw (RuntimeException) ex;
+			} else {
+				throw new RuntimeException(ex);
+			}
+		}
 	}
 	}
 
 
 	private boolean shouldRequestAuthorization(HttpServletRequest request, HttpServletResponse response) {
 	private boolean shouldRequestAuthorization(HttpServletRequest request, HttpServletResponse response) {
@@ -133,14 +181,25 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
 	}
 	}
 
 
 	private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response)
 	private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response)
-			throws IOException, ServletException {
+		throws IOException, ServletException {
 
 
 		String registrationId = this.authorizationRequestMatcher
 		String registrationId = this.authorizationRequestMatcher
 			.extractUriTemplateVariables(request).get(REGISTRATION_ID_URI_VARIABLE_NAME);
 			.extractUriTemplateVariables(request).get(REGISTRATION_ID_URI_VARIABLE_NAME);
+		this.sendRedirectForAuthorization(request, response, registrationId);
+	}
+
+	private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
+												String registrationId) throws IOException, ServletException {
+
 		ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
 		ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
 		if (clientRegistration == null) {
 		if (clientRegistration == null) {
 			throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
 			throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
 		}
 		}
+		this.sendRedirectForAuthorization(request, response, clientRegistration);
+	}
+
+	private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
+												ClientRegistration clientRegistration) throws IOException, ServletException {
 
 
 		String redirectUriStr = this.expandRedirectUri(request, clientRegistration);
 		String redirectUriStr = this.expandRedirectUri(request, clientRegistration);
 
 
@@ -188,6 +247,11 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
 			port = -1;		// Removes the port in UriComponentsBuilder
 			port = -1;		// Removes the port in UriComponentsBuilder
 		}
 		}
 
 
+		// Supported URI variables -> baseUrl, action, registrationId
+		// Used in -> CommonOAuth2Provider.DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}"
+		Map<String, String> uriVariables = new HashMap<>();
+		uriVariables.put("registrationId", clientRegistration.getRegistrationId());
+
 		String baseUrl = UriComponentsBuilder.newInstance()
 		String baseUrl = UriComponentsBuilder.newInstance()
 			.scheme(request.getScheme())
 			.scheme(request.getScheme())
 			.host(request.getServerName())
 			.host(request.getServerName())
@@ -195,13 +259,40 @@ public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilt
 			.path(request.getContextPath())
 			.path(request.getContextPath())
 			.build()
 			.build()
 			.toUriString();
 			.toUriString();
-
-		Map<String, String> uriVariables = new HashMap<>();
 		uriVariables.put("baseUrl", baseUrl);
 		uriVariables.put("baseUrl", baseUrl);
-		uriVariables.put("registrationId", clientRegistration.getRegistrationId());
+
+		if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
+			String loginAction = "login";
+			String authorizeAction = "authorize";
+			String actionParameter = "action";
+			String action;
+			if (request.getAttribute(AUTHORIZATION_REQUIRED_EXCEPTION_ATTR_NAME) != null) {
+				action = authorizeAction;
+			} else if (request.getParameter(actionParameter) == null) {
+				action = loginAction;
+			} else {
+				String actionValue = request.getParameter(actionParameter);
+				if (loginAction.equalsIgnoreCase(actionValue)) {
+					action = loginAction;
+				} else {
+					action = authorizeAction;
+				}
+			}
+			uriVariables.put("action", action);
+		}
 
 
 		return UriComponentsBuilder.fromUriString(clientRegistration.getRedirectUriTemplate())
 		return UriComponentsBuilder.fromUriString(clientRegistration.getRedirectUriTemplate())
 			.buildAndExpand(uriVariables)
 			.buildAndExpand(uriVariables)
 			.toUriString();
 			.toUriString();
 	}
 	}
+
+	private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer {
+		protected void initExtractorMap() {
+			super.initExtractorMap();
+			registerExtractor(ServletException.class, throwable -> {
+				ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class);
+				return ((ServletException) throwable).getRootCause();
+			});
+		}
+	}
 }
 }

+ 72 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationResponseUtils.java

@@ -0,0 +1,72 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.oauth2.client.web;
+
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.util.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Utility methods for an OAuth 2.0 Authorization Response.
+ *
+ * @author Joe Grandja
+ * @since 5.1
+ * @see OAuth2AuthorizationResponse
+ */
+final class OAuth2AuthorizationResponseUtils {
+
+	private OAuth2AuthorizationResponseUtils() {
+	}
+
+	static boolean isAuthorizationResponse(HttpServletRequest request) {
+		return isAuthorizationResponseSuccess(request) || isAuthorizationResponseError(request);
+	}
+
+	static boolean isAuthorizationResponseSuccess(HttpServletRequest request) {
+		return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.CODE)) &&
+			StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE));
+	}
+
+	static boolean isAuthorizationResponseError(HttpServletRequest request) {
+		return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.ERROR)) &&
+			StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE));
+	}
+
+	static OAuth2AuthorizationResponse convert(HttpServletRequest request) {
+		String code = request.getParameter(OAuth2ParameterNames.CODE);
+		String errorCode = request.getParameter(OAuth2ParameterNames.ERROR);
+		String state = request.getParameter(OAuth2ParameterNames.STATE);
+		String redirectUri = request.getRequestURL().toString();
+
+		if (StringUtils.hasText(code)) {
+			return OAuth2AuthorizationResponse.success(code)
+				.redirectUri(redirectUri)
+				.state(state)
+				.build();
+		} else {
+			String errorDescription = request.getParameter(OAuth2ParameterNames.ERROR_DESCRIPTION);
+			String errorUri = request.getParameter(OAuth2ParameterNames.ERROR_URI);
+			return OAuth2AuthorizationResponse.error(errorCode)
+				.redirectUri(redirectUri)
+				.errorDescription(errorDescription)
+				.errorUri(errorUri)
+				.state(state)
+				.build();
+		}
+	}
+}

+ 3 - 38
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilter.java

@@ -35,7 +35,6 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
 import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
 import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
 import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.security.web.context.SecurityContextRepository;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
-import org.springframework.util.StringUtils;
 
 
 import javax.servlet.ServletException;
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequest;
@@ -134,22 +133,21 @@ public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProce
 	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
 	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
 			throws AuthenticationException, IOException, ServletException {
 			throws AuthenticationException, IOException, ServletException {
 
 
-		if (!this.authorizationResponseSuccess(request) && !this.authorizationResponseError(request)) {
+		if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(request)) {
 			OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
 			OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
 			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
 			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
 		}
 		}
 
 
-		OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.loadAuthorizationRequest(request);
+		OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request);
 		if (authorizationRequest == null) {
 		if (authorizationRequest == null) {
 			OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
 			OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
 			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
 			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
 		}
 		}
-		this.authorizationRequestRepository.removeAuthorizationRequest(request);
 
 
 		String registrationId = (String) authorizationRequest.getAdditionalParameters().get(OAuth2ParameterNames.REGISTRATION_ID);
 		String registrationId = (String) authorizationRequest.getAdditionalParameters().get(OAuth2ParameterNames.REGISTRATION_ID);
 		ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
 		ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
 
 
-		OAuth2AuthorizationResponse authorizationResponse = this.convert(request);
+		OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(request);
 
 
 		OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(
 		OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(
 				clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
 				clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
@@ -182,37 +180,4 @@ public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProce
 		Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null");
 		Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null");
 		this.authorizationRequestRepository = authorizationRequestRepository;
 		this.authorizationRequestRepository = authorizationRequestRepository;
 	}
 	}
-
-	private OAuth2AuthorizationResponse convert(HttpServletRequest request) {
-		String code = request.getParameter(OAuth2ParameterNames.CODE);
-		String errorCode = request.getParameter(OAuth2ParameterNames.ERROR);
-		String state = request.getParameter(OAuth2ParameterNames.STATE);
-		String redirectUri = request.getRequestURL().toString();
-
-		if (StringUtils.hasText(code)) {
-			return OAuth2AuthorizationResponse.success(code)
-				.redirectUri(redirectUri)
-				.state(state)
-				.build();
-		} else {
-			String errorDescription = request.getParameter(OAuth2ParameterNames.ERROR_DESCRIPTION);
-			String errorUri = request.getParameter(OAuth2ParameterNames.ERROR_URI);
-			return OAuth2AuthorizationResponse.error(errorCode)
-				.redirectUri(redirectUri)
-				.errorDescription(errorDescription)
-				.errorUri(errorUri)
-				.state(state)
-				.build();
-		}
-	}
-
-	private boolean authorizationResponseSuccess(HttpServletRequest request) {
-		return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.CODE)) &&
-			StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE));
-	}
-
-	private boolean authorizationResponseError(HttpServletRequest request) {
-		return StringUtils.hasText(request.getParameter(OAuth2ParameterNames.ERROR)) &&
-			StringUtils.hasText(request.getParameter(OAuth2ParameterNames.STATE));
-	}
 }
 }

+ 141 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationProviderTests.java

@@ -0,0 +1,141 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.oauth2.client.authentication;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+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.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
+
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link OAuth2AuthorizationCodeAuthenticationProvider}.
+ *
+ * @author Joe Grandja
+ */
+@PrepareForTest({ClientRegistration.class, OAuth2AuthorizationRequest.class,
+	OAuth2AuthorizationResponse.class, OAuth2AccessTokenResponse.class})
+@RunWith(PowerMockRunner.class)
+public class OAuth2AuthorizationCodeAuthenticationProviderTests {
+	private ClientRegistration clientRegistration;
+	private OAuth2AuthorizationRequest authorizationRequest;
+	private OAuth2AuthorizationResponse authorizationResponse;
+	private OAuth2AuthorizationExchange authorizationExchange;
+	private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
+	private OAuth2AuthorizationCodeAuthenticationProvider authenticationProvider;
+
+	@Before
+	@SuppressWarnings("unchecked")
+	public void setUp() throws Exception {
+		this.clientRegistration = mock(ClientRegistration.class);
+		this.authorizationRequest = mock(OAuth2AuthorizationRequest.class);
+		this.authorizationResponse = mock(OAuth2AuthorizationResponse.class);
+		this.authorizationExchange = new OAuth2AuthorizationExchange(this.authorizationRequest, this.authorizationResponse);
+		this.accessTokenResponseClient = mock(OAuth2AccessTokenResponseClient.class);
+		this.authenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(this.accessTokenResponseClient);
+
+		when(this.authorizationRequest.getState()).thenReturn("12345");
+		when(this.authorizationResponse.getState()).thenReturn("12345");
+		when(this.authorizationRequest.getRedirectUri()).thenReturn("http://example.com");
+		when(this.authorizationResponse.getRedirectUri()).thenReturn("http://example.com");
+	}
+
+	@Test
+	public void constructorWhenAccessTokenResponseClientIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationProvider(null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void supportsWhenTypeOAuth2AuthorizationCodeAuthenticationTokenThenReturnTrue() {
+		assertThat(this.authenticationProvider.supports(OAuth2AuthorizationCodeAuthenticationToken.class)).isTrue();
+	}
+
+	@Test
+	public void authenticateWhenAuthorizationErrorResponseThenThrowOAuth2AuthenticationException() {
+		when(this.authorizationResponse.statusError()).thenReturn(true);
+		when(this.authorizationResponse.getError()).thenReturn(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST));
+
+		assertThatThrownBy(() -> {
+			this.authenticationProvider.authenticate(
+					new OAuth2AuthorizationCodeAuthenticationToken(
+							this.clientRegistration, this.authorizationExchange));
+		}).isInstanceOf(OAuth2AuthenticationException.class).hasMessageContaining(OAuth2ErrorCodes.INVALID_REQUEST);
+	}
+
+	@Test
+	public void authenticateWhenAuthorizationResponseStateNotEqualAuthorizationRequestStateThenThrowOAuth2AuthenticationException() {
+		when(this.authorizationRequest.getState()).thenReturn("12345");
+		when(this.authorizationResponse.getState()).thenReturn("67890");
+
+		assertThatThrownBy(() -> {
+			this.authenticationProvider.authenticate(
+					new OAuth2AuthorizationCodeAuthenticationToken(
+							this.clientRegistration, this.authorizationExchange));
+		}).isInstanceOf(OAuth2AuthenticationException.class).hasMessageContaining("invalid_state_parameter");
+	}
+
+	@Test
+	public void authenticateWhenAuthorizationResponseRedirectUriNotEqualAuthorizationRequestRedirectUriThenThrowOAuth2AuthenticationException() {
+		when(this.authorizationRequest.getRedirectUri()).thenReturn("http://example.com");
+		when(this.authorizationResponse.getRedirectUri()).thenReturn("http://example2.com");
+
+		assertThatThrownBy(() -> {
+			this.authenticationProvider.authenticate(
+					new OAuth2AuthorizationCodeAuthenticationToken(
+							this.clientRegistration, this.authorizationExchange));
+		}).isInstanceOf(OAuth2AuthenticationException.class).hasMessageContaining("invalid_redirect_uri_parameter");
+	}
+
+	@Test
+	public void authenticateWhenAuthorizationSuccessResponseThenExchangedForAccessToken() {
+		OAuth2AccessToken accessToken = mock(OAuth2AccessToken.class);
+		OAuth2AccessTokenResponse accessTokenResponse = mock(OAuth2AccessTokenResponse.class);
+		when(accessTokenResponse.getAccessToken()).thenReturn(accessToken);
+		when(this.accessTokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+
+		OAuth2AuthorizationCodeAuthenticationToken authenticationResult =
+			(OAuth2AuthorizationCodeAuthenticationToken) this.authenticationProvider.authenticate(
+				new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, this.authorizationExchange));
+
+		assertThat(authenticationResult.isAuthenticated()).isTrue();
+		assertThat(authenticationResult.getPrincipal()).isEqualTo(this.clientRegistration.getClientId());
+		assertThat(authenticationResult.getCredentials()).isEqualTo(accessToken.getTokenValue());
+		assertThat(authenticationResult.getAuthorities()).isEqualTo(Collections.emptyList());
+		assertThat(authenticationResult.getClientRegistration()).isEqualTo(this.clientRegistration);
+		assertThat(authenticationResult.getAuthorizationExchange()).isEqualTo(this.authorizationExchange);
+		assertThat(authenticationResult.getAccessToken()).isEqualTo(accessToken);
+	}
+}

+ 115 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/authentication/OAuth2AuthorizationCodeAuthenticationTokenTests.java

@@ -0,0 +1,115 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.oauth2.client.authentication;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
+
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link OAuth2AuthorizationCodeAuthenticationToken}.
+ *
+ * @author Joe Grandja
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ClientRegistration.class, OAuth2AuthorizationExchange.class, OAuth2AuthorizationResponse.class})
+public class OAuth2AuthorizationCodeAuthenticationTokenTests {
+	private ClientRegistration clientRegistration;
+	private OAuth2AuthorizationExchange authorizationExchange;
+	private OAuth2AccessToken accessToken;
+
+	@Before
+	public void setUp() {
+		this.clientRegistration = mock(ClientRegistration.class);
+		this.authorizationExchange = mock(OAuth2AuthorizationExchange.class);
+		this.accessToken = mock(OAuth2AccessToken.class);
+	}
+
+	@Test
+	public void constructorAuthorizationRequestResponseWhenClientRegistrationIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(null, this.authorizationExchange))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void constructorAuthorizationRequestResponseWhenAuthorizationExchangeIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void constructorAuthorizationRequestResponseWhenAllParametersProvidedAndValidThenCreated() {
+		OAuth2AuthorizationResponse authorizationResponse = mock(OAuth2AuthorizationResponse.class);
+		when(authorizationResponse.getCode()).thenReturn("code");
+		when(this.authorizationExchange.getAuthorizationResponse()).thenReturn(authorizationResponse);
+
+		OAuth2AuthorizationCodeAuthenticationToken authentication =
+			new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, this.authorizationExchange);
+
+		assertThat(authentication.getPrincipal()).isEqualTo(this.clientRegistration.getClientId());
+		assertThat(authentication.getCredentials()).isEqualTo(this.authorizationExchange.getAuthorizationResponse().getCode());
+		assertThat(authentication.getAuthorities()).isEqualTo(Collections.emptyList());
+		assertThat(authentication.getClientRegistration()).isEqualTo(this.clientRegistration);
+		assertThat(authentication.getAuthorizationExchange()).isEqualTo(this.authorizationExchange);
+		assertThat(authentication.getAccessToken()).isNull();
+		assertThat(authentication.isAuthenticated()).isEqualTo(false);
+	}
+
+	@Test
+	public void constructorTokenRequestResponseWhenClientRegistrationIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(null, this.authorizationExchange, this.accessToken))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void constructorTokenRequestResponseWhenAuthorizationExchangeIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, null, this.accessToken))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void constructorTokenRequestResponseWhenAccessTokenIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationCodeAuthenticationToken(this.clientRegistration, this.authorizationExchange, null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void constructorTokenRequestResponseWhenAllParametersProvidedAndValidThenCreated() {
+		OAuth2AuthorizationCodeAuthenticationToken authentication = new OAuth2AuthorizationCodeAuthenticationToken(
+			this.clientRegistration, this.authorizationExchange, this.accessToken);
+
+		assertThat(authentication.getPrincipal()).isEqualTo(this.clientRegistration.getClientId());
+		assertThat(authentication.getCredentials()).isEqualTo(this.accessToken.getTokenValue());
+		assertThat(authentication.getAuthorities()).isEqualTo(Collections.emptyList());
+		assertThat(authentication.getClientRegistration()).isEqualTo(this.clientRegistration);
+		assertThat(authentication.getAuthorizationExchange()).isEqualTo(this.authorizationExchange);
+		assertThat(authentication.getAccessToken()).isEqualTo(this.accessToken);
+		assertThat(authentication.isAuthenticated()).isEqualTo(true);
+	}
+}

+ 304 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationCodeGrantFilterTests.java

@@ -0,0 +1,304 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.oauth2.client.web;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.TestingAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
+import org.springframework.security.oauth2.core.AuthorizationGrantType;
+import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
+import org.springframework.security.web.savedrequest.RequestCache;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.*;
+
+/**
+ * Tests for {@link OAuth2AuthorizationCodeGrantFilter}.
+ *
+ * @author Joe Grandja
+ */
+@PowerMockIgnore("javax.security.*")
+@PrepareForTest({OAuth2AuthorizationRequest.class, OAuth2AuthorizationExchange.class, OAuth2AuthorizationCodeGrantFilter.class})
+@RunWith(PowerMockRunner.class)
+public class OAuth2AuthorizationCodeGrantFilterTests {
+	private ClientRegistration registration1;
+	private String principalName1 = "principal-1";
+	private ClientRegistrationRepository clientRegistrationRepository;
+	private OAuth2AuthorizedClientService authorizedClientService;
+	private AuthenticationManager authenticationManager;
+	private AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository;
+	private OAuth2AuthorizationCodeGrantFilter filter;
+
+	@Before
+	public void setUp() {
+		this.registration1 = ClientRegistration.withRegistrationId("registration-1")
+			.clientId("client-1")
+			.clientSecret("secret")
+			.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
+			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+			.redirectUriTemplate("{baseUrl}/callback/client-1")
+			.scope("user")
+			.authorizationUri("https://provider.com/oauth2/authorize")
+			.tokenUri("https://provider.com/oauth2/token")
+			.userInfoUri("https://provider.com/oauth2/user")
+			.userNameAttributeName("id")
+			.clientName("client-1")
+			.build();
+		this.clientRegistrationRepository = new InMemoryClientRegistrationRepository(this.registration1);
+		this.authorizedClientService = new InMemoryOAuth2AuthorizedClientService(this.clientRegistrationRepository);
+		this.authorizationRequestRepository = new HttpSessionOAuth2AuthorizationRequestRepository();
+		this.authenticationManager = mock(AuthenticationManager.class);
+		this.filter = spy(new OAuth2AuthorizationCodeGrantFilter(
+			this.clientRegistrationRepository, this.authorizedClientService, this.authenticationManager));
+		this.filter.setAuthorizationRequestRepository(this.authorizationRequestRepository);
+
+		SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
+		securityContext.setAuthentication(new TestingAuthenticationToken(this.principalName1, "password"));
+		SecurityContextHolder.setContext(securityContext);
+	}
+
+	@Test
+	public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(null, this.authorizedClientService, this.authenticationManager))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void constructorWhenAuthorizedClientServiceIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(this.clientRegistrationRepository, null, this.authenticationManager))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void constructorWhenAuthenticationManagerIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> new OAuth2AuthorizationCodeGrantFilter(this.clientRegistrationRepository, this.authorizedClientService, null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void setAuthorizationRequestRepositoryWhenAuthorizationRequestRepositoryIsNullThenThrowIllegalArgumentException() {
+		assertThatThrownBy(() -> this.filter.setAuthorizationRequestRepository(null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void doFilterWhenNotAuthorizationResponseThenNotProcessed() throws Exception {
+		String requestUri = "/path";
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+		request.setServletPath(requestUri);
+		// NOTE: A valid Authorization Response contains either a 'code' or 'error' parameter.
+
+		HttpServletResponse response = mock(HttpServletResponse.class);
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
+	}
+
+	@Test
+	public void doFilterWhenAuthorizationRequestNotFoundThenNotProcessed() throws Exception {
+		String requestUri = "/path";
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+		request.setServletPath(requestUri);
+		request.addParameter(OAuth2ParameterNames.CODE, "code");
+		request.addParameter(OAuth2ParameterNames.STATE, "state");
+
+		HttpServletResponse response = mock(HttpServletResponse.class);
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
+	}
+
+	@Test
+	public void doFilterWhenAuthorizationResponseUrlDoesNotMatchAuthorizationRequestRedirectUriThenNotProcessed() throws Exception {
+		String requestUri = "/callback/client-1";
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+		request.setServletPath(requestUri);
+		request.addParameter(OAuth2ParameterNames.CODE, "code");
+		request.addParameter(OAuth2ParameterNames.STATE, "state");
+
+		HttpServletResponse response = mock(HttpServletResponse.class);
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.setUpAuthorizationRequest(request, response, this.registration1);
+		request.setRequestURI(requestUri + "-no-match");
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
+	}
+
+	@Test
+	public void doFilterWhenAuthorizationResponseValidThenAuthorizationRequestRemoved() throws Exception {
+		String requestUri = "/callback/client-1";
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+		request.setServletPath(requestUri);
+		request.addParameter(OAuth2ParameterNames.CODE, "code");
+		request.addParameter(OAuth2ParameterNames.STATE, "state");
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.setUpAuthorizationRequest(request, response, this.registration1);
+		this.setUpAuthenticationResult(this.registration1);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		assertThat(this.authorizationRequestRepository.loadAuthorizationRequest(request)).isNull();
+	}
+
+	@Test
+	public void doFilterWhenAuthenticationFailsThenHandleOAuth2AuthenticationException() throws Exception {
+		String requestUri = "/callback/client-1";
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+		request.setServletPath(requestUri);
+		request.addParameter(OAuth2ParameterNames.CODE, "code");
+		request.addParameter(OAuth2ParameterNames.STATE, "state");
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.setUpAuthorizationRequest(request, response, this.registration1);
+		OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_GRANT);
+		when(this.authenticationManager.authenticate(any(Authentication.class)))
+			.thenThrow(new OAuth2AuthenticationException(error, error.toString()));
+
+		this.filter.doFilter(request, response, filterChain);
+
+		assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1?error=invalid_grant");
+	}
+
+	@Test
+	public void doFilterWhenAuthorizationResponseSuccessThenAuthorizedClientSaved() throws Exception {
+		String requestUri = "/callback/client-1";
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+		request.setServletPath(requestUri);
+		request.addParameter(OAuth2ParameterNames.CODE, "code");
+		request.addParameter(OAuth2ParameterNames.STATE, "state");
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.setUpAuthorizationRequest(request, response, this.registration1);
+		this.setUpAuthenticationResult(this.registration1);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
+			this.registration1.getRegistrationId(), this.principalName1);
+		assertThat(authorizedClient).isNotNull();
+		assertThat(authorizedClient.getClientRegistration()).isEqualTo(this.registration1);
+		assertThat(authorizedClient.getPrincipalName()).isEqualTo(this.principalName1);
+		assertThat(authorizedClient.getAccessToken()).isNotNull();
+	}
+
+	@Test
+	public void doFilterWhenAuthorizationResponseSuccessThenRedirected() throws Exception {
+		String requestUri = "/callback/client-1";
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+		request.setServletPath(requestUri);
+		request.addParameter(OAuth2ParameterNames.CODE, "code");
+		request.addParameter(OAuth2ParameterNames.STATE, "state");
+
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.setUpAuthorizationRequest(request, response, this.registration1);
+		this.setUpAuthenticationResult(this.registration1);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/callback/client-1");
+	}
+
+	@Test
+	public void doFilterWhenAuthorizationResponseSuccessHasSavedRequestThenRedirectedToSavedRequest() throws Exception {
+		String requestUri = "/saved-request";
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+		request.setServletPath(requestUri);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		RequestCache requestCache = new HttpSessionRequestCache();
+		requestCache.saveRequest(request, response);
+
+		requestUri = "/callback/client-1";
+		request.setRequestURI(requestUri);
+		request.addParameter(OAuth2ParameterNames.CODE, "code");
+		request.addParameter(OAuth2ParameterNames.STATE, "state");
+
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.setUpAuthorizationRequest(request, response, this.registration1);
+		this.setUpAuthenticationResult(this.registration1);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		assertThat(response.getRedirectedUrl()).isEqualTo("http://localhost/saved-request");
+	}
+
+	private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response,
+											ClientRegistration registration) {
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
+		OAuth2AuthorizationRequest authorizationRequest = mock(OAuth2AuthorizationRequest.class);
+		when(authorizationRequest.getAdditionalParameters()).thenReturn(additionalParameters);
+		when(authorizationRequest.getRedirectUri()).thenReturn(request.getRequestURL().toString());
+		when(authorizationRequest.getState()).thenReturn("state");
+		this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
+	}
+
+	private void setUpAuthenticationResult(ClientRegistration registration) {
+		OAuth2AuthorizationCodeAuthenticationToken authentication = mock(OAuth2AuthorizationCodeAuthenticationToken.class);
+		when(authentication.getClientRegistration()).thenReturn(registration);
+		when(authentication.getAuthorizationExchange()).thenReturn(mock(OAuth2AuthorizationExchange.class));
+		when(authentication.getAccessToken()).thenReturn(mock(OAuth2AccessToken.class));
+		when(this.authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication);
+	}
+}

+ 119 - 12
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2AuthorizationRequestRedirectFilterTests.java

@@ -21,18 +21,25 @@ import org.mockito.ArgumentCaptor;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.HttpStatus;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException;
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
 import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
 import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.AuthorizationGrantType;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.security.web.savedrequest.SavedRequest;
 
 
 import javax.servlet.FilterChain;
 import javax.servlet.FilterChain;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import java.util.Collections;
 
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.*;
 import static org.mockito.Mockito.*;
 
 
 /**
 /**
@@ -54,7 +61,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
 			.clientSecret("secret")
 			.clientSecret("secret")
 			.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
 			.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
 			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
 			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
-			.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
+			.redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}")
 			.scope("user")
 			.scope("user")
 			.authorizationUri("https://provider.com/oauth2/authorize")
 			.authorizationUri("https://provider.com/oauth2/authorize")
 			.tokenUri("https://provider.com/oauth2/token")
 			.tokenUri("https://provider.com/oauth2/token")
@@ -67,7 +74,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
 			.clientSecret("secret")
 			.clientSecret("secret")
 			.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
 			.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
 			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
 			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
-			.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
+			.redirectUriTemplate("{baseUrl}/{action}/oauth2/code/{registrationId}")
 			.scope("openid", "profile", "email")
 			.scope("openid", "profile", "email")
 			.authorizationUri("https://provider.com/oauth2/authorize")
 			.authorizationUri("https://provider.com/oauth2/authorize")
 			.tokenUri("https://provider.com/oauth2/token")
 			.tokenUri("https://provider.com/oauth2/token")
@@ -78,7 +85,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
 		this.registration3 = ClientRegistration.withRegistrationId("registration-3")
 		this.registration3 = ClientRegistration.withRegistrationId("registration-3")
 			.clientId("client-3")
 			.clientId("client-3")
 			.authorizationGrantType(AuthorizationGrantType.IMPLICIT)
 			.authorizationGrantType(AuthorizationGrantType.IMPLICIT)
-			.redirectUriTemplate("{baseUrl}/login/oauth2/implicit/{registrationId}")
+			.redirectUriTemplate("{baseUrl}/authorize/oauth2/implicit/{registrationId}")
 			.scope("openid", "profile", "email")
 			.scope("openid", "profile", "email")
 			.authorizationUri("https://provider.com/oauth2/authorize")
 			.authorizationUri("https://provider.com/oauth2/authorize")
 			.tokenUri("https://provider.com/oauth2/token")
 			.tokenUri("https://provider.com/oauth2/token")
@@ -90,19 +97,22 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
 		this.filter = new OAuth2AuthorizationRequestRedirectFilter(this.clientRegistrationRepository);
 		this.filter = new OAuth2AuthorizationRequestRedirectFilter(this.clientRegistrationRepository);
 	}
 	}
 
 
-	@Test(expected = IllegalArgumentException.class)
+	@Test
 	public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() {
 	public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() {
-		new OAuth2AuthorizationRequestRedirectFilter(null);
+		assertThatThrownBy(() -> new OAuth2AuthorizationRequestRedirectFilter(null))
+				.isInstanceOf(IllegalArgumentException.class);
 	}
 	}
 
 
-	@Test(expected = IllegalArgumentException.class)
+	@Test
 	public void constructorWhenAuthorizationRequestBaseUriIsNullThenThrowIllegalArgumentException() {
 	public void constructorWhenAuthorizationRequestBaseUriIsNullThenThrowIllegalArgumentException() {
-		new OAuth2AuthorizationRequestRedirectFilter(this.clientRegistrationRepository, null);
+		assertThatThrownBy(() -> new OAuth2AuthorizationRequestRedirectFilter(this.clientRegistrationRepository, null))
+				.isInstanceOf(IllegalArgumentException.class);
 	}
 	}
 
 
-	@Test(expected = IllegalArgumentException.class)
+	@Test
 	public void setAuthorizationRequestRepositoryWhenAuthorizationRequestRepositoryIsNullThenThrowIllegalArgumentException() {
 	public void setAuthorizationRequestRepositoryWhenAuthorizationRequestRepositoryIsNullThenThrowIllegalArgumentException() {
-		this.filter.setAuthorizationRequestRepository(null);
+		assertThatThrownBy(() -> this.filter.setAuthorizationRequestRepository(null))
+				.isInstanceOf(IllegalArgumentException.class);
 	}
 	}
 
 
 	@Test
 	@Test
@@ -136,7 +146,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
 	}
 	}
 
 
 	@Test
 	@Test
-	public void doFilterWhenAuthorizationRequestAuthorizationCodeGrantThenRedirectForAuthorization() throws Exception {
+	public void doFilterWhenAuthorizationRequestOAuth2LoginThenRedirectForAuthorization() throws Exception {
 		String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI +
 		String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI +
 			"/" + this.registration1.getRegistrationId();
 			"/" + this.registration1.getRegistrationId();
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
@@ -152,7 +162,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
 	}
 	}
 
 
 	@Test
 	@Test
-	public void doFilterWhenAuthorizationRequestAuthorizationCodeGrantThenAuthorizationRequestSaved() throws Exception {
+	public void doFilterWhenAuthorizationRequestOAuth2LoginThenAuthorizationRequestSaved() throws Exception {
 		String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI +
 		String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI +
 			"/" + this.registration2.getRegistrationId();
 			"/" + this.registration2.getRegistrationId();
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
@@ -184,7 +194,7 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
 
 
 		verifyZeroInteractions(filterChain);
 		verifyZeroInteractions(filterChain);
 
 
-		assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=token&client_id=client-3&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/login/oauth2/implicit/registration-3");
+		assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=token&client_id=client-3&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/implicit/registration-3");
 	}
 	}
 
 
 	@Test
 	@Test
@@ -292,4 +302,101 @@ public class OAuth2AuthorizationRequestRedirectFilterTests {
 
 
 		assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=https://example.com/login/oauth2/code/registration-1");
 		assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=https://example.com/login/oauth2/code/registration-1");
 	}
 	}
+
+	@Test
+	public void doFilterWhenNotAuthorizationRequestAndClientAuthorizationRequiredExceptionThrownThenRedirectForAuthorization() throws Exception {
+		String requestUri = "/path";
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+		request.setServletPath(requestUri);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		doThrow(new ClientAuthorizationRequiredException(this.registration1.getRegistrationId()))
+			.when(filterChain).doFilter(any(ServletRequest.class), any(ServletResponse.class));
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
+
+		assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/code/registration-1");
+
+		HttpSession session = request.getSession(false);
+		assertThat(session).isNotNull();
+		boolean requestSaved = false;
+		for (String attrName : Collections.list(session.getAttributeNames())) {
+			if (SavedRequest.class.isAssignableFrom(session.getAttribute(attrName).getClass())) {
+				requestSaved = true;
+				break;
+			}
+		}
+		assertThat(requestSaved).isTrue();
+	}
+
+	@Test
+	public void doFilterWhenNotAuthorizationRequestAndClientAuthorizationRequiredExceptionThrownThenRedirectUriIsAuthorize() throws Exception {
+		String requestUri = "/path";
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+		request.setServletPath(requestUri);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		doThrow(new ClientAuthorizationRequiredException(this.registration1.getRegistrationId()))
+				.when(filterChain).doFilter(any(ServletRequest.class), any(ServletResponse.class));
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
+
+		assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/code/registration-1");
+	}
+
+	@Test
+	public void doFilterWhenAuthorizationRequestOAuth2LoginThenRedirectUriIsLogin() throws Exception {
+		String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI +
+				"/" + this.registration2.getRegistrationId();
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+		request.setServletPath(requestUri);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verifyZeroInteractions(filterChain);
+
+		assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-2&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/login/oauth2/code/registration-2");
+	}
+
+	@Test
+	public void doFilterWhenAuthorizationRequestHasActionParameterAuthorizeThenRedirectUriIsAuthorize() throws Exception {
+		String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI +
+				"/" + this.registration1.getRegistrationId();
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+		request.addParameter("action", "authorize");
+		request.setServletPath(requestUri);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verifyZeroInteractions(filterChain);
+
+		assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-1&scope=user&state=.{15,}&redirect_uri=http://localhost/authorize/oauth2/code/registration-1");
+	}
+
+	@Test
+	public void doFilterWhenAuthorizationRequestHasActionParameterLoginThenRedirectUriIsLogin() throws Exception {
+		String requestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI +
+				"/" + this.registration2.getRegistrationId();
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
+		request.addParameter("action", "login");
+		request.setServletPath(requestUri);
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		FilterChain filterChain = mock(FilterChain.class);
+
+		this.filter.doFilter(request, response, filterChain);
+
+		verifyZeroInteractions(filterChain);
+
+		assertThat(response.getRedirectedUrl()).matches("https://provider.com/oauth2/authorize\\?response_type=code&client_id=client-2&scope=openid%20profile%20email&state=.{15,}&redirect_uri=http://localhost/login/oauth2/code/registration-2");
+	}
 }
 }

+ 13 - 30
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/web/OAuth2LoginAuthenticationFilterTests.java

@@ -19,7 +19,6 @@ import org.junit.Before;
 import org.junit.Test;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentCaptor;
-import org.powermock.api.mockito.PowerMockito;
 import org.powermock.core.classloader.annotations.PowerMockIgnore;
 import org.powermock.core.classloader.annotations.PowerMockIgnore;
 import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.powermock.modules.junit4.PowerMockRunner;
 import org.powermock.modules.junit4.PowerMockRunner;
@@ -54,9 +53,8 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Map;
 
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.fail;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.Mockito.*;
 import static org.mockito.Mockito.*;
-import static org.powermock.api.mockito.PowerMockito.verifyPrivate;
 
 
 /**
 /**
  * Tests for {@link OAuth2LoginAuthenticationFilter}.
  * Tests for {@link OAuth2LoginAuthenticationFilter}.
@@ -118,24 +116,28 @@ public class OAuth2LoginAuthenticationFilterTests {
 		this.filter.setAuthenticationManager(this.authenticationManager);
 		this.filter.setAuthenticationManager(this.authenticationManager);
 	}
 	}
 
 
-	@Test(expected = IllegalArgumentException.class)
+	@Test
 	public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() {
 	public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() {
-		new OAuth2LoginAuthenticationFilter(null, this.authorizedClientService);
+		assertThatThrownBy(() -> new OAuth2LoginAuthenticationFilter(null, this.authorizedClientService))
+				.isInstanceOf(IllegalArgumentException.class);
 	}
 	}
 
 
-	@Test(expected = IllegalArgumentException.class)
+	@Test
 	public void constructorWhenAuthorizedClientServiceIsNullThenThrowIllegalArgumentException() {
 	public void constructorWhenAuthorizedClientServiceIsNullThenThrowIllegalArgumentException() {
-		new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, null);
+		assertThatThrownBy(() -> new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, null))
+				.isInstanceOf(IllegalArgumentException.class);
 	}
 	}
 
 
-	@Test(expected = IllegalArgumentException.class)
+	@Test
 	public void constructorWhenFilterProcessesUrlIsNullThenThrowIllegalArgumentException() {
 	public void constructorWhenFilterProcessesUrlIsNullThenThrowIllegalArgumentException() {
-		new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, this.authorizedClientService, null);
+		assertThatThrownBy(() -> new OAuth2LoginAuthenticationFilter(this.clientRegistrationRepository, this.authorizedClientService, null))
+				.isInstanceOf(IllegalArgumentException.class);
 	}
 	}
 
 
-	@Test(expected = IllegalArgumentException.class)
+	@Test
 	public void setAuthorizationRequestRepositoryWhenAuthorizationRequestRepositoryIsNullThenThrowIllegalArgumentException() {
 	public void setAuthorizationRequestRepositoryWhenAuthorizationRequestRepositoryIsNullThenThrowIllegalArgumentException() {
-		this.filter.setAuthorizationRequestRepository(null);
+		assertThatThrownBy(() -> this.filter.setAuthorizationRequestRepository(null))
+				.isInstanceOf(IllegalArgumentException.class);
 	}
 	}
 
 
 	@Test
 	@Test
@@ -268,25 +270,6 @@ public class OAuth2LoginAuthenticationFilterTests {
 		verify(this.filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
 		verify(this.filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
 	}
 	}
 
 
-	@Test
-	public void attemptAuthenticationWhenAuthorizationRequestIsNullThenAuthorizationResponseNotCreated() throws Exception {
-		OAuth2LoginAuthenticationFilter filter = PowerMockito.spy(new OAuth2LoginAuthenticationFilter(
-			this.clientRegistrationRepository, this.authorizedClientService));
-
-		MockHttpServletRequest request = new MockHttpServletRequest();
-		request.addParameter(OAuth2ParameterNames.CODE, "code");
-		request.addParameter(OAuth2ParameterNames.STATE, "state");
-
-		MockHttpServletResponse response = new MockHttpServletResponse();
-
-		try {
-			filter.attemptAuthentication(request, response);
-			fail();
-		} catch (OAuth2AuthenticationException ex) {
-			verifyPrivate(filter, never()).invoke("convert", any(HttpServletRequest.class));
-		}
-	}
-
 	private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response,
 	private void setUpAuthorizationRequest(HttpServletRequest request, HttpServletResponse response,
 											ClientRegistration registration, String state) {
 											ClientRegistration registration, String state) {
 		OAuth2AuthorizationRequest authorizationRequest = mock(OAuth2AuthorizationRequest.class);
 		OAuth2AuthorizationRequest authorizationRequest = mock(OAuth2AuthorizationRequest.class);

+ 64 - 0
samples/boot/oauth2/authcodegrant/README.adoc

@@ -0,0 +1,64 @@
+= OAuth 2.0 Authorization Code Grant Sample
+
+== GitHub Repositories
+
+This guide provides instructions on setting up the sample application, which leverages the OAuth 2.0 Authorization Code Grant, and displays a list of public GitHub repositories that are accessible to the authenticated user.
+
+This includes repositories owned by the authenticated user, repositories where the authenticated user is a collaborator, and repositories that the authenticated user has access to through an organization membership.
+
+The following sections provide detailed steps for setting up the sample and covers the following topics:
+
+* <<github-register-application,Register OAuth application>>
+* <<github-application-config,Configure application.yml>>
+* <<github-boot-application,Boot up the application>>
+
+[[github-register-application]]
+=== Register OAuth application
+
+To use GitHub's OAuth 2.0 authorization system, you must https://github.com/settings/applications/new[Register a new OAuth application].
+
+When registering the OAuth application, ensure the *Authorization callback URL* is set to `http://localhost:8080/github-repos`.
+
+The Authorization callback URL (redirect URI) is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with GitHub and have granted access to the OAuth application on the _Authorize application_ page.
+
+[[github-application-config]]
+=== Configure application.yml
+
+Now that you have a new OAuth application with GitHub, you need to configure the sample to use the OAuth application for the _authorization code grant flow_.
+To do so:
+
+. Go to `application.yml` and set the following configuration:
++
+[source,yaml]
+----
+spring:
+  security:
+    oauth2:
+      client:
+        registration:	<1>
+          github:       <2>
+            client-id: github-client-id
+            client-secret: github-client-secret
+            scope: public_repo
+            redirect-uri-template: "{baseUrl}/github-repos"
+            client-name: GitHub Repositories
+----
++
+.OAuth Client properties
+====
+<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties.
+<2> Following the base property prefix is the ID for the `ClientRegistration`, which is github.
+====
+
+. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier.
+
+[[github-boot-application]]
+=== Boot up the application
+
+Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`.
+You are then redirected to the default _auto-generated_ form login page.
+Log in using *'user'* (username) and *'password'* (password) and then you'll be redirected to GitHub for authentication.
+
+After authenticating with your GitHub credentials, the next page presented to you is "Authorize application".
+This page will ask you to *Authorize* the application you created in the previous step.
+Click _Authorize application_ to allow the OAuth application to access and display your public repository information.

+ 16 - 0
samples/boot/oauth2/authcodegrant/spring-security-samples-boot-oauth2-authcodegrant.gradle

@@ -0,0 +1,16 @@
+apply plugin: 'io.spring.convention.spring-sample-boot'
+
+ext['thymeleaf.version'] = '3.0.9.RELEASE'
+
+dependencies {
+	compile project(':spring-security-config')
+	compile project(':spring-security-oauth2-client')
+	compile 'org.springframework:spring-webflux'
+	compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
+	compile 'org.springframework.boot:spring-boot-starter-web'
+	compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
+	compile 'io.projectreactor.ipc:reactor-netty'
+
+	testCompile project(':spring-security-test')
+	testCompile 'org.springframework.boot:spring-boot-starter-test'
+}

+ 165 - 0
samples/boot/oauth2/authcodegrant/src/integration-test/java/org/springframework/security/samples/OAuth2AuthorizationCodeGrantApplicationTests.java

@@ -0,0 +1,165 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.samples;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.mock.web.MockHttpSession;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
+import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+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.test.annotation.DirtiesContext;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * Integration tests for the OAuth 2.0 client filters {@link OAuth2AuthorizationRequestRedirectFilter}
+ * and {@link OAuth2AuthorizationCodeGrantFilter}. These filters work together to realize
+ * the OAuth 2.0 Authorization Code Grant flow.
+ *
+ * @author Joe Grandja
+ * @since 5.1
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@AutoConfigureMockMvc
+public class OAuth2AuthorizationCodeGrantApplicationTests {
+	@Autowired
+	private ClientRegistrationRepository clientRegistrationRepository;
+
+	@Autowired
+	private OAuth2AuthorizedClientService authorizedClientService;
+
+	@Autowired
+	private MockMvc mockMvc;
+
+	@Test
+	public void requestWhenClientNotAuthorizedThenRedirectForAuthorization() throws Exception {
+		MvcResult mvcResult = this.mockMvc.perform(get("/repos").with(user("user")))
+			.andExpect(status().is3xxRedirection())
+			.andReturn();
+		assertThat(mvcResult.getResponse().getRedirectedUrl()).matches("https://github.com/login/oauth/authorize\\?response_type=code&client_id=your-app-client-id&scope=public_repo&state=.{15,}&redirect_uri=http://localhost/github-repos");
+	}
+
+	@Test
+	@DirtiesContext
+	public void requestWhenClientGrantedAuthorizationThenAuthorizedClientSaved() throws Exception {
+		// Setup the Authorization Request in the session
+		ClientRegistration registration = this.clientRegistrationRepository.findByRegistrationId("github");
+		Map<String, Object> additionalParameters = new HashMap<>();
+		additionalParameters.put(OAuth2ParameterNames.REGISTRATION_ID, registration.getRegistrationId());
+		OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
+			.authorizationUri(registration.getProviderDetails().getAuthorizationUri())
+			.clientId(registration.getClientId())
+			.redirectUri("http://localhost/github-repos")
+			.scopes(registration.getScopes())
+			.state("state")
+			.additionalParameters(additionalParameters)
+			.build();
+
+		AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository =
+				new HttpSessionOAuth2AuthorizationRequestRepository();
+		MockHttpServletRequest request = new MockHttpServletRequest();
+		MockHttpServletResponse response = new MockHttpServletResponse();
+		authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
+
+		MockHttpSession session = (MockHttpSession) request.getSession();
+
+		String principalName = "user";
+
+		// Authorization Response
+		this.mockMvc.perform(get("/github-repos")
+			.param(OAuth2ParameterNames.CODE, "code")
+			.param(OAuth2ParameterNames.STATE, "state")
+			.with(user(principalName))
+			.session(session))
+			.andExpect(status().is3xxRedirection())
+			.andExpect(redirectedUrl("http://localhost/github-repos"));
+
+		OAuth2AuthorizedClient authorizedClient = this.authorizedClientService.loadAuthorizedClient(
+			registration.getRegistrationId(), principalName);
+		assertThat(authorizedClient).isNotNull();
+	}
+
+	@EnableWebSecurity
+	static class OAuth2ClientConfig extends WebSecurityConfigurerAdapter {
+		// @formatter:off
+		@Override
+		protected void configure(HttpSecurity http) throws Exception {
+			http
+				.authorizeRequests()
+					.anyRequest().authenticated()
+					.and()
+				.oauth2()
+					.client()
+						.authorizationCodeGrant()
+							.tokenEndpoint()
+								.accessTokenResponseClient(this.accessTokenResponseClient());
+		}
+		// @formatter:on
+
+		private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
+			OAuth2AccessTokenResponse accessTokenResponse = OAuth2AccessTokenResponse.withToken("access-token-1234")
+				.tokenType(OAuth2AccessToken.TokenType.BEARER)
+				.expiresIn(60 * 1000)
+				.build();
+			OAuth2AccessTokenResponseClient tokenResponseClient = mock(OAuth2AccessTokenResponseClient.class);
+			when(tokenResponseClient.getTokenResponse(any())).thenReturn(accessTokenResponse);
+			return tokenResponseClient;
+		}
+	}
+
+	@SpringBootConfiguration
+	@EnableAutoConfiguration
+	@ComponentScan(basePackages = "sample.web")
+	public static class SpringBootApplicationTestConfig {
+	}
+}

+ 30 - 0
samples/boot/oauth2/authcodegrant/src/main/java/sample/OAuth2AuthorizationCodeGrantApplication.java

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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 sample;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author Joe Grandja
+ */
+@SpringBootApplication
+public class OAuth2AuthorizationCodeGrantApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(OAuth2AuthorizationCodeGrantApplication.class, args);
+	}
+}

+ 55 - 0
samples/boot/oauth2/authcodegrant/src/main/java/sample/config/SecurityConfig.java

@@ -0,0 +1,55 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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 sample.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+
+/**
+ * @author Joe Grandja
+ */
+@EnableWebSecurity
+public class SecurityConfig extends WebSecurityConfigurerAdapter {
+
+	@Override
+	protected void configure(HttpSecurity http) throws Exception {
+		http
+			.authorizeRequests()
+				.anyRequest().authenticated()
+				.and()
+			.formLogin()
+				.and()
+			.oauth2()
+				.client()
+					.authorizationCodeGrant();
+	}
+
+	@Bean
+	public UserDetailsService userDetailsService() {
+		UserDetails userDetails = User.withDefaultPasswordEncoder()
+			.username("user")
+			.password("password")
+			.roles("USER")
+			.build();
+		return new InMemoryUserDetailsManager(userDetails);
+	}
+}

+ 81 - 0
samples/boot/oauth2/authcodegrant/src/main/java/sample/web/MainController.java

@@ -0,0 +1,81 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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 sample.web;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.client.ClientAuthorizationRequiredException;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
+import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.reactive.function.client.ClientRequest;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+
+/**
+ * @author Joe Grandja
+ */
+@Controller
+public class MainController {
+	@Autowired
+	private OAuth2AuthorizedClientService authorizedClientService;
+
+	@GetMapping("/")
+	public String index() {
+		return "redirect:/repos";
+	}
+
+	@GetMapping("/repos")
+	public String gitHubRepos(Model model, Authentication authentication) {
+		String registrationId = "github";
+
+		OAuth2AuthorizedClient authorizedClient =
+			this.authorizedClientService.loadAuthorizedClient(
+				registrationId, authentication.getName());
+		if (authorizedClient == null) {
+			throw new ClientAuthorizationRequiredException(registrationId);
+		}
+
+		String endpointUri = "https://api.github.com/user/repos";
+		List repos = WebClient.builder()
+			.filter(oauth2Credentials(authorizedClient))
+			.build()
+			.get()
+			.uri(endpointUri)
+			.retrieve()
+			.bodyToMono(List.class)
+			.block();
+		model.addAttribute("repos", repos);
+
+		return "github-repos";
+	}
+
+	private ExchangeFilterFunction oauth2Credentials(OAuth2AuthorizedClient authorizedClient) {
+		return ExchangeFilterFunction.ofRequestProcessor(
+			clientRequest -> {
+				ClientRequest authorizedRequest = ClientRequest.from(clientRequest)
+					.header(HttpHeaders.AUTHORIZATION, "Bearer " + authorizedClient.getAccessToken().getTokenValue())
+					.build();
+				return Mono.just(authorizedRequest);
+			});
+	}
+}

+ 23 - 0
samples/boot/oauth2/authcodegrant/src/main/resources/application.yml

@@ -0,0 +1,23 @@
+server:
+  port: 8080
+
+logging:
+  level:
+    root: INFO
+    org.springframework.web: INFO
+    org.springframework.security: INFO
+#    org.springframework.boot.autoconfigure: DEBUG
+
+spring:
+  thymeleaf:
+    cache: false
+  security:
+    oauth2:
+      client:
+        registration:
+          github:
+            client-id: your-app-client-id
+            client-secret: your-app-client-secret
+            scope: public_repo
+            redirect-uri-template: "{baseUrl}/github-repos"
+            client-name: GitHub Repositories

+ 28 - 0
samples/boot/oauth2/authcodegrant/src/main/resources/templates/github-repos.html

@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
+<head>
+	<title>Spring Security - OAuth 2.0 Authorization Code Grant</title>
+	<meta charset="utf-8" />
+</head>
+<body>
+<div style="float: right" th:fragment="logout" sec:authorize="isAuthenticated()">
+	<div style="float:left">
+		<span style="font-weight:bold">User: </span><span sec:authentication="name"></span>
+	</div>
+	<div style="float:none">&nbsp;</div>
+	<div style="float:right">
+		<form action="#" th:action="@{/logout}" method="post">
+			<input type="submit" value="Logout" />
+		</form>
+	</div>
+</div>
+<h1>GitHub Repositories</h1>
+<div>
+	<ul>
+		<li th:each="repo : ${repos}">
+			<span style="font-weight:bold" th:text="${repo.name}"></span>: <span th:text="${repo.url}"></span>
+		</li>
+	</ul>
+</div>
+</body>
+</html>