33 changed files with 2531 additions and 187 deletions
@ -0,0 +1,52 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,295 @@
@@ -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)); |
||||
} |
||||
} |
||||
@ -0,0 +1,74 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,171 @@
@@ -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); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,60 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,44 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,88 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,114 @@
@@ -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; |
||||
} |
||||
} |
||||
@ -0,0 +1,54 @@
@@ -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 "exchange" 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()); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,209 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,72 @@
@@ -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(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,141 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,115 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,304 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,64 @@
@@ -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. |
||||
@ -0,0 +1,16 @@
@@ -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' |
||||
} |
||||
@ -0,0 +1,165 @@
@@ -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 { |
||||
} |
||||
} |
||||
@ -0,0 +1,30 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,55 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,81 @@
@@ -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); |
||||
}); |
||||
} |
||||
} |
||||
@ -0,0 +1,23 @@
@@ -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 |
||||
@ -0,0 +1,28 @@
@@ -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"> </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> |
||||
Loading…
Reference in new issue