parent
a38352c4cc
commit
829c386756
|
@ -113,6 +113,13 @@
|
|||
<scope>compile</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-client</artifactId>
|
||||
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-openid</artifactId>
|
||||
|
|
|
@ -12,6 +12,7 @@ dependencies {
|
|||
|
||||
optional project(':spring-security-ldap')
|
||||
optional project(':spring-security-messaging')
|
||||
optional project(':spring-security-oauth2-client')
|
||||
optional project(':spring-security-openid')
|
||||
optional project(':spring-security-web')
|
||||
optional 'org.aspectj:aspectjweaver'
|
||||
|
|
|
@ -77,6 +77,10 @@ final class FilterComparator implements Comparator<Filter>, Serializable {
|
|||
order += STEP;
|
||||
put(LogoutFilter.class, order);
|
||||
order += STEP;
|
||||
filterToOrder.put(
|
||||
"org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter",
|
||||
order);
|
||||
order += STEP;
|
||||
put(X509AuthenticationFilter.class, order);
|
||||
order += STEP;
|
||||
put(AbstractPreAuthenticatedProcessingFilter.class, order);
|
||||
|
@ -84,6 +88,10 @@ final class FilterComparator implements Comparator<Filter>, Serializable {
|
|||
filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter",
|
||||
order);
|
||||
order += STEP;
|
||||
filterToOrder.put(
|
||||
"org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter",
|
||||
order);
|
||||
order += STEP;
|
||||
put(UsernamePasswordAuthenticationFilter.class, order);
|
||||
order += STEP;
|
||||
put(ConcurrentSessionFilter.class, order);
|
||||
|
|
|
@ -61,6 +61,7 @@ import org.springframework.security.core.Authentication;
|
|||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain;
|
||||
import org.springframework.security.web.PortMapper;
|
||||
import org.springframework.security.web.PortMapperImpl;
|
||||
|
@ -896,6 +897,158 @@ public final class HttpSecurity extends
|
|||
return getOrApply(new FormLoginConfigurer<HttpSecurity>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures authentication against an external <i>OAuth 2.0</i> or <i>OpenID Connect 1.0</i> Provider.
|
||||
* <br>
|
||||
* <br>
|
||||
*
|
||||
* The <i>"authentication flow"</i> is realized using the <b>Authorization Code Grant</b>,
|
||||
* as specified in the <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1">OAuth 2.0 Authorization Framework</a>.
|
||||
* <br>
|
||||
* <br>
|
||||
*
|
||||
* As a prerequisite to using this feature, the developer must register a <i>Client</i> with an <i>Authorization Server</i>.
|
||||
* The output of the <i>Client Registration</i> process results in a number of properties that are then used for configuring
|
||||
* an instance of a {@link org.springframework.security.oauth2.client.registration.ClientRegistration}.
|
||||
* Properties specific to a <i>Client</i> include: <i>client_id</i>, <i>client_secret</i>, <i>scope</i>, <i>redirect_uri</i>, etc.
|
||||
* There are also properties specific to the <i>Provider</i>, for example,
|
||||
* <i>Authorization Endpoint URI</i>, <i>Token Endpoint URI</i>, <i>UserInfo Endpoint URI</i>, etc.
|
||||
* <br>
|
||||
* <br>
|
||||
*
|
||||
* Multiple client support is provided for use cases where the application provides the user the option
|
||||
* for <i>"Logging in"</i> against one or more Providers, for example, <i>Google</i>, <i>GitHub</i>, <i>Facebook</i>, etc.
|
||||
* <br>
|
||||
* <br>
|
||||
*
|
||||
* {@link org.springframework.security.oauth2.client.registration.ClientRegistration}(s) are composed within a
|
||||
* {@link org.springframework.security.oauth2.client.registration.ClientRegistrationRepository}.
|
||||
* An instance of {@link org.springframework.security.oauth2.client.registration.ClientRegistrationRepository} is <b>required</b>
|
||||
* and may be supplied via the {@link ApplicationContext} or configured using
|
||||
* {@link OAuth2LoginConfigurer#clients(org.springframework.security.oauth2.client.registration.ClientRegistrationRepository)}.
|
||||
* <br>
|
||||
* <br>
|
||||
*
|
||||
* The default configuration provides an auto-generated login page at <code>"/login"</code> and
|
||||
* redirects to <code>"/login?error"</code> when an authentication error occurs.
|
||||
* The login page will display each of the clients (composed within the
|
||||
* {@link org.springframework.security.oauth2.client.registration.ClientRegistrationRepository})
|
||||
* with an anchor link to <code>"/oauth2/authorization/code/{clientAlias}"</code>.
|
||||
* Clicking through the link will initiate the <i>"Authorization Request"</i> flow
|
||||
* redirecting the end-user's user-agent to the <i>Authorization Endpoint</i> of the <i>Provider</i>.
|
||||
* Assuming the <i>Resource Owner</i> (end-user) grants the <i>Client</i> access, the <i>Authorization Server</i>
|
||||
* will redirect the end-user's user-agent to the <i>Redirection Endpoint</i> containing the <i>Authorization Code</i>
|
||||
* - the <i>Redirection Endpoint</i> is automatically configured for the application and
|
||||
* defaults to <code>"/oauth2/authorize/code/{clientAlias}"</code>.
|
||||
*
|
||||
* <p>
|
||||
* At this point in the <i>"authentication flow"</i>, the configured
|
||||
* {@link org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger}
|
||||
* will exchange the <i>Authorization Code</i> for an <i>Access Token</i> and then use it to access the protected resource
|
||||
* at the <i>UserInfo Endpoint</i> (via {@link org.springframework.security.oauth2.client.user.OAuth2UserService})
|
||||
* in order to retrieve the details of the <i>Resource Owner</i> (end-user) and establish the <i>"authenticated"</i> session.
|
||||
*
|
||||
* <h2>Example Configurations</h2>
|
||||
*
|
||||
* The minimal configuration defaults to automatically generating a login page at <code>"/login"</code>
|
||||
* and redirecting to <code>"/login?error"</code> when an authentication error occurs or redirecting to
|
||||
* <code>"/"</code> when an authenticated session is established.
|
||||
*
|
||||
* <pre>
|
||||
* @EnableWebSecurity
|
||||
* public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
*
|
||||
* @Override
|
||||
* protected void configure(HttpSecurity http) throws Exception {
|
||||
* http
|
||||
* .authorizeRequests()
|
||||
* .anyRequest().authenticated()
|
||||
* .and()
|
||||
* .oauth2Login();
|
||||
* }
|
||||
*
|
||||
* @Bean
|
||||
* public ClientRegistrationRepository clientRegistrationRepository() {
|
||||
* // ClientRegistrationRepositoryImpl must be composed of at least one ClientRegistration instance
|
||||
* return new ClientRegistrationRepositoryImpl();
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* The following shows the configuration options available for customizing the defaults.
|
||||
*
|
||||
* <pre>
|
||||
* @EnableWebSecurity
|
||||
* public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
*
|
||||
* @Override
|
||||
* protected void configure(HttpSecurity http) throws Exception {
|
||||
* http
|
||||
* .authorizeRequests()
|
||||
* .anyRequest().authenticated()
|
||||
* .and()
|
||||
* .oauth2Login()
|
||||
* .clients(this.clientRegistrationRepository())
|
||||
* .authorizationRequestBuilder(this.authorizationRequestBuilder())
|
||||
* .authorizationCodeTokenExchanger(this.authorizationCodeTokenExchanger())
|
||||
* .userInfoEndpoint()
|
||||
* .userInfoService(this.userInfoService())
|
||||
* .userInfoEndpoint()
|
||||
* // Provide a mapping between a Converter implementation and a UserInfo Endpoint URI
|
||||
* .userInfoTypeConverter(this.userInfoConverter(),
|
||||
* new URI("https://www.googleapis.com/oauth2/v3/userinfo"));
|
||||
* }
|
||||
*
|
||||
* @Bean
|
||||
* public ClientRegistrationRepository clientRegistrationRepository() {
|
||||
* // ClientRegistrationRepositoryImpl must be composed of at least one ClientRegistration instance
|
||||
* return new ClientRegistrationRepositoryImpl();
|
||||
* }
|
||||
*
|
||||
* @Bean
|
||||
* public AuthorizationRequestUriBuilder authorizationRequestBuilder() {
|
||||
* // Custom URI builder for the "Authorization Request"
|
||||
* return new AuthorizationRequestUriBuilderImpl();
|
||||
* }
|
||||
*
|
||||
* @Bean
|
||||
* public AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger() {
|
||||
* // Custom implementation that exchanges an "Authorization Code Grant" for an "Access Token"
|
||||
* return new AuthorizationCodeTokenExchangerImpl();
|
||||
* }
|
||||
*
|
||||
* @Bean
|
||||
* public OAuth2UserService userInfoService() {
|
||||
* // Custom implementation that retrieves the details of the authenticated user at the "UserInfo Endpoint"
|
||||
* return new OAuth2UserServiceImpl();
|
||||
* }
|
||||
*
|
||||
* @Bean
|
||||
* public Converter<ClientHttpResponse, UserInfo> userInfoConverter() {
|
||||
* // Default converter implementation for UserInfo
|
||||
* return new org.springframework.security.oauth2.client.user.converter.UserInfoConverter();
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @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.1">Section 4.1.1 Authorization Request</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
|
||||
* @see org.springframework.security.oauth2.client.registration.ClientRegistration
|
||||
* @see org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
|
||||
* @see org.springframework.security.oauth2.client.authentication.AuthorizationRequestUriBuilder
|
||||
* @see org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger
|
||||
* @see org.springframework.security.oauth2.client.user.OAuth2UserService
|
||||
*
|
||||
* @return the {@link OAuth2LoginConfigurer} for further customizations
|
||||
* @throws Exception
|
||||
*/
|
||||
public OAuth2LoginConfigurer<HttpSecurity> oauth2Login() throws Exception {
|
||||
return getOrApply(new OAuth2LoginConfigurer<HttpSecurity>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures channel security. In order for this configuration to be useful at least
|
||||
* one mapping to a required channel must be provided.
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core.convert.converter.Converter;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger;
|
||||
import org.springframework.security.oauth2.client.authentication.nimbus.NimbusAuthorizationCodeTokenExchanger;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.user.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.user.nimbus.NimbusOAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
final class AuthorizationCodeAuthenticationFilterConfigurer<H extends HttpSecurityBuilder<H>> extends
|
||||
AbstractAuthenticationFilterConfigurer<H, AuthorizationCodeAuthenticationFilterConfigurer<H>, AuthorizationCodeAuthenticationProcessingFilter> {
|
||||
|
||||
private AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger;
|
||||
private OAuth2UserService userInfoService;
|
||||
private Map<URI, Converter<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters = new HashMap<>();
|
||||
|
||||
|
||||
AuthorizationCodeAuthenticationFilterConfigurer() {
|
||||
super(new AuthorizationCodeAuthenticationProcessingFilter(), null);
|
||||
}
|
||||
|
||||
AuthorizationCodeAuthenticationFilterConfigurer<H> clientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty");
|
||||
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
|
||||
return this;
|
||||
}
|
||||
|
||||
AuthorizationCodeAuthenticationFilterConfigurer<H> authorizationCodeTokenExchanger(
|
||||
AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger) {
|
||||
|
||||
Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null");
|
||||
this.authorizationCodeTokenExchanger = authorizationCodeTokenExchanger;
|
||||
return this;
|
||||
}
|
||||
|
||||
AuthorizationCodeAuthenticationFilterConfigurer<H> userInfoService(OAuth2UserService userInfoService) {
|
||||
Assert.notNull(userInfoService, "userInfoService cannot be null");
|
||||
this.userInfoService = userInfoService;
|
||||
return this;
|
||||
}
|
||||
|
||||
AuthorizationCodeAuthenticationFilterConfigurer<H> userInfoTypeConverter(Converter<ClientHttpResponse, ? extends OAuth2User> userInfoConverter, URI userInfoUri) {
|
||||
Assert.notNull(userInfoConverter, "userInfoConverter cannot be null");
|
||||
Assert.notNull(userInfoUri, "userInfoUri cannot be null");
|
||||
this.userInfoTypeConverters.put(userInfoUri, userInfoConverter);
|
||||
return this;
|
||||
}
|
||||
|
||||
String getLoginUrl() {
|
||||
return super.getLoginPage();
|
||||
}
|
||||
|
||||
String getLoginFailureUrl() {
|
||||
return super.getFailureUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(H http) throws Exception {
|
||||
AuthorizationCodeAuthenticationProvider authenticationProvider = new AuthorizationCodeAuthenticationProvider(
|
||||
this.getAuthorizationCodeTokenExchanger(), this.getUserInfoService());
|
||||
authenticationProvider = this.postProcess(authenticationProvider);
|
||||
http.authenticationProvider(authenticationProvider);
|
||||
super.init(http);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(H http) throws Exception {
|
||||
AuthorizationCodeAuthenticationProcessingFilter authFilter = this.getAuthenticationFilter();
|
||||
authFilter.setClientRegistrationRepository(OAuth2LoginConfigurer.getClientRegistrationRepository(this.getBuilder()));
|
||||
super.configure(http);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
|
||||
return this.getAuthenticationFilter().getAuthorizeRequestMatcher();
|
||||
}
|
||||
|
||||
private AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> getAuthorizationCodeTokenExchanger() {
|
||||
if (this.authorizationCodeTokenExchanger == null) {
|
||||
this.authorizationCodeTokenExchanger = new NimbusAuthorizationCodeTokenExchanger();
|
||||
}
|
||||
return this.authorizationCodeTokenExchanger;
|
||||
}
|
||||
|
||||
private OAuth2UserService getUserInfoService() {
|
||||
if (this.userInfoService == null) {
|
||||
this.userInfoService = new NimbusOAuth2UserService(this.userInfoTypeConverters);
|
||||
}
|
||||
return this.userInfoService;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationRequestUriBuilder;
|
||||
import org.springframework.security.oauth2.client.authentication.DefaultAuthorizationRequestUriBuilder;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
final class AuthorizationCodeRequestRedirectFilterConfigurer<B extends HttpSecurityBuilder<B>> extends
|
||||
AbstractHttpConfigurer<AuthorizationCodeRequestRedirectFilterConfigurer<B>, B> {
|
||||
|
||||
private AuthorizationRequestUriBuilder authorizationRequestBuilder;
|
||||
|
||||
AuthorizationCodeRequestRedirectFilterConfigurer<B> clientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty");
|
||||
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
|
||||
return this;
|
||||
}
|
||||
|
||||
AuthorizationCodeRequestRedirectFilterConfigurer<B> authorizationRequestBuilder(AuthorizationRequestUriBuilder authorizationRequestBuilder) {
|
||||
Assert.notNull(authorizationRequestBuilder, "authorizationRequestBuilder cannot be null");
|
||||
this.authorizationRequestBuilder = authorizationRequestBuilder;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(B http) throws Exception {
|
||||
AuthorizationCodeRequestRedirectFilter filter = new AuthorizationCodeRequestRedirectFilter(
|
||||
OAuth2LoginConfigurer.getClientRegistrationRepository(this.getBuilder()),
|
||||
this.getAuthorizationRequestBuilder());
|
||||
http.addFilter(this.postProcess(filter));
|
||||
}
|
||||
|
||||
private AuthorizationRequestUriBuilder getAuthorizationRequestBuilder() {
|
||||
if (this.authorizationRequestBuilder == null) {
|
||||
this.authorizationRequestBuilder = new DefaultAuthorizationRequestUriBuilder();
|
||||
}
|
||||
return this.authorizationRequestBuilder;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.context.ApplicationContext;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationRequestUriBuilder;
|
||||
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.user.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>> extends
|
||||
AbstractHttpConfigurer<OAuth2LoginConfigurer<B>, B> {
|
||||
|
||||
private final AuthorizationCodeRequestRedirectFilterConfigurer<B> authorizationCodeRequestRedirectFilterConfigurer;
|
||||
private final AuthorizationCodeAuthenticationFilterConfigurer<B> authorizationCodeAuthenticationFilterConfigurer;
|
||||
private final UserInfoEndpointConfig userInfoEndpointConfig;
|
||||
|
||||
public OAuth2LoginConfigurer() {
|
||||
this.authorizationCodeRequestRedirectFilterConfigurer = new AuthorizationCodeRequestRedirectFilterConfigurer<>();
|
||||
this.authorizationCodeAuthenticationFilterConfigurer = new AuthorizationCodeAuthenticationFilterConfigurer<>();
|
||||
this.userInfoEndpointConfig = new UserInfoEndpointConfig();
|
||||
}
|
||||
|
||||
public OAuth2LoginConfigurer<B> clients(ClientRegistration... clientRegistrations) {
|
||||
Assert.notEmpty(clientRegistrations, "clientRegistrations cannot be empty");
|
||||
return clients(new InMemoryClientRegistrationRepository(Arrays.asList(clientRegistrations)));
|
||||
}
|
||||
|
||||
public OAuth2LoginConfigurer<B> clients(ClientRegistrationRepository clientRegistrationRepository) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty");
|
||||
this.getBuilder().setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OAuth2LoginConfigurer<B> authorizationRequestBuilder(AuthorizationRequestUriBuilder authorizationRequestBuilder) {
|
||||
Assert.notNull(authorizationRequestBuilder, "authorizationRequestBuilder cannot be null");
|
||||
this.authorizationCodeRequestRedirectFilterConfigurer.authorizationRequestBuilder(authorizationRequestBuilder);
|
||||
return this;
|
||||
}
|
||||
|
||||
public OAuth2LoginConfigurer<B> authorizationCodeTokenExchanger(
|
||||
AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger) {
|
||||
|
||||
Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null");
|
||||
this.authorizationCodeAuthenticationFilterConfigurer.authorizationCodeTokenExchanger(authorizationCodeTokenExchanger);
|
||||
return this;
|
||||
}
|
||||
|
||||
public UserInfoEndpointConfig userInfoEndpoint() {
|
||||
return this.userInfoEndpointConfig;
|
||||
}
|
||||
|
||||
public class UserInfoEndpointConfig {
|
||||
|
||||
private UserInfoEndpointConfig() {
|
||||
}
|
||||
|
||||
public OAuth2LoginConfigurer<B> userInfoService(OAuth2UserService userInfoService) {
|
||||
Assert.notNull(userInfoService, "userInfoService cannot be null");
|
||||
OAuth2LoginConfigurer.this.authorizationCodeAuthenticationFilterConfigurer.userInfoService(userInfoService);
|
||||
return this.and();
|
||||
}
|
||||
|
||||
public OAuth2LoginConfigurer<B> userInfoTypeConverter(Converter<ClientHttpResponse, ? extends OAuth2User> userInfoConverter, URI userInfoUri) {
|
||||
Assert.notNull(userInfoConverter, "userInfoConverter cannot be null");
|
||||
Assert.notNull(userInfoUri, "userInfoUri cannot be null");
|
||||
OAuth2LoginConfigurer.this.authorizationCodeAuthenticationFilterConfigurer.userInfoTypeConverter(userInfoConverter, userInfoUri);
|
||||
return this.and();
|
||||
}
|
||||
|
||||
public OAuth2LoginConfigurer<B> and() {
|
||||
return OAuth2LoginConfigurer.this;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(B http) throws Exception {
|
||||
this.authorizationCodeRequestRedirectFilterConfigurer.setBuilder(http);
|
||||
this.authorizationCodeAuthenticationFilterConfigurer.setBuilder(http);
|
||||
|
||||
this.authorizationCodeRequestRedirectFilterConfigurer.init(http);
|
||||
this.authorizationCodeAuthenticationFilterConfigurer.init(http);
|
||||
this.initDefaultLoginFilter(http);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(B http) throws Exception {
|
||||
this.authorizationCodeRequestRedirectFilterConfigurer.configure(http);
|
||||
this.authorizationCodeAuthenticationFilterConfigurer.configure(http);
|
||||
}
|
||||
|
||||
static <B extends HttpSecurityBuilder<B>> ClientRegistrationRepository getClientRegistrationRepository(B http) {
|
||||
ClientRegistrationRepository clientRegistrationRepository = http.getSharedObject(ClientRegistrationRepository.class);
|
||||
if (clientRegistrationRepository == null) {
|
||||
clientRegistrationRepository = getDefaultClientRegistrationRepository(http);
|
||||
http.setSharedObject(ClientRegistrationRepository.class, clientRegistrationRepository);
|
||||
}
|
||||
return clientRegistrationRepository;
|
||||
}
|
||||
|
||||
private static <B extends HttpSecurityBuilder<B>> ClientRegistrationRepository getDefaultClientRegistrationRepository(B http) {
|
||||
return http.getSharedObject(ApplicationContext.class).getBean(ClientRegistrationRepository.class);
|
||||
}
|
||||
|
||||
private void initDefaultLoginFilter(B http) {
|
||||
DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageGeneratingFilter.class);
|
||||
if (loginPageGeneratingFilter != null && !this.authorizationCodeAuthenticationFilterConfigurer.isCustomLoginPage()) {
|
||||
ClientRegistrationRepository clientRegistrationRepository = getClientRegistrationRepository(this.getBuilder());
|
||||
if (!CollectionUtils.isEmpty(clientRegistrationRepository.getRegistrations())) {
|
||||
Map<String, String> oauth2AuthenticationUrlToClientName = clientRegistrationRepository.getRegistrations().stream()
|
||||
.collect(Collectors.toMap(e -> AuthorizationCodeRequestRedirectFilter.AUTHORIZATION_BASE_URI + "/" + e.getClientAlias(),
|
||||
e -> e.getClientName()));
|
||||
loginPageGeneratingFilter.setOauth2LoginEnabled(true);
|
||||
loginPageGeneratingFilter.setOauth2AuthenticationUrlToClientName(oauth2AuthenticationUrlToClientName);
|
||||
loginPageGeneratingFilter.setLoginPageUrl(this.authorizationCodeAuthenticationFilterConfigurer.getLoginUrl());
|
||||
loginPageGeneratingFilter.setFailureUrl(this.authorizationCodeAuthenticationFilterConfigurer.getLoginFailureUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -78,7 +78,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
|
|||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then:
|
||||
response.getContentAsString() == """<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'>
|
||||
<p><font color='red'>Your login attempt was not successful, try again.<br/><br/>Reason: Bad credentials</font></p><h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
|
||||
<p style='color:red;'>Your login attempt was not successful, try again.<br/><br/>Reason: Bad credentials</p><h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
|
||||
<table>
|
||||
<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
|
||||
<tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
|
||||
|
@ -107,7 +107,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
|
|||
springSecurityFilterChain.doFilter(request,response,chain)
|
||||
then: "sent to default success page"
|
||||
response.getContentAsString() == """<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'>
|
||||
<p><font color='green'>You have been logged out</font></p><h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
|
||||
<p style='color:green;'>You have been logged out</p><h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
|
||||
<table>
|
||||
<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
|
||||
<tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
|
||||
|
|
|
@ -375,42 +375,15 @@ git clone https://github.com/spring-projects/spring-security.git
|
|||
This will give you access to the entire project history (including all releases and branches) on your local machine.
|
||||
|
||||
[[new]]
|
||||
== What's New in Spring Security 4.2
|
||||
== What's New in Spring Security 5.0
|
||||
|
||||
Among other things, Spring Security 4.2 brings early support for Spring Framework 5.
|
||||
You can find the change logs for https://github.com/spring-projects/spring-security/milestone/86?closed=1[4.2.0.M1], https://github.com/spring-projects/spring-security/milestone/91?closed=1[4.2.0.RC1], https://github.com/spring-projects/spring-security/milestone/92?closed=1[4.2.0.RELEASE] which closes over 80 issues.
|
||||
The overwhelming majority of these features were contributed by the community.
|
||||
Below you can find the highlights of this release.
|
||||
Spring Security 5.0 provides a number of new features as well as support for Spring Framework 5.
|
||||
You can find the change log at https://github.com/spring-projects/spring-security/milestone/90?closed=1[5.0.0.M1].
|
||||
Below are the highlights of this milestone release.
|
||||
|
||||
=== Web Improvements
|
||||
|
||||
* https://github.com/spring-projects/spring-security/pull/3812[#3812] - <<jackson,Jackson Support>>
|
||||
* https://github.com/spring-projects/spring-security/pull/4116[#4116] - <<headers-referrer,Referrer Policy>>
|
||||
* https://github.com/spring-projects/spring-security/pull/3938[#3938] - Add <<request-matching,HTTP response splitting prevention>>
|
||||
* https://github.com/spring-projects/spring-security/issues/3949[#3949] - Add <<mvc-authentication-principal,bean reference support to @AuthenticationPrincipal>>.
|
||||
* https://github.com/spring-projects/spring-security/pull/3978[#3978] - Support for Standford WebAuth and Shibboleth using the newly added http://docs.spring.io/spring-security/site/docs/4.2.x-SNAPSHOT/apidocs/org/springframework/security/web/authentication/preauth/RequestAttributeAuthenticationFilter.html[RequestAttributeAuthenticationFilter].
|
||||
* https://github.com/spring-projects/spring-security/issues/4076[#4076] - Document <<appendix-proxy-server,Proxy Server>> Configuration
|
||||
* https://github.com/spring-projects/spring-security/issues/3795[#3795] - `ConcurrentSessionFilter` supports `InvalidSessionStrategy`
|
||||
* https://github.com/spring-projects/spring-security/pull/3904[#3904] - Add `CompositeLogoutHandler`
|
||||
|
||||
=== Configuration Improvements
|
||||
|
||||
* https://github.com/spring-projects/spring-security/pull/3956[#3956] - Central configuration of the http://docs.spring.io/spring-security/site/migrate/current/3-to-4/html5/migrate-3-to-4-jc.html#m3to4-role-prefixing[default role prefix]. See the issue for details.
|
||||
* https://github.com/spring-projects/spring-security/issues/4102[#4102] - Custom default configuration in `WebSecurityConfigurerAdapter`. See <<jc-custom-dsls>>
|
||||
* https://github.com/spring-projects/spring-security/issues/3899[#3899] - <<nsa-concurrency-control-max-sessions,concurrency-control@max-sessions>> supports unlimited sessions.
|
||||
* https://github.com/spring-projects/spring-security/issues/4097[#4097] - <<nsa-intercept-url-request-matcher-ref,intercept-url@request-matcher-ref>> adds more powerful request matching support to the XML namespace.
|
||||
* https://github.com/spring-projects/spring-security/issues/3990[#3990] - Support for constructing `RoleHierarchy` from `Map` (i.e. `yml`)
|
||||
* https://github.com/spring-projects/spring-security/pull/4062[#4062] - Custom cookiePath to `CookieCsrfTokenRepository`
|
||||
* https://github.com/spring-projects/spring-security/issues/3794[#3794] - Allow configuration of `InvalidSessionStrategy` on `SessionManagementConfigurer`
|
||||
* https://github.com/spring-projects/spring-security/issues/4020[#4020] - Fix Exposing Beans for defaultMethodExpressionHandler can prevent Method Security
|
||||
|
||||
=== Miscellaneous
|
||||
|
||||
* https://github.com/spring-projects/spring-security/issues/4080[#4080] - Spring 5 support
|
||||
* https://github.com/spring-projects/spring-security/issues/4095[#4095] - `Add UserBuilder`
|
||||
* https://github.com/spring-projects/spring-security/issues/4018[#4018] - Fix after `csrf()` is invoked, future `MockMvc` invocations use original `CsrfTokenRepository`
|
||||
* Version Updates
|
||||
=== New Features
|
||||
|
||||
* https://github.com/spring-projects/spring-security/issues/3907[#3907] - Support added for OAuth 2.0 Login (start with {gh-samples-url}/boot/oauth2login/README.adoc[Sample README])
|
||||
|
||||
[[samples]]
|
||||
== Samples and Guides (Start Here)
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-client</artifactId>
|
||||
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||
<name>spring-security-oauth2-client</name>
|
||||
<description>spring-security-oauth2-client</description>
|
||||
<url>http://spring.io/spring-security</url>
|
||||
<organization>
|
||||
<name>spring.io</name>
|
||||
<url>http://spring.io/</url>
|
||||
</organization>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>The Apache Software License, Version 2.0</name>
|
||||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer>
|
||||
<id>rwinch</id>
|
||||
<name>Rob Winch</name>
|
||||
<email>rwinch@pivotal.io</email>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>jgrandja</id>
|
||||
<name>Joe Grandja</name>
|
||||
<email>jgrandja@pivotal.io</email>
|
||||
</developer>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection>scm:git:git://github.com/spring-projects/spring-security</connection>
|
||||
<developerConnection>scm:git:git://github.com/spring-projects/spring-security</developerConnection>
|
||||
<url>https://github.com/spring-projects/spring-security</url>
|
||||
</scm>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-framework-bom</artifactId>
|
||||
<version>4.3.5.RELEASE</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.nimbusds</groupId>
|
||||
<artifactId>oauth2-oidc-sdk</artifactId>
|
||||
<version>5.21</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-core</artifactId>
|
||||
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-web</artifactId>
|
||||
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
<scope>compile</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<groupId>commons-logging</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<version>1.2</version>
|
||||
<scope>compile</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.6.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>1.10.19</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jcl-over-slf4j</artifactId>
|
||||
<version>1.7.7</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshot</id>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,12 @@
|
|||
apply plugin: 'io.spring.convention.spring-module'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-security-core')
|
||||
compile project(':spring-security-oauth2-core')
|
||||
compile project(':spring-security-web')
|
||||
compile springCoreDependency
|
||||
compile 'com.nimbusds:oauth2-oidc-sdk'
|
||||
compile 'org.springframework:spring-web'
|
||||
|
||||
provided 'javax.servlet:javax.servlet-api'
|
||||
}
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.AuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.user.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.web.converter.AuthorizationCodeAuthorizationResponseAttributesConverter;
|
||||
import org.springframework.security.oauth2.client.web.converter.ErrorResponseAttributesConverter;
|
||||
import org.springframework.security.oauth2.core.AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.*;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter.isDefaultRedirectUri;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link AbstractAuthenticationProcessingFilter} that handles
|
||||
* the processing of an <i>OAuth 2.0 Authorization Response</i> for the authorization code grant flow.
|
||||
*
|
||||
* <p>
|
||||
* This <code>Filter</code> processes the <i>Authorization Response</i> in the following step sequence:
|
||||
*
|
||||
* <ol>
|
||||
* <li>
|
||||
* Assuming the resource owner (end-user) has granted access to the client, the authorization server will append the
|
||||
* {@link OAuth2Parameter#CODE} and {@link OAuth2Parameter#STATE} (if provided in the <i>Authorization Request</i>) parameters
|
||||
* to the {@link OAuth2Parameter#REDIRECT_URI} (provided in the <i>Authorization Request</i>)
|
||||
* and redirect the end-user's user-agent back to this <code>Filter</code> (the client).
|
||||
* </li>
|
||||
* <li>
|
||||
* This <code>Filter</code> will then create an {@link AuthorizationCodeAuthenticationToken} with
|
||||
* the {@link OAuth2Parameter#CODE} received in the previous step and pass it to
|
||||
* {@link AuthorizationCodeAuthenticationProvider#authenticate(Authentication)} (indirectly via {@link AuthenticationManager}).
|
||||
* The {@link AuthorizationCodeAuthenticationProvider} will use an {@link AuthorizationGrantTokenExchanger} to make a request
|
||||
* to the authorization server's <i>Token Endpoint</i> for exchanging the {@link OAuth2Parameter#CODE} for an {@link AccessToken}.
|
||||
* </li>
|
||||
* <li>
|
||||
* Upon receiving the <i>Access Token Request</i>, the authorization server will authenticate the client,
|
||||
* verify the {@link OAuth2Parameter#CODE}, and ensure that the {@link OAuth2Parameter#REDIRECT_URI}
|
||||
* received matches the <code>URI</code> originally provided in the <i>Authorization Request</i>.
|
||||
* If the request is valid, the authorization server will respond back with a {@link TokenResponseAttributes}.
|
||||
* </li>
|
||||
* <li>
|
||||
* The {@link AuthorizationCodeAuthenticationProvider} will then create a new {@link OAuth2AuthenticationToken}
|
||||
* associating the {@link AccessToken} from the {@link TokenResponseAttributes} and pass it to
|
||||
* {@link OAuth2UserService#loadUser(OAuth2AuthenticationToken)}. The {@link OAuth2UserService} will make a request
|
||||
* to the authorization server's <i>UserInfo Endpoint</i> (using the {@link AccessToken})
|
||||
* to obtain the end-user's (resource owner) attributes and return it in the form of an {@link OAuth2User}.
|
||||
* </li>
|
||||
* <li>
|
||||
* The {@link AuthorizationCodeAuthenticationProvider} will create another new {@link OAuth2AuthenticationToken}
|
||||
* but this time associating the {@link AccessToken} and {@link OAuth2User} returned from the {@link OAuth2UserService}.
|
||||
* Finally, the {@link OAuth2AuthenticationToken} is returned to the {@link AuthenticationManager}
|
||||
* and then back to this <code>Filter</code> at which point the session is considered <i>"authenticated"</i>.
|
||||
* </li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> Steps 4-5 are <b>not</b> part of the authorization code grant flow and instead are
|
||||
* <i>"authentication flow"</i> steps that are required in order to authenticate the end-user with the system.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see AbstractAuthenticationProcessingFilter
|
||||
* @see AuthorizationCodeAuthenticationToken
|
||||
* @see AuthorizationCodeAuthenticationProvider
|
||||
* @see AuthorizationGrantTokenExchanger
|
||||
* @see AuthorizationCodeAuthorizationResponseAttributes
|
||||
* @see AuthorizationRequestAttributes
|
||||
* @see AuthorizationRequestRepository
|
||||
* @see AuthorizationCodeRequestRedirectFilter
|
||||
* @see ClientRegistration
|
||||
* @see ClientRegistrationRepository
|
||||
* @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.2">Section 4.1.2 Authorization Response</a>
|
||||
*/
|
||||
public class AuthorizationCodeAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
|
||||
public static final String AUTHORIZE_BASE_URI = "/oauth2/authorize/code";
|
||||
private static final String CLIENT_ALIAS_VARIABLE_NAME = "clientAlias";
|
||||
private static final String AUTHORIZE_URI = AUTHORIZE_BASE_URI + "/{" + CLIENT_ALIAS_VARIABLE_NAME + "}";
|
||||
private static final String AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE = "authorization_request_not_found";
|
||||
private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
|
||||
private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
|
||||
private final ErrorResponseAttributesConverter errorResponseConverter = new ErrorResponseAttributesConverter();
|
||||
private final AuthorizationCodeAuthorizationResponseAttributesConverter authorizationCodeResponseConverter =
|
||||
new AuthorizationCodeAuthorizationResponseAttributesConverter();
|
||||
private final RequestMatcher authorizeRequestMatcher = new AntPathRequestMatcher(AUTHORIZE_URI);
|
||||
private ClientRegistrationRepository clientRegistrationRepository;
|
||||
private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
|
||||
|
||||
public AuthorizationCodeAuthenticationProcessingFilter() {
|
||||
super(AUTHORIZE_URI);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
|
||||
throws AuthenticationException, IOException, ServletException {
|
||||
|
||||
ErrorResponseAttributes authorizationError = this.errorResponseConverter.convert(request);
|
||||
if (authorizationError != null) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(authorizationError.getErrorCode(),
|
||||
authorizationError.getDescription(), authorizationError.getUri());
|
||||
this.getAuthorizationRequestRepository().removeAuthorizationRequest(request);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
AuthorizationRequestAttributes matchingAuthorizationRequest = this.resolveAuthorizationRequest(request);
|
||||
|
||||
ClientRegistration clientRegistration = this.getClientRegistrationRepository().getRegistrationByClientId(
|
||||
matchingAuthorizationRequest.getClientId());
|
||||
|
||||
// If clientRegistration.redirectUri is the default one (with Uri template variables)
|
||||
// then use matchingAuthorizationRequest.redirectUri instead
|
||||
if (isDefaultRedirectUri(clientRegistration)) {
|
||||
clientRegistration = new ClientRegistrationBuilderWithUriOverrides(
|
||||
clientRegistration, matchingAuthorizationRequest.getRedirectUri()).build();
|
||||
}
|
||||
|
||||
AuthorizationCodeAuthorizationResponseAttributes authorizationCodeResponseAttributes =
|
||||
this.authorizationCodeResponseConverter.convert(request);
|
||||
|
||||
AuthorizationCodeAuthenticationToken authRequest = new AuthorizationCodeAuthenticationToken(
|
||||
authorizationCodeResponseAttributes.getCode(), clientRegistration);
|
||||
|
||||
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
|
||||
|
||||
Authentication authenticated = this.getAuthenticationManager().authenticate(authRequest);
|
||||
|
||||
return authenticated;
|
||||
}
|
||||
|
||||
public RequestMatcher getAuthorizeRequestMatcher() {
|
||||
return this.authorizeRequestMatcher;
|
||||
}
|
||||
|
||||
protected ClientRegistrationRepository getClientRegistrationRepository() {
|
||||
return this.clientRegistrationRepository;
|
||||
}
|
||||
|
||||
public final void setClientRegistrationRepository(ClientRegistrationRepository clientRegistrationRepository) {
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
Assert.notEmpty(clientRegistrationRepository.getRegistrations(), "clientRegistrationRepository cannot be empty");
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
}
|
||||
|
||||
protected AuthorizationRequestRepository getAuthorizationRequestRepository() {
|
||||
return this.authorizationRequestRepository;
|
||||
}
|
||||
|
||||
public final void setAuthorizationRequestRepository(AuthorizationRequestRepository authorizationRequestRepository) {
|
||||
Assert.notNull(authorizationRequestRepository, "authorizationRequestRepository cannot be null");
|
||||
this.authorizationRequestRepository = authorizationRequestRepository;
|
||||
}
|
||||
|
||||
private AuthorizationRequestAttributes resolveAuthorizationRequest(HttpServletRequest request) {
|
||||
AuthorizationRequestAttributes authorizationRequest =
|
||||
this.getAuthorizationRequestRepository().loadAuthorizationRequest(request);
|
||||
if (authorizationRequest == null) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
this.getAuthorizationRequestRepository().removeAuthorizationRequest(request);
|
||||
this.assertMatchingAuthorizationRequest(request, authorizationRequest);
|
||||
return authorizationRequest;
|
||||
}
|
||||
|
||||
private void assertMatchingAuthorizationRequest(HttpServletRequest request, AuthorizationRequestAttributes authorizationRequest) {
|
||||
String state = request.getParameter(OAuth2Parameter.STATE);
|
||||
if (!authorizationRequest.getState().equals(state)) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
if (!request.getRequestURL().toString().equals(authorizationRequest.getRedirectUri())) {
|
||||
OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class ClientRegistrationBuilderWithUriOverrides extends ClientRegistration.Builder {
|
||||
|
||||
private ClientRegistrationBuilderWithUriOverrides(ClientRegistration clientRegistration, String redirectUri) {
|
||||
super(clientRegistration.getClientId());
|
||||
this.clientSecret(clientRegistration.getClientSecret());
|
||||
this.clientAuthenticationMethod(clientRegistration.getClientAuthenticationMethod());
|
||||
this.authorizedGrantType(clientRegistration.getAuthorizedGrantType());
|
||||
this.redirectUri(redirectUri);
|
||||
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
|
||||
this.scopes(clientRegistration.getScopes().stream().toArray(String[]::new));
|
||||
}
|
||||
this.authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri());
|
||||
this.tokenUri(clientRegistration.getProviderDetails().getTokenUri());
|
||||
this.userInfoUri(clientRegistration.getProviderDetails().getUserInfoUri());
|
||||
this.clientName(clientRegistration.getClientName());
|
||||
this.clientAlias(clientRegistration.getClientAlias());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
|
||||
import org.springframework.security.oauth2.client.user.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.AccessToken;
|
||||
import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link AuthenticationProvider} that is responsible for authenticating
|
||||
* an <i>authorization code</i> credential with the authorization server's <i>Token Endpoint</i>
|
||||
* and if valid, exchanging it for an <i>access token</i> credential.
|
||||
* Additionally, it will also obtain the end-user's (resource owner) attributes from the <i>UserInfo Endpoint</i>
|
||||
* (using the <i>access token</i>) and create a <code>Principal</code> in the form of an {@link OAuth2User}
|
||||
* associating it with the returned {@link OAuth2AuthenticationToken}.
|
||||
*
|
||||
* <p>
|
||||
* The {@link AuthorizationCodeAuthenticationProvider} uses an {@link AuthorizationGrantTokenExchanger}
|
||||
* to make a request to the authorization server's <i>Token Endpoint</i>
|
||||
* to verify the {@link AuthorizationCodeAuthenticationToken#getAuthorizationCode()}.
|
||||
* If the request is valid, the authorization server will respond back with a {@link TokenResponseAttributes}.
|
||||
*
|
||||
* <p>
|
||||
* It will then create a {@link OAuth2AuthenticationToken} associating the {@link AccessToken}
|
||||
* from the {@link TokenResponseAttributes} and pass it to {@link OAuth2UserService#loadUser(OAuth2AuthenticationToken)}
|
||||
* to obtain the end-user's (resource owner) attributes in the form of an {@link OAuth2User}.
|
||||
*
|
||||
* <p>
|
||||
* Finally, it will create another {@link OAuth2AuthenticationToken}, this time associating
|
||||
* the {@link AccessToken} and {@link OAuth2User} and return it to the {@link AuthenticationManager},
|
||||
* at which point the {@link OAuth2AuthenticationToken} is considered <i>"authenticated"</i>.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see AuthorizationCodeAuthenticationToken
|
||||
* @see AuthorizationGrantTokenExchanger
|
||||
* @see TokenResponseAttributes
|
||||
* @see AccessToken
|
||||
* @see OAuth2UserService
|
||||
* @see OAuth2User
|
||||
* @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 AuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
|
||||
private final AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger;
|
||||
private final OAuth2UserService userInfoService;
|
||||
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
|
||||
|
||||
public AuthorizationCodeAuthenticationProvider(
|
||||
AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> authorizationCodeTokenExchanger,
|
||||
OAuth2UserService userInfoService) {
|
||||
|
||||
Assert.notNull(authorizationCodeTokenExchanger, "authorizationCodeTokenExchanger cannot be null");
|
||||
Assert.notNull(userInfoService, "userInfoService cannot be null");
|
||||
this.authorizationCodeTokenExchanger = authorizationCodeTokenExchanger;
|
||||
this.userInfoService = userInfoService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
AuthorizationCodeAuthenticationToken authorizationCodeAuthentication =
|
||||
(AuthorizationCodeAuthenticationToken) authentication;
|
||||
|
||||
TokenResponseAttributes tokenResponse =
|
||||
this.authorizationCodeTokenExchanger.exchange(authorizationCodeAuthentication);
|
||||
|
||||
AccessToken accessToken = new AccessToken(tokenResponse.getTokenType(),
|
||||
tokenResponse.getTokenValue(), tokenResponse.getIssuedAt(),
|
||||
tokenResponse.getExpiresAt(), tokenResponse.getScopes());
|
||||
OAuth2AuthenticationToken accessTokenAuthentication = new OAuth2AuthenticationToken(
|
||||
authorizationCodeAuthentication.getClientRegistration(), accessToken);
|
||||
accessTokenAuthentication.setDetails(authorizationCodeAuthentication.getDetails());
|
||||
|
||||
OAuth2User user = this.userInfoService.loadUser(accessTokenAuthentication);
|
||||
|
||||
Collection<? extends GrantedAuthority> authorities =
|
||||
this.authoritiesMapper.mapAuthorities(user.getAuthorities());
|
||||
|
||||
OAuth2AuthenticationToken authenticationResult = new OAuth2AuthenticationToken(user, authorities,
|
||||
accessTokenAuthentication.getClientRegistration(), accessTokenAuthentication.getAccessToken());
|
||||
authenticationResult.setDetails(accessTokenAuthentication.getDetails());
|
||||
|
||||
return authenticationResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return AuthorizationCodeAuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
|
||||
public final void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
|
||||
Assert.notNull(authoritiesMapper, "authoritiesMapper cannot be null");
|
||||
this.authoritiesMapper = authoritiesMapper;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link AuthorizationGrantAuthenticationToken} that holds
|
||||
* an <i>authorization code grant</i> credential for a specific client identified in {@link #getClientRegistration()}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see AuthorizationGrantAuthenticationToken
|
||||
* @see ClientRegistration
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3.1">Section 1.3.1 Authorization Code Grant</a>
|
||||
*/
|
||||
public class AuthorizationCodeAuthenticationToken extends AuthorizationGrantAuthenticationToken {
|
||||
private final String authorizationCode;
|
||||
private final ClientRegistration clientRegistration;
|
||||
|
||||
public AuthorizationCodeAuthenticationToken(String authorizationCode, ClientRegistration clientRegistration) {
|
||||
super(AuthorizationGrantType.AUTHORIZATION_CODE, AuthorityUtils.NO_AUTHORITIES);
|
||||
Assert.hasText(authorizationCode, "authorizationCode cannot be empty");
|
||||
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
|
||||
this.authorizationCode = authorizationCode;
|
||||
this.clientRegistration = clientRegistration;
|
||||
this.setAuthenticated(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
return this.getAuthorizationCode();
|
||||
}
|
||||
|
||||
public String getAuthorizationCode() {
|
||||
return this.authorizationCode;
|
||||
}
|
||||
|
||||
public ClientRegistration getClientRegistration() {
|
||||
return this.clientRegistration;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.crypto.keygen.StringKeyGenerator;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
|
||||
import org.springframework.security.web.DefaultRedirectStrategy;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
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;
|
||||
import java.net.URI;
|
||||
|
||||
import static org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter.AUTHORIZE_BASE_URI;
|
||||
|
||||
/**
|
||||
* This <code>Filter</code> initiates the authorization code grant flow by redirecting
|
||||
* the end-user's user-agent to the authorization server's <i>Authorization Endpoint</i>.
|
||||
*
|
||||
* <p>
|
||||
* It uses an {@link AuthorizationRequestUriBuilder} to build the <i>OAuth 2.0 Authorization Request</i>,
|
||||
* which is used as the redirect <code>URI</code> to the <i>Authorization Endpoint</i>.
|
||||
* The redirect <code>URI</code> will include the client identifier, requested scope(s), state, response type, and a redirection URI
|
||||
* which the authorization server will send the user-agent back to (handled by {@link AuthorizationCodeAuthenticationProcessingFilter})
|
||||
* once access is granted (or denied) by the end-user (resource owner).
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see AuthorizationRequestAttributes
|
||||
* @see AuthorizationRequestRepository
|
||||
* @see AuthorizationRequestUriBuilder
|
||||
* @see ClientRegistration
|
||||
* @see ClientRegistrationRepository
|
||||
* @see AuthorizationCodeAuthenticationProcessingFilter
|
||||
* @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.1">Section 4.1.1 Authorization Request</a>
|
||||
*/
|
||||
public class AuthorizationCodeRequestRedirectFilter extends OncePerRequestFilter {
|
||||
public static final String AUTHORIZATION_BASE_URI = "/oauth2/authorization/code";
|
||||
private static final String CLIENT_ALIAS_VARIABLE_NAME = "clientAlias";
|
||||
private static final String AUTHORIZATION_URI = AUTHORIZATION_BASE_URI + "/{" + CLIENT_ALIAS_VARIABLE_NAME + "}";
|
||||
private static final String DEFAULT_REDIRECT_URI_TEMPLATE = "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}";
|
||||
private final AntPathRequestMatcher authorizationRequestMatcher;
|
||||
private final ClientRegistrationRepository clientRegistrationRepository;
|
||||
private final AuthorizationRequestUriBuilder authorizationUriBuilder;
|
||||
private final RedirectStrategy authorizationRedirectStrategy = new DefaultRedirectStrategy();
|
||||
private final StringKeyGenerator stateGenerator = new DefaultStateGenerator();
|
||||
private AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
|
||||
|
||||
public AuthorizationCodeRequestRedirectFilter(ClientRegistrationRepository clientRegistrationRepository,
|
||||
AuthorizationRequestUriBuilder authorizationUriBuilder) {
|
||||
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
|
||||
Assert.notNull(authorizationUriBuilder, "authorizationUriBuilder cannot be null");
|
||||
this.authorizationRequestMatcher = new AntPathRequestMatcher(AUTHORIZATION_URI);
|
||||
this.clientRegistrationRepository = clientRegistrationRepository;
|
||||
this.authorizationUriBuilder = authorizationUriBuilder;
|
||||
}
|
||||
|
||||
public final void setAuthorizationRequestRepository(AuthorizationRequestRepository 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.requiresAuthorization(request, response)) {
|
||||
try {
|
||||
this.sendRedirectForAuthorization(request, response);
|
||||
} catch (Exception failed) {
|
||||
this.unsuccessfulAuthorization(request, response, failed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
protected boolean requiresAuthorization(HttpServletRequest request, HttpServletResponse response) {
|
||||
return this.authorizationRequestMatcher.matches(request);
|
||||
}
|
||||
|
||||
protected void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException, ServletException {
|
||||
|
||||
String clientAlias = this.authorizationRequestMatcher
|
||||
.extractUriTemplateVariables(request).get(CLIENT_ALIAS_VARIABLE_NAME);
|
||||
ClientRegistration clientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias(clientAlias);
|
||||
if (clientRegistration == null) {
|
||||
throw new IllegalArgumentException("Invalid Client Identifier (Alias): " + clientAlias);
|
||||
}
|
||||
|
||||
String redirectUriStr;
|
||||
if (isDefaultRedirectUri(clientRegistration)) {
|
||||
redirectUriStr = this.expandDefaultRedirectUri(request, clientRegistration);
|
||||
} else {
|
||||
redirectUriStr = clientRegistration.getRedirectUri();
|
||||
}
|
||||
|
||||
AuthorizationRequestAttributes authorizationRequestAttributes =
|
||||
AuthorizationRequestAttributes.withAuthorizationCode()
|
||||
.clientId(clientRegistration.getClientId())
|
||||
.authorizeUri(clientRegistration.getProviderDetails().getAuthorizationUri())
|
||||
.redirectUri(redirectUriStr)
|
||||
.scopes(clientRegistration.getScopes())
|
||||
.state(this.stateGenerator.generateKey())
|
||||
.build();
|
||||
|
||||
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequestAttributes, request);
|
||||
|
||||
URI redirectUri = this.authorizationUriBuilder.build(authorizationRequestAttributes);
|
||||
this.authorizationRedirectStrategy.sendRedirect(request, response, redirectUri.toString());
|
||||
}
|
||||
|
||||
protected void unsuccessfulAuthorization(HttpServletRequest request, HttpServletResponse response,
|
||||
Exception failed) throws IOException, ServletException {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Authorization Request failed: " + failed.toString(), failed);
|
||||
}
|
||||
response.sendError(HttpServletResponse.SC_BAD_REQUEST, failed.getMessage());
|
||||
}
|
||||
|
||||
static boolean isDefaultRedirectUri(ClientRegistration clientRegistration) {
|
||||
return DEFAULT_REDIRECT_URI_TEMPLATE.equals(clientRegistration.getRedirectUri());
|
||||
}
|
||||
|
||||
private String expandDefaultRedirectUri(HttpServletRequest request, ClientRegistration clientRegistration) {
|
||||
return UriComponentsBuilder.fromUriString(DEFAULT_REDIRECT_URI_TEMPLATE)
|
||||
.buildAndExpand(request.getScheme(), request.getServerName(), request.getServerPort(),
|
||||
AUTHORIZE_BASE_URI, clientRegistration.getClientAlias())
|
||||
.encode()
|
||||
.toUriString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.GrantedAuthority;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Base implementation of an {@link AbstractAuthenticationToken} that holds
|
||||
* an <i>authorization grant</i> credential for a specific {@link AuthorizationGrantType}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see AuthorizationGrantType
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
|
||||
*/
|
||||
public abstract class AuthorizationGrantAuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||
private final AuthorizationGrantType authorizationGrantType;
|
||||
|
||||
protected AuthorizationGrantAuthenticationToken(AuthorizationGrantType authorizationGrantType,
|
||||
Collection<? extends GrantedAuthority> authorities) {
|
||||
|
||||
super(authorities);
|
||||
Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
|
||||
this.authorizationGrantType = authorizationGrantType;
|
||||
}
|
||||
|
||||
public AuthorizationGrantType getGrantType() {
|
||||
return this.authorizationGrantType;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for <i>"exchanging"</i>
|
||||
* an <i>authorization grant</i> credential (for example, an authorization code) for an
|
||||
* <i>access token</i> credential at the authorization server's <i>Token Endpoint</i>.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see AuthorizationGrantType
|
||||
* @see AuthorizationGrantAuthenticationToken
|
||||
* @see TokenResponseAttributes
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
|
||||
*/
|
||||
public interface AuthorizationGrantTokenExchanger<T extends AuthorizationGrantAuthenticationToken> {
|
||||
|
||||
TokenResponseAttributes exchange(T authorizationGrantAuthentication) throws OAuth2AuthenticationException;
|
||||
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.endpoint.AuthorizationRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for the persistence
|
||||
* of {@link AuthorizationRequestAttributes} between requests.
|
||||
*
|
||||
* <p>
|
||||
* Used by the {@link AuthorizationCodeRequestRedirectFilter} for persisting the <i>Authorization Request</i>
|
||||
* before it initiates the authorization code grant flow.
|
||||
* As well, used by the {@link AuthorizationCodeAuthenticationProcessingFilter} when resolving
|
||||
* the associated <i>Authorization Request</i> during the handling of the <i>Authorization Response</i>.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see AuthorizationRequestAttributes
|
||||
* @see HttpSessionAuthorizationRequestRepository
|
||||
*/
|
||||
public interface AuthorizationRequestRepository {
|
||||
|
||||
AuthorizationRequestAttributes loadAuthorizationRequest(HttpServletRequest request);
|
||||
|
||||
void saveAuthorizationRequest(AuthorizationRequestAttributes authorizationRequest, HttpServletRequest request);
|
||||
|
||||
AuthorizationRequestAttributes removeAuthorizationRequest(HttpServletRequest request);
|
||||
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.endpoint.AuthorizationRequestAttributes;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface AuthorizationRequestUriBuilder {
|
||||
|
||||
URI build(AuthorizationRequestAttributes authorizationRequestAttributes);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.endpoint.OAuth2Parameter;
|
||||
import org.springframework.security.oauth2.core.endpoint.ResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public class DefaultAuthorizationRequestUriBuilder implements AuthorizationRequestUriBuilder {
|
||||
|
||||
@Override
|
||||
public URI build(AuthorizationRequestAttributes authorizationRequestAttributes) {
|
||||
UriComponentsBuilder uriBuilder = UriComponentsBuilder
|
||||
.fromUriString(authorizationRequestAttributes.getAuthorizeUri())
|
||||
.queryParam(OAuth2Parameter.RESPONSE_TYPE, ResponseType.CODE.value());
|
||||
if (authorizationRequestAttributes.getRedirectUri() != null) {
|
||||
uriBuilder.queryParam(OAuth2Parameter.REDIRECT_URI, authorizationRequestAttributes.getRedirectUri());
|
||||
}
|
||||
uriBuilder
|
||||
.queryParam(OAuth2Parameter.CLIENT_ID, authorizationRequestAttributes.getClientId())
|
||||
.queryParam(OAuth2Parameter.SCOPE,
|
||||
authorizationRequestAttributes.getScopes().stream().collect(Collectors.joining(" ")))
|
||||
.queryParam(OAuth2Parameter.STATE, authorizationRequestAttributes.getState());
|
||||
|
||||
return uriBuilder.build().encode().toUri();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.crypto.codec.Base64;
|
||||
import org.springframework.security.crypto.keygen.BytesKeyGenerator;
|
||||
import org.springframework.security.crypto.keygen.KeyGenerators;
|
||||
import org.springframework.security.crypto.keygen.StringKeyGenerator;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The default implementation for generating the
|
||||
* {@link org.springframework.security.oauth2.core.endpoint.OAuth2Parameter#STATE} parameter
|
||||
* used in the <i>Authorization Request</i> and correlated in the <i>Authorization Response</i> (or <i>Error Response</i>).
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> The value of the <i>state</i> parameter is an opaque <code>String</code>
|
||||
* used by the client to prevent cross-site request forgery, as described in
|
||||
* <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-10.12">Section 10.12</a> of the specification.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public class DefaultStateGenerator implements StringKeyGenerator {
|
||||
private static final int DEFAULT_BYTE_LENGTH = 32;
|
||||
private final BytesKeyGenerator keyGenerator;
|
||||
|
||||
public DefaultStateGenerator() {
|
||||
this(DEFAULT_BYTE_LENGTH);
|
||||
}
|
||||
|
||||
public DefaultStateGenerator(int byteLength) {
|
||||
Assert.isTrue(byteLength > 0, "byteLength must be greater than 0");
|
||||
this.keyGenerator = KeyGenerators.secureRandom(byteLength);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateKey() {
|
||||
return new String(Base64.encode(keyGenerator.generateKey()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.endpoint.AuthorizationRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link AuthorizationRequestRepository} that stores
|
||||
* {@link AuthorizationRequestAttributes} in the {@link HttpSession}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see AuthorizationRequestAttributes
|
||||
*/
|
||||
public final class HttpSessionAuthorizationRequestRepository implements AuthorizationRequestRepository {
|
||||
private static final String DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME =
|
||||
HttpSessionAuthorizationRequestRepository.class.getName() + ".AUTHORIZATION_REQUEST";
|
||||
private String sessionAttributeName = DEFAULT_AUTHORIZATION_REQUEST_ATTR_NAME;
|
||||
|
||||
@Override
|
||||
public AuthorizationRequestAttributes loadAuthorizationRequest(HttpServletRequest request) {
|
||||
AuthorizationRequestAttributes authorizationRequest = null;
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
authorizationRequest = (AuthorizationRequestAttributes) session.getAttribute(this.sessionAttributeName);
|
||||
}
|
||||
return authorizationRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveAuthorizationRequest(AuthorizationRequestAttributes authorizationRequest, HttpServletRequest request) {
|
||||
if (authorizationRequest == null) {
|
||||
this.removeAuthorizationRequest(request);
|
||||
return;
|
||||
}
|
||||
request.getSession().setAttribute(this.sessionAttributeName, authorizationRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthorizationRequestAttributes removeAuthorizationRequest(HttpServletRequest request) {
|
||||
AuthorizationRequestAttributes authorizationRequest = this.loadAuthorizationRequest(request);
|
||||
if (authorizationRequest != null) {
|
||||
request.getSession().removeAttribute(this.sessionAttributeName);
|
||||
}
|
||||
return authorizationRequest;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* This exception is thrown for all <i>OAuth 2.0</i> related {@link Authentication} errors.
|
||||
*
|
||||
* <p>
|
||||
* There are a number of scenarios where an error may occur, for example:
|
||||
* <ul>
|
||||
* <li>The authorization request or token request is missing a required parameter</li>
|
||||
* <li>Missing or invalid client identifier</li>
|
||||
* <li>Invalid or mismatching redirection URI</li>
|
||||
* <li>The requested scope is invalid, unknown, or malformed</li>
|
||||
* <li>The resource owner or authorization server denied the access request</li>
|
||||
* <li>Client authentication failed</li>
|
||||
* <li>The provided authorization grant (authorization code, resource owner credentials) is invalid, expired, or revoked</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public class OAuth2AuthenticationException extends AuthenticationException {
|
||||
private OAuth2Error errorObject;
|
||||
|
||||
public OAuth2AuthenticationException(OAuth2Error errorObject, Throwable cause) {
|
||||
this(errorObject, cause.getMessage(), cause);
|
||||
}
|
||||
|
||||
public OAuth2AuthenticationException(OAuth2Error errorObject, String message) {
|
||||
super(message);
|
||||
this.setErrorObject(errorObject);
|
||||
}
|
||||
|
||||
public OAuth2AuthenticationException(OAuth2Error errorObject, String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.setErrorObject(errorObject);
|
||||
}
|
||||
|
||||
public OAuth2Error getErrorObject() {
|
||||
return errorObject;
|
||||
}
|
||||
|
||||
private void setErrorObject(OAuth2Error errorObject) {
|
||||
Assert.notNull(errorObject, "OAuth2 Error object cannot be null");
|
||||
this.errorObject = errorObject;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.user.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.AccessToken;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link AbstractAuthenticationToken}
|
||||
* that represents an <i>OAuth 2.0</i> {@link Authentication}.
|
||||
*
|
||||
* <p>
|
||||
* It associates an {@link OAuth2User}, {@link ClientRegistration} and an {@link AccessToken}.
|
||||
* This <code>Authentication</code> is considered <i>"authenticated"</i> if the {@link OAuth2User}
|
||||
* is provided in the respective constructor. This typically happens after the {@link OAuth2UserService}
|
||||
* retrieves the end-user's (resource owner) attributes from the <i>UserInfo Endpoint</i>.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see OAuth2User
|
||||
* @see ClientRegistration
|
||||
* @see AccessToken
|
||||
*/
|
||||
public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||
private final OAuth2User principal;
|
||||
private final ClientRegistration clientRegistration;
|
||||
private final AccessToken accessToken;
|
||||
|
||||
public OAuth2AuthenticationToken(ClientRegistration clientRegistration, AccessToken accessToken) {
|
||||
this(null, AuthorityUtils.NO_AUTHORITIES, clientRegistration, accessToken);
|
||||
}
|
||||
|
||||
public OAuth2AuthenticationToken(OAuth2User principal, Collection<? extends GrantedAuthority> authorities,
|
||||
ClientRegistration clientRegistration, AccessToken accessToken) {
|
||||
|
||||
super(authorities);
|
||||
Assert.notNull(clientRegistration, "clientRegistration cannot be null");
|
||||
Assert.notNull(accessToken, "accessToken cannot be null");
|
||||
this.principal = principal;
|
||||
this.clientRegistration = clientRegistration;
|
||||
this.accessToken = accessToken;
|
||||
this.setAuthenticated(principal != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPrincipal() {
|
||||
return this.principal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCredentials() {
|
||||
// Credentials are never exposed (by the Provider) for an OAuth2 User
|
||||
return "";
|
||||
}
|
||||
|
||||
public ClientRegistration getClientRegistration() {
|
||||
return this.clientRegistration;
|
||||
}
|
||||
|
||||
public AccessToken getAccessToken() {
|
||||
return this.accessToken;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.nimbus;
|
||||
|
||||
|
||||
import com.nimbusds.oauth2.sdk.*;
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
|
||||
import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
|
||||
import com.nimbusds.oauth2.sdk.auth.Secret;
|
||||
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
|
||||
import com.nimbusds.oauth2.sdk.id.ClientID;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.AccessToken;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link AuthorizationGrantTokenExchanger} that <i>"exchanges"</i>
|
||||
* an <i>authorization code</i> credential for an <i>access token</i> credential
|
||||
* at the authorization server's <i>Token Endpoint</i>.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> This implementation uses the <b>Nimbus OAuth 2.0 SDK</b> internally.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see AuthorizationCodeAuthenticationToken
|
||||
* @see TokenResponseAttributes
|
||||
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request (Authorization Code Grant)</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response (Authorization Code Grant)</a>
|
||||
*/
|
||||
public class NimbusAuthorizationCodeTokenExchanger implements AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> {
|
||||
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
|
||||
|
||||
@Override
|
||||
public TokenResponseAttributes exchange(AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken)
|
||||
throws OAuth2AuthenticationException {
|
||||
|
||||
ClientRegistration clientRegistration = authorizationCodeAuthenticationToken.getClientRegistration();
|
||||
|
||||
// Build the authorization code grant request for the token endpoint
|
||||
AuthorizationCode authorizationCode = new AuthorizationCode(authorizationCodeAuthenticationToken.getAuthorizationCode());
|
||||
URI redirectUri = this.toURI(clientRegistration.getRedirectUri());
|
||||
AuthorizationGrant authorizationCodeGrant = new AuthorizationCodeGrant(authorizationCode, redirectUri);
|
||||
URI tokenUri = this.toURI(clientRegistration.getProviderDetails().getTokenUri());
|
||||
|
||||
// Set the credentials to authenticate the client at the token endpoint
|
||||
ClientID clientId = new ClientID(clientRegistration.getClientId());
|
||||
Secret clientSecret = new Secret(clientRegistration.getClientSecret());
|
||||
ClientAuthentication clientAuthentication;
|
||||
if (ClientAuthenticationMethod.FORM.equals(clientRegistration.getClientAuthenticationMethod())) {
|
||||
clientAuthentication = new ClientSecretPost(clientId, clientSecret);
|
||||
} else {
|
||||
clientAuthentication = new ClientSecretBasic(clientId, clientSecret);
|
||||
}
|
||||
|
||||
TokenResponse tokenResponse;
|
||||
try {
|
||||
// Send the Access Token request
|
||||
TokenRequest tokenRequest = new TokenRequest(tokenUri, clientAuthentication, authorizationCodeGrant);
|
||||
HTTPRequest httpRequest = tokenRequest.toHTTPRequest();
|
||||
httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE);
|
||||
tokenResponse = TokenResponse.parse(httpRequest.send());
|
||||
} catch (ParseException pe) {
|
||||
// This error occurs if the Access Token Response is not well-formed,
|
||||
// for example, a required attribute is missing
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE), pe);
|
||||
} catch (IOException ioe) {
|
||||
// This error occurs when there is a network-related issue
|
||||
throw new AuthenticationServiceException("An error occurred while sending the Access Token Request: " +
|
||||
ioe.getMessage(), ioe);
|
||||
}
|
||||
|
||||
if (!tokenResponse.indicatesSuccess()) {
|
||||
TokenErrorResponse tokenErrorResponse = (TokenErrorResponse) tokenResponse;
|
||||
ErrorObject errorObject = tokenErrorResponse.getErrorObject();
|
||||
OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), errorObject.getDescription(),
|
||||
(errorObject.getURI() != null ? errorObject.getURI().toString() : null));
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
AccessTokenResponse accessTokenResponse = (AccessTokenResponse) tokenResponse;
|
||||
|
||||
String accessToken = accessTokenResponse.getTokens().getAccessToken().getValue();
|
||||
AccessToken.TokenType accessTokenType = null;
|
||||
if (AccessToken.TokenType.BEARER.value().equals(accessTokenResponse.getTokens().getAccessToken().getType().getValue())) {
|
||||
accessTokenType = AccessToken.TokenType.BEARER;
|
||||
}
|
||||
long expiresIn = accessTokenResponse.getTokens().getAccessToken().getLifetime();
|
||||
Set<String> scopes = Collections.emptySet();
|
||||
if (!CollectionUtils.isEmpty(accessTokenResponse.getTokens().getAccessToken().getScope())) {
|
||||
scopes = new HashSet<>(accessTokenResponse.getTokens().getAccessToken().getScope().toStringList());
|
||||
}
|
||||
Map<String, Object> additionalParameters = accessTokenResponse.getCustomParameters().entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
|
||||
return TokenResponseAttributes.withToken(accessToken)
|
||||
.tokenType(accessTokenType)
|
||||
.expiresIn(expiresIn)
|
||||
.scopes(scopes)
|
||||
.additionalParameters(additionalParameters)
|
||||
.build();
|
||||
}
|
||||
|
||||
private URI toURI(String uriStr) {
|
||||
try {
|
||||
return new URI(uriStr);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException("An error occurred parsing URI: " + uriStr, ex);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.registration;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ClientRegistration {
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.HEADER;
|
||||
private AuthorizationGrantType authorizedGrantType;
|
||||
private String redirectUri;
|
||||
private Set<String> scopes = Collections.emptySet();
|
||||
private ProviderDetails providerDetails = new ProviderDetails();
|
||||
private String clientName;
|
||||
private String clientAlias;
|
||||
|
||||
protected ClientRegistration() {
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
protected void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public String getClientSecret() {
|
||||
return this.clientSecret;
|
||||
}
|
||||
|
||||
protected void setClientSecret(String clientSecret) {
|
||||
this.clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
public ClientAuthenticationMethod getClientAuthenticationMethod() {
|
||||
return this.clientAuthenticationMethod;
|
||||
}
|
||||
|
||||
protected void setClientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) {
|
||||
this.clientAuthenticationMethod = clientAuthenticationMethod;
|
||||
}
|
||||
|
||||
public AuthorizationGrantType getAuthorizedGrantType() {
|
||||
return this.authorizedGrantType;
|
||||
}
|
||||
|
||||
protected void setAuthorizedGrantType(AuthorizationGrantType authorizedGrantType) {
|
||||
this.authorizedGrantType = authorizedGrantType;
|
||||
}
|
||||
|
||||
public String getRedirectUri() {
|
||||
return this.redirectUri;
|
||||
}
|
||||
|
||||
protected void setRedirectUri(String redirectUri) {
|
||||
this.redirectUri = redirectUri;
|
||||
}
|
||||
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
protected void setScopes(Set<String> scopes) {
|
||||
this.scopes = scopes;
|
||||
}
|
||||
|
||||
public ProviderDetails getProviderDetails() {
|
||||
return this.providerDetails;
|
||||
}
|
||||
|
||||
protected void setProviderDetails(ProviderDetails providerDetails) {
|
||||
this.providerDetails = providerDetails;
|
||||
}
|
||||
|
||||
public String getClientName() {
|
||||
return this.clientName;
|
||||
}
|
||||
|
||||
protected void setClientName(String clientName) {
|
||||
this.clientName = clientName;
|
||||
}
|
||||
|
||||
public String getClientAlias() {
|
||||
return this.clientAlias;
|
||||
}
|
||||
|
||||
protected void setClientAlias(String clientAlias) {
|
||||
this.clientAlias = clientAlias;
|
||||
}
|
||||
|
||||
public class ProviderDetails {
|
||||
private String authorizationUri;
|
||||
private String tokenUri;
|
||||
private String userInfoUri;
|
||||
|
||||
protected ProviderDetails() {
|
||||
}
|
||||
|
||||
public String getAuthorizationUri() {
|
||||
return this.authorizationUri;
|
||||
}
|
||||
|
||||
protected void setAuthorizationUri(String authorizationUri) {
|
||||
this.authorizationUri = authorizationUri;
|
||||
}
|
||||
|
||||
public String getTokenUri() {
|
||||
return this.tokenUri;
|
||||
}
|
||||
|
||||
protected void setTokenUri(String tokenUri) {
|
||||
this.tokenUri = tokenUri;
|
||||
}
|
||||
|
||||
public String getUserInfoUri() {
|
||||
return this.userInfoUri;
|
||||
}
|
||||
|
||||
protected void setUserInfoUri(String userInfoUri) {
|
||||
this.userInfoUri = userInfoUri;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
protected String clientId;
|
||||
protected String clientSecret;
|
||||
protected ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.HEADER;
|
||||
protected AuthorizationGrantType authorizedGrantType;
|
||||
protected String redirectUri;
|
||||
protected Set<String> scopes;
|
||||
protected String authorizationUri;
|
||||
protected String tokenUri;
|
||||
protected String userInfoUri;
|
||||
protected String clientName;
|
||||
protected String clientAlias;
|
||||
|
||||
public Builder(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public Builder(ClientRegistrationProperties clientRegistrationProperties) {
|
||||
this(clientRegistrationProperties.getClientId());
|
||||
this.clientSecret(clientRegistrationProperties.getClientSecret());
|
||||
this.clientAuthenticationMethod(clientRegistrationProperties.getClientAuthenticationMethod());
|
||||
this.authorizedGrantType(clientRegistrationProperties.getAuthorizedGrantType());
|
||||
this.redirectUri(clientRegistrationProperties.getRedirectUri());
|
||||
if (!CollectionUtils.isEmpty(clientRegistrationProperties.getScopes())) {
|
||||
this.scopes(clientRegistrationProperties.getScopes().stream().toArray(String[]::new));
|
||||
}
|
||||
this.authorizationUri(clientRegistrationProperties.getAuthorizationUri());
|
||||
this.tokenUri(clientRegistrationProperties.getTokenUri());
|
||||
this.userInfoUri(clientRegistrationProperties.getUserInfoUri());
|
||||
this.clientName(clientRegistrationProperties.getClientName());
|
||||
this.clientAlias(clientRegistrationProperties.getClientAlias());
|
||||
}
|
||||
|
||||
public Builder clientSecret(String clientSecret) {
|
||||
this.clientSecret = clientSecret;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder clientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) {
|
||||
this.clientAuthenticationMethod = clientAuthenticationMethod;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder authorizedGrantType(AuthorizationGrantType authorizedGrantType) {
|
||||
this.authorizedGrantType = authorizedGrantType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder redirectUri(String redirectUri) {
|
||||
this.redirectUri = redirectUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder scopes(String... scopes) {
|
||||
if (scopes != null && scopes.length > 0) {
|
||||
this.scopes = Collections.unmodifiableSet(
|
||||
new LinkedHashSet<>(Arrays.asList(scopes)));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder authorizationUri(String authorizationUri) {
|
||||
this.authorizationUri = authorizationUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder tokenUri(String tokenUri) {
|
||||
this.tokenUri = tokenUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder userInfoUri(String userInfoUri) {
|
||||
this.userInfoUri = userInfoUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder clientName(String clientName) {
|
||||
this.clientName = clientName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder clientAlias(String clientAlias) {
|
||||
this.clientAlias = clientAlias;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientRegistration build() {
|
||||
this.validateClientWithAuthorizationCodeGrantType();
|
||||
ClientRegistration clientRegistration = new ClientRegistration();
|
||||
this.setProperties(clientRegistration);
|
||||
return clientRegistration;
|
||||
}
|
||||
|
||||
protected void setProperties(ClientRegistration clientRegistration) {
|
||||
clientRegistration.setClientId(this.clientId);
|
||||
clientRegistration.setClientSecret(this.clientSecret);
|
||||
clientRegistration.setClientAuthenticationMethod(this.clientAuthenticationMethod);
|
||||
clientRegistration.setAuthorizedGrantType(this.authorizedGrantType);
|
||||
clientRegistration.setRedirectUri(this.redirectUri);
|
||||
clientRegistration.setScopes(this.scopes);
|
||||
|
||||
ProviderDetails providerDetails = clientRegistration.new ProviderDetails();
|
||||
providerDetails.setAuthorizationUri(this.authorizationUri);
|
||||
providerDetails.setTokenUri(this.tokenUri);
|
||||
providerDetails.setUserInfoUri(this.userInfoUri);
|
||||
clientRegistration.setProviderDetails(providerDetails);
|
||||
|
||||
clientRegistration.setClientName(this.clientName);
|
||||
clientRegistration.setClientAlias(this.clientAlias);
|
||||
}
|
||||
|
||||
protected void validateClientWithAuthorizationCodeGrantType() {
|
||||
Assert.isTrue(AuthorizationGrantType.AUTHORIZATION_CODE.equals(this.authorizedGrantType),
|
||||
"authorizedGrantType must be " + AuthorizationGrantType.AUTHORIZATION_CODE.value());
|
||||
Assert.hasText(this.clientId, "clientId cannot be empty");
|
||||
Assert.hasText(this.clientSecret, "clientSecret cannot be empty");
|
||||
Assert.notNull(this.clientAuthenticationMethod, "clientAuthenticationMethod cannot be null");
|
||||
Assert.hasText(this.redirectUri, "redirectUri cannot be empty");
|
||||
Assert.notEmpty(this.scopes, "scopes cannot be empty");
|
||||
Assert.hasText(this.authorizationUri, "authorizationUri cannot be empty");
|
||||
Assert.hasText(this.tokenUri, "tokenUri cannot be empty");
|
||||
Assert.hasText(this.userInfoUri, "userInfoUri cannot be empty");
|
||||
Assert.hasText(this.clientName, "clientName cannot be empty");
|
||||
Assert.hasText(this.clientAlias, "clientAlias cannot be empty");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.registration;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ClientRegistrationProperties {
|
||||
private String clientId;
|
||||
private String clientSecret;
|
||||
private ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.HEADER;
|
||||
private AuthorizationGrantType authorizedGrantType;
|
||||
private String redirectUri;
|
||||
private Set<String> scopes;
|
||||
private String authorizationUri;
|
||||
private String tokenUri;
|
||||
private String userInfoUri;
|
||||
private String clientName;
|
||||
private String clientAlias;
|
||||
|
||||
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public String getClientSecret() {
|
||||
return this.clientSecret;
|
||||
}
|
||||
|
||||
public void setClientSecret(String clientSecret) {
|
||||
this.clientSecret = clientSecret;
|
||||
}
|
||||
|
||||
public ClientAuthenticationMethod getClientAuthenticationMethod() {
|
||||
return this.clientAuthenticationMethod;
|
||||
}
|
||||
|
||||
public void setClientAuthenticationMethod(ClientAuthenticationMethod clientAuthenticationMethod) {
|
||||
this.clientAuthenticationMethod = clientAuthenticationMethod;
|
||||
}
|
||||
|
||||
public AuthorizationGrantType getAuthorizedGrantType() {
|
||||
return this.authorizedGrantType;
|
||||
}
|
||||
|
||||
public void setAuthorizedGrantType(AuthorizationGrantType authorizedGrantType) {
|
||||
this.authorizedGrantType = authorizedGrantType;
|
||||
}
|
||||
|
||||
public String getRedirectUri() {
|
||||
return this.redirectUri;
|
||||
}
|
||||
|
||||
public void setRedirectUri(String redirectUri) {
|
||||
this.redirectUri = redirectUri;
|
||||
}
|
||||
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
public void setScopes(Set<String> scopes) {
|
||||
this.scopes = scopes;
|
||||
}
|
||||
|
||||
public String getAuthorizationUri() {
|
||||
return this.authorizationUri;
|
||||
}
|
||||
|
||||
public void setAuthorizationUri(String authorizationUri) {
|
||||
this.authorizationUri = authorizationUri;
|
||||
}
|
||||
|
||||
public String getTokenUri() {
|
||||
return this.tokenUri;
|
||||
}
|
||||
|
||||
public void setTokenUri(String tokenUri) {
|
||||
this.tokenUri = tokenUri;
|
||||
}
|
||||
|
||||
public String getUserInfoUri() {
|
||||
return this.userInfoUri;
|
||||
}
|
||||
|
||||
public void setUserInfoUri(String userInfoUri) {
|
||||
this.userInfoUri = userInfoUri;
|
||||
}
|
||||
|
||||
public String getClientName() {
|
||||
return this.clientName;
|
||||
}
|
||||
|
||||
public void setClientName(String clientName) {
|
||||
this.clientName = clientName;
|
||||
}
|
||||
|
||||
public String getClientAlias() {
|
||||
return this.clientAlias;
|
||||
}
|
||||
|
||||
public void setClientAlias(String clientAlias) {
|
||||
this.clientAlias = clientAlias;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.registration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public interface ClientRegistrationRepository {
|
||||
|
||||
ClientRegistration getRegistrationByClientId(String clientId);
|
||||
|
||||
ClientRegistration getRegistrationByClientAlias(String clientAlias);
|
||||
|
||||
List<ClientRegistration> getRegistrations();
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.registration;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public final class InMemoryClientRegistrationRepository implements ClientRegistrationRepository {
|
||||
private final List<ClientRegistration> clientRegistrations;
|
||||
|
||||
public InMemoryClientRegistrationRepository(List<ClientRegistration> clientRegistrations) {
|
||||
Assert.notEmpty(clientRegistrations, "clientRegistrations cannot be empty");
|
||||
this.clientRegistrations = Collections.unmodifiableList(clientRegistrations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRegistration getRegistrationByClientId(String clientId) {
|
||||
Optional<ClientRegistration> clientRegistration =
|
||||
this.clientRegistrations.stream()
|
||||
.filter(c -> c.getClientId().equals(clientId))
|
||||
.findFirst();
|
||||
return clientRegistration.isPresent() ? clientRegistration.get() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRegistration getRegistrationByClientAlias(String clientAlias) {
|
||||
Optional<ClientRegistration> clientRegistration =
|
||||
this.clientRegistrations.stream()
|
||||
.filter(c -> c.getClientAlias().equals(clientAlias))
|
||||
.findFirst();
|
||||
return clientRegistration.isPresent() ? clientRegistration.get() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ClientRegistration> getRegistrations() {
|
||||
return this.clientRegistrations;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.user;
|
||||
|
||||
import org.springframework.security.core.AuthenticatedPrincipal;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.oauth2.oidc.user.UserInfo;
|
||||
|
||||
/**
|
||||
* Implementations of this interface are responsible for obtaining
|
||||
* the end-user's (resource owner) attributes from the <i>UserInfo Endpoint</i>
|
||||
* using the provided {@link OAuth2AuthenticationToken#getAccessToken()}
|
||||
* and returning an {@link AuthenticatedPrincipal} in the form of an {@link OAuth2User}
|
||||
* (for a standard <i>OAuth 2.0 Provider</i>) or {@link UserInfo} (for an <i>OpenID Connect 1.0 Provider</i>).
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see OAuth2AuthenticationToken
|
||||
* @see AuthenticatedPrincipal
|
||||
* @see OAuth2User
|
||||
* @see UserInfo
|
||||
*/
|
||||
public interface OAuth2UserService {
|
||||
|
||||
OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException;
|
||||
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.user.converter;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class AbstractOAuth2UserConverter<T extends OAuth2User> implements Converter<ClientHttpResponse, T> {
|
||||
private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
|
||||
|
||||
protected AbstractOAuth2UserConverter() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public final T convert(ClientHttpResponse clientHttpResponse) {
|
||||
Map<String, Object> userAttributes;
|
||||
|
||||
try {
|
||||
userAttributes = (Map<String, Object>) this.jackson2HttpMessageConverter.read(Map.class, clientHttpResponse);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalArgumentException("An error occurred reading the UserInfo response: " + ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
return this.convert(userAttributes);
|
||||
}
|
||||
|
||||
protected abstract T convert(Map<String, Object> userAttributes);
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.user.converter;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public final class CustomOAuth2UserConverter<T extends OAuth2User> implements Converter<ClientHttpResponse, T> {
|
||||
private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
|
||||
private final Class<T> customType;
|
||||
|
||||
public CustomOAuth2UserConverter(Class<T> customType) {
|
||||
this.customType = customType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T convert(ClientHttpResponse clientHttpResponse) {
|
||||
T user;
|
||||
|
||||
try {
|
||||
user = (T) this.jackson2HttpMessageConverter.read(this.customType, clientHttpResponse);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalArgumentException("An error occurred reading the UserInfo response: " + ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.user.converter;
|
||||
|
||||
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public final class OAuth2UserConverter extends AbstractOAuth2UserConverter<OAuth2User> {
|
||||
private final String nameAttributeKey;
|
||||
|
||||
public OAuth2UserConverter(String nameAttributeKey) {
|
||||
Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty");
|
||||
this.nameAttributeKey = nameAttributeKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected OAuth2User convert(Map<String, Object> userAttributes) {
|
||||
return new DefaultOAuth2User(userAttributes, this.nameAttributeKey);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.user.converter;
|
||||
|
||||
import org.springframework.security.oauth2.oidc.user.DefaultUserInfo;
|
||||
import org.springframework.security.oauth2.oidc.user.UserInfo;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public final class UserInfoConverter extends AbstractOAuth2UserConverter<UserInfo> {
|
||||
|
||||
@Override
|
||||
protected UserInfo convert(Map<String, Object> userAttributes) {
|
||||
return new DefaultUserInfo(userAttributes);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.user.nimbus;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.client.AbstractClientHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
final class NimbusClientHttpResponse extends AbstractClientHttpResponse {
|
||||
private final HTTPResponse httpResponse;
|
||||
private final HttpHeaders headers;
|
||||
|
||||
NimbusClientHttpResponse(HTTPResponse httpResponse) {
|
||||
Assert.notNull(httpResponse, "httpResponse cannot be null");
|
||||
this.httpResponse = httpResponse;
|
||||
this.headers = new HttpHeaders();
|
||||
this.headers.setAll(httpResponse.getHeaders());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRawStatusCode() throws IOException {
|
||||
return this.httpResponse.getStatusCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStatusText() throws IOException {
|
||||
return String.valueOf(this.getRawStatusCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getBody() throws IOException {
|
||||
InputStream inputStream = new ByteArrayInputStream(
|
||||
this.httpResponse.getContent().getBytes(Charset.forName("UTF-8")));
|
||||
return inputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return this.headers;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.user.nimbus;
|
||||
|
||||
import com.nimbusds.oauth2.sdk.ErrorObject;
|
||||
import com.nimbusds.oauth2.sdk.ParseException;
|
||||
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
|
||||
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
|
||||
import com.nimbusds.oauth2.sdk.token.BearerAccessToken;
|
||||
import com.nimbusds.openid.connect.sdk.UserInfoErrorResponse;
|
||||
import com.nimbusds.openid.connect.sdk.UserInfoRequest;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.core.AuthenticatedPrincipal;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.user.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.oauth2.oidc.user.UserInfo;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link OAuth2UserService} that uses the <b>Nimbus OAuth 2.0 SDK</b> internally.
|
||||
*
|
||||
* <p>
|
||||
* This implementation uses a <code>Map</code> of <code>Converter</code>'s <i>keyed</i> by <code>URI</code>.
|
||||
* The <code>URI</code> represents the <i>UserInfo Endpoint</i> address and the mapped <code>Converter</code>
|
||||
* is capable of converting the <i>UserInfo Response</i> to either an
|
||||
* {@link OAuth2User} (for a standard <i>OAuth 2.0 Provider</i>) or
|
||||
* {@link UserInfo} (for an <i>OpenID Connect 1.0 Provider</i>).
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see OAuth2AuthenticationToken
|
||||
* @see AuthenticatedPrincipal
|
||||
* @see OAuth2User
|
||||
* @see UserInfo
|
||||
* @see Converter
|
||||
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
|
||||
*/
|
||||
public class NimbusOAuth2UserService implements OAuth2UserService {
|
||||
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
|
||||
private final Map<URI, Converter<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters;
|
||||
|
||||
public NimbusOAuth2UserService(Map<URI, Converter<ClientHttpResponse, ? extends OAuth2User>> userInfoTypeConverters) {
|
||||
Assert.notEmpty(userInfoTypeConverters, "userInfoTypeConverters cannot be empty");
|
||||
this.userInfoTypeConverters = new HashMap<>(userInfoTypeConverters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2User loadUser(OAuth2AuthenticationToken token) throws OAuth2AuthenticationException {
|
||||
OAuth2User user;
|
||||
|
||||
try {
|
||||
ClientRegistration clientRegistration = token.getClientRegistration();
|
||||
|
||||
URI userInfoUri;
|
||||
try {
|
||||
userInfoUri = new URI(clientRegistration.getProviderDetails().getUserInfoUri());
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException("An error occurred parsing the userInfo URI: " +
|
||||
clientRegistration.getProviderDetails().getUserInfoUri(), ex);
|
||||
}
|
||||
|
||||
Converter<ClientHttpResponse, ? extends OAuth2User> userInfoConverter = this.userInfoTypeConverters.get(userInfoUri);
|
||||
if (userInfoConverter == null) {
|
||||
throw new IllegalArgumentException("There is no available User Info converter for " + userInfoUri.toString());
|
||||
}
|
||||
|
||||
BearerAccessToken accessToken = new BearerAccessToken(token.getAccessToken().getTokenValue());
|
||||
|
||||
// Request the User Info
|
||||
UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken);
|
||||
HTTPRequest httpRequest = userInfoRequest.toHTTPRequest();
|
||||
httpRequest.setAccept(MediaType.APPLICATION_JSON_VALUE);
|
||||
HTTPResponse httpResponse = httpRequest.send();
|
||||
|
||||
if (httpResponse.getStatusCode() != HTTPResponse.SC_OK) {
|
||||
UserInfoErrorResponse userInfoErrorResponse = UserInfoErrorResponse.parse(httpResponse);
|
||||
ErrorObject errorObject = userInfoErrorResponse.getErrorObject();
|
||||
OAuth2Error oauth2Error = new OAuth2Error(errorObject.getCode(), errorObject.getDescription(),
|
||||
(errorObject.getURI() != null ? errorObject.getURI().toString() : null));
|
||||
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
|
||||
}
|
||||
|
||||
user = userInfoConverter.convert(new NimbusClientHttpResponse(httpResponse));
|
||||
|
||||
} catch (ParseException ex) {
|
||||
// This error occurs if the User Info Response is not well-formed or invalid
|
||||
throw new OAuth2AuthenticationException(new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE), ex);
|
||||
} catch (IOException ex) {
|
||||
// This error occurs when there is a network-related issue
|
||||
throw new AuthenticationServiceException("An error occurred while sending the User Info Request: " +
|
||||
ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.converter;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
|
||||
import org.springframework.security.oauth2.core.endpoint.AuthorizationCodeAuthorizationResponseAttributes;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public final class AuthorizationCodeAuthorizationResponseAttributesConverter implements Converter<HttpServletRequest, AuthorizationCodeAuthorizationResponseAttributes> {
|
||||
|
||||
@Override
|
||||
public AuthorizationCodeAuthorizationResponseAttributes convert(HttpServletRequest request) {
|
||||
AuthorizationCodeAuthorizationResponseAttributes response;
|
||||
|
||||
String code = request.getParameter(OAuth2Parameter.CODE);
|
||||
Assert.hasText(code, OAuth2Parameter.CODE + " attribute is required");
|
||||
|
||||
String state = request.getParameter(OAuth2Parameter.STATE);
|
||||
|
||||
response = new AuthorizationCodeAuthorizationResponseAttributes(code, state);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.converter;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.security.oauth2.core.endpoint.ErrorResponseAttributes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public final class ErrorResponseAttributesConverter implements Converter<HttpServletRequest, ErrorResponseAttributes> {
|
||||
|
||||
@Override
|
||||
public ErrorResponseAttributes convert(HttpServletRequest request) {
|
||||
ErrorResponseAttributes response;
|
||||
|
||||
String errorCode = request.getParameter(OAuth2Parameter.ERROR);
|
||||
if (!StringUtils.hasText(errorCode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String description = request.getParameter(OAuth2Parameter.ERROR_DESCRIPTION);
|
||||
String uri = request.getParameter(OAuth2Parameter.ERROR_URI);
|
||||
String state = request.getParameter(OAuth2Parameter.STATE);
|
||||
|
||||
response = ErrorResponseAttributes.withErrorCode(errorCode)
|
||||
.description(description)
|
||||
.uri(uri)
|
||||
.state(state)
|
||||
.build();
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
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.AuthenticationException;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.security.oauth2.client.authentication.TestUtil.*;
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCodeAuthenticationProcessingFilter}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class AuthorizationCodeAuthenticationProcessingFilterTests {
|
||||
|
||||
@Test
|
||||
public void doFilterWhenNotAuthorizationCodeResponseThenContinueChain() throws Exception {
|
||||
ClientRegistration clientRegistration = googleClientRegistration();
|
||||
|
||||
AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
|
||||
|
||||
String requestURI = "/path";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestURI);
|
||||
request.setServletPath(requestURI);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
verify(filter, never()).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationCodeErrorResponseThenAuthenticationFailureHandlerIsCalled() throws Exception {
|
||||
ClientRegistration clientRegistration = githubClientRegistration();
|
||||
|
||||
AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
|
||||
AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
|
||||
filter.setAuthenticationFailureHandler(failureHandler);
|
||||
|
||||
MockHttpServletRequest request = this.setupRequest(clientRegistration);
|
||||
String errorCode = OAuth2Error.INVALID_GRANT_ERROR_CODE;
|
||||
request.addParameter(OAuth2Parameter.ERROR, errorCode);
|
||||
request.addParameter(OAuth2Parameter.STATE, "some state");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
|
||||
any(AuthenticationException.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationCodeSuccessResponseThenAuthenticationSuccessHandlerIsCalled() throws Exception {
|
||||
TestingAuthenticationToken authentication = new TestingAuthenticationToken("joe", "password", "user", "admin");
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
when(authenticationManager.authenticate(any(Authentication.class))).thenReturn(authentication);
|
||||
|
||||
ClientRegistration clientRegistration = githubClientRegistration();
|
||||
|
||||
AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(authenticationManager, clientRegistration));
|
||||
AuthenticationSuccessHandler successHandler = mock(AuthenticationSuccessHandler.class);
|
||||
filter.setAuthenticationSuccessHandler(successHandler);
|
||||
AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
|
||||
filter.setAuthorizationRequestRepository(authorizationRequestRepository);
|
||||
|
||||
MockHttpServletRequest request = this.setupRequest(clientRegistration);
|
||||
String authCode = "some code";
|
||||
String state = "some state";
|
||||
request.addParameter(OAuth2Parameter.CODE, authCode);
|
||||
request.addParameter(OAuth2Parameter.STATE, state);
|
||||
setupAuthorizationRequest(authorizationRequestRepository, request, clientRegistration, state);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
|
||||
ArgumentCaptor<Authentication> authenticationArgCaptor = ArgumentCaptor.forClass(Authentication.class);
|
||||
verify(successHandler).onAuthenticationSuccess(any(HttpServletRequest.class), any(HttpServletResponse.class),
|
||||
authenticationArgCaptor.capture());
|
||||
assertThat(authenticationArgCaptor.getValue()).isEqualTo(authentication);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationCodeSuccessResponseAndNoMatchingAuthorizationRequestThenThrowOAuth2AuthenticationExceptionAuthorizationRequestNotFound() throws Exception {
|
||||
ClientRegistration clientRegistration = githubClientRegistration();
|
||||
|
||||
AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
|
||||
AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
|
||||
filter.setAuthenticationFailureHandler(failureHandler);
|
||||
|
||||
MockHttpServletRequest request = this.setupRequest(clientRegistration);
|
||||
String authCode = "some code";
|
||||
String state = "some state";
|
||||
request.addParameter(OAuth2Parameter.CODE, authCode);
|
||||
request.addParameter(OAuth2Parameter.STATE, state);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(filter, failureHandler, "authorization_request_not_found");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationCodeSuccessResponseWithInvalidStateParamThenThrowOAuth2AuthenticationExceptionInvalidStateParameter() throws Exception {
|
||||
ClientRegistration clientRegistration = githubClientRegistration();
|
||||
|
||||
AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
|
||||
AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
|
||||
filter.setAuthenticationFailureHandler(failureHandler);
|
||||
AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
|
||||
filter.setAuthorizationRequestRepository(authorizationRequestRepository);
|
||||
|
||||
MockHttpServletRequest request = this.setupRequest(clientRegistration);
|
||||
String authCode = "some code";
|
||||
String state = "some other state";
|
||||
request.addParameter(OAuth2Parameter.CODE, authCode);
|
||||
request.addParameter(OAuth2Parameter.STATE, state);
|
||||
setupAuthorizationRequest(authorizationRequestRepository, request, clientRegistration, "some state");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(filter, failureHandler, "invalid_state_parameter");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenAuthorizationCodeSuccessResponseWithInvalidRedirectUriParamThenThrowOAuth2AuthenticationExceptionInvalidRedirectUriParameter() throws Exception {
|
||||
ClientRegistration clientRegistration = githubClientRegistration();
|
||||
|
||||
AuthorizationCodeAuthenticationProcessingFilter filter = spy(setupFilter(clientRegistration));
|
||||
AuthenticationFailureHandler failureHandler = mock(AuthenticationFailureHandler.class);
|
||||
filter.setAuthenticationFailureHandler(failureHandler);
|
||||
AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
|
||||
filter.setAuthorizationRequestRepository(authorizationRequestRepository);
|
||||
|
||||
MockHttpServletRequest request = this.setupRequest(clientRegistration);
|
||||
request.setRequestURI(request.getRequestURI() + "-other");
|
||||
String authCode = "some code";
|
||||
String state = "some state";
|
||||
request.addParameter(OAuth2Parameter.CODE, authCode);
|
||||
request.addParameter(OAuth2Parameter.STATE, state);
|
||||
setupAuthorizationRequest(authorizationRequestRepository, request, clientRegistration, state);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(filter, failureHandler, "invalid_redirect_uri_parameter");
|
||||
}
|
||||
|
||||
private void verifyThrowsOAuth2AuthenticationExceptionWithErrorCode(AuthorizationCodeAuthenticationProcessingFilter filter,
|
||||
AuthenticationFailureHandler failureHandler,
|
||||
String errorCode) throws Exception {
|
||||
|
||||
verify(filter).attemptAuthentication(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
|
||||
ArgumentCaptor<AuthenticationException> authenticationExceptionArgCaptor =
|
||||
ArgumentCaptor.forClass(AuthenticationException.class);
|
||||
verify(failureHandler).onAuthenticationFailure(any(HttpServletRequest.class), any(HttpServletResponse.class),
|
||||
authenticationExceptionArgCaptor.capture());
|
||||
assertThat(authenticationExceptionArgCaptor.getValue()).isInstanceOf(OAuth2AuthenticationException.class);
|
||||
OAuth2AuthenticationException oauth2AuthenticationException =
|
||||
(OAuth2AuthenticationException)authenticationExceptionArgCaptor.getValue();
|
||||
assertThat(oauth2AuthenticationException.getErrorObject()).isNotNull();
|
||||
assertThat(oauth2AuthenticationException.getErrorObject().getErrorCode()).isEqualTo(errorCode);
|
||||
}
|
||||
|
||||
private AuthorizationCodeAuthenticationProcessingFilter setupFilter(ClientRegistration... clientRegistrations) throws Exception {
|
||||
AuthenticationManager authenticationManager = mock(AuthenticationManager.class);
|
||||
|
||||
return setupFilter(authenticationManager, clientRegistrations);
|
||||
}
|
||||
|
||||
private AuthorizationCodeAuthenticationProcessingFilter setupFilter(
|
||||
AuthenticationManager authenticationManager, ClientRegistration... clientRegistrations) throws Exception {
|
||||
|
||||
ClientRegistrationRepository clientRegistrationRepository = clientRegistrationRepository(clientRegistrations);
|
||||
|
||||
AuthorizationCodeAuthenticationProcessingFilter filter = new AuthorizationCodeAuthenticationProcessingFilter();
|
||||
filter.setClientRegistrationRepository(clientRegistrationRepository);
|
||||
filter.setAuthenticationManager(authenticationManager);
|
||||
|
||||
return filter;
|
||||
}
|
||||
|
||||
private void setupAuthorizationRequest(AuthorizationRequestRepository authorizationRequestRepository,
|
||||
HttpServletRequest request,
|
||||
ClientRegistration clientRegistration,
|
||||
String state) {
|
||||
|
||||
AuthorizationRequestAttributes authorizationRequestAttributes =
|
||||
AuthorizationRequestAttributes.withAuthorizationCode()
|
||||
.clientId(clientRegistration.getClientId())
|
||||
.authorizeUri(clientRegistration.getProviderDetails().getAuthorizationUri())
|
||||
.redirectUri(clientRegistration.getRedirectUri())
|
||||
.scopes(clientRegistration.getScopes())
|
||||
.state(state)
|
||||
.build();
|
||||
|
||||
authorizationRequestRepository.saveAuthorizationRequest(authorizationRequestAttributes, request);
|
||||
}
|
||||
|
||||
private MockHttpServletRequest setupRequest(ClientRegistration clientRegistration) {
|
||||
String requestURI = AUTHORIZE_BASE_URI + "/" + clientRegistration.getClientAlias();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestURI);
|
||||
request.setScheme(DEFAULT_SCHEME);
|
||||
request.setServerName(DEFAULT_SERVER_NAME);
|
||||
request.setServerPort(DEFAULT_SERVER_PORT);
|
||||
request.setServletPath(requestURI);
|
||||
return request;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.endpoint.AuthorizationRequestAttributes;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.net.URI;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.security.oauth2.client.authentication.TestUtil.*;
|
||||
|
||||
/**
|
||||
* Tests {@link AuthorizationCodeRequestRedirectFilter}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class AuthorizationCodeRequestRedirectFilterTests {
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorWhenClientRegistrationRepositoryIsNullThenThrowIllegalArgumentException() {
|
||||
new AuthorizationCodeRequestRedirectFilter(null, mock(AuthorizationRequestUriBuilder.class));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void constructorWhenAuthorizationRequestUriBuilderIsNullThenThrowIllegalArgumentException() {
|
||||
new AuthorizationCodeRequestRedirectFilter(mock(ClientRegistrationRepository.class), null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenRequestDoesNotMatchClientThenContinueChain() throws Exception {
|
||||
ClientRegistration clientRegistration = googleClientRegistration();
|
||||
String authorizationUri = clientRegistration.getProviderDetails().getAuthorizationUri().toString();
|
||||
AuthorizationCodeRequestRedirectFilter filter =
|
||||
setupFilter(authorizationUri, clientRegistration);
|
||||
|
||||
String requestURI = "/path";
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestURI);
|
||||
request.setServletPath(requestURI);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verify(filterChain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenRequestMatchesClientThenRedirectForAuthorization() throws Exception {
|
||||
ClientRegistration clientRegistration = googleClientRegistration();
|
||||
String authorizationUri = clientRegistration.getProviderDetails().getAuthorizationUri().toString();
|
||||
AuthorizationCodeRequestRedirectFilter filter =
|
||||
setupFilter(authorizationUri, clientRegistration);
|
||||
|
||||
String requestUri = AUTHORIZATION_BASE_URI + "/" + clientRegistration.getClientAlias();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyZeroInteractions(filterChain); // Request should not proceed up the chain
|
||||
|
||||
assertThat(response.getRedirectedUrl()).isEqualTo(authorizationUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void doFilterWhenRequestMatchesClientThenAuthorizationRequestSavedInSession() throws Exception {
|
||||
ClientRegistration clientRegistration = githubClientRegistration();
|
||||
String authorizationUri = clientRegistration.getProviderDetails().getAuthorizationUri().toString();
|
||||
AuthorizationCodeRequestRedirectFilter filter =
|
||||
setupFilter(authorizationUri, clientRegistration);
|
||||
AuthorizationRequestRepository authorizationRequestRepository = new HttpSessionAuthorizationRequestRepository();
|
||||
filter.setAuthorizationRequestRepository(authorizationRequestRepository);
|
||||
|
||||
String requestUri = AUTHORIZATION_BASE_URI + "/" + clientRegistration.getClientAlias();
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
request.setServletPath(requestUri);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
FilterChain filterChain = mock(FilterChain.class);
|
||||
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
verifyZeroInteractions(filterChain); // Request should not proceed up the chain
|
||||
|
||||
// The authorization request attributes are saved in the session before the redirect happens
|
||||
AuthorizationRequestAttributes authorizationRequestAttributes =
|
||||
authorizationRequestRepository.loadAuthorizationRequest(request);
|
||||
assertThat(authorizationRequestAttributes).isNotNull();
|
||||
|
||||
assertThat(authorizationRequestAttributes.getAuthorizeUri()).isNotNull();
|
||||
assertThat(authorizationRequestAttributes.getGrantType()).isNotNull();
|
||||
assertThat(authorizationRequestAttributes.getResponseType()).isNotNull();
|
||||
assertThat(authorizationRequestAttributes.getClientId()).isNotNull();
|
||||
assertThat(authorizationRequestAttributes.getRedirectUri()).isNotNull();
|
||||
assertThat(authorizationRequestAttributes.getScopes()).isNotNull();
|
||||
assertThat(authorizationRequestAttributes.getState()).isNotNull();
|
||||
}
|
||||
|
||||
private AuthorizationCodeRequestRedirectFilter setupFilter(String authorizationUri,
|
||||
ClientRegistration... clientRegistrations) throws Exception {
|
||||
|
||||
AuthorizationRequestUriBuilder authorizationUriBuilder = mock(AuthorizationRequestUriBuilder.class);
|
||||
URI authorizationURI = new URI(authorizationUri);
|
||||
when(authorizationUriBuilder.build(any(AuthorizationRequestAttributes.class))).thenReturn(authorizationURI);
|
||||
|
||||
return setupFilter(authorizationUriBuilder, clientRegistrations);
|
||||
}
|
||||
|
||||
private AuthorizationCodeRequestRedirectFilter setupFilter(AuthorizationRequestUriBuilder authorizationUriBuilder,
|
||||
ClientRegistration... clientRegistrations) throws Exception {
|
||||
|
||||
ClientRegistrationRepository clientRegistrationRepository = clientRegistrationRepository(clientRegistrations);
|
||||
|
||||
AuthorizationCodeRequestRedirectFilter filter = new AuthorizationCodeRequestRedirectFilter(
|
||||
clientRegistrationRepository, authorizationUriBuilder);
|
||||
|
||||
return filter;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationProperties;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
class TestUtil {
|
||||
static final String DEFAULT_SCHEME = "https";
|
||||
static final String DEFAULT_SERVER_NAME = "localhost";
|
||||
static final int DEFAULT_SERVER_PORT = 8080;
|
||||
static final String DEFAULT_SERVER_URL = DEFAULT_SCHEME + "://" + DEFAULT_SERVER_NAME + ":" + DEFAULT_SERVER_PORT;
|
||||
static final String AUTHORIZATION_BASE_URI = "/oauth2/authorization/code";
|
||||
static final String AUTHORIZE_BASE_URI = "/oauth2/authorize/code";
|
||||
static final String GOOGLE_CLIENT_ALIAS = "google";
|
||||
static final String GITHUB_CLIENT_ALIAS = "github";
|
||||
|
||||
static ClientRegistrationRepository clientRegistrationRepository(ClientRegistration... clientRegistrations) {
|
||||
return new InMemoryClientRegistrationRepository(Arrays.asList(clientRegistrations));
|
||||
}
|
||||
|
||||
static ClientRegistration googleClientRegistration() {
|
||||
return googleClientRegistration(DEFAULT_SERVER_URL + AUTHORIZE_BASE_URI + "/" + GOOGLE_CLIENT_ALIAS);
|
||||
}
|
||||
|
||||
static ClientRegistration googleClientRegistration(String redirectUri) {
|
||||
ClientRegistrationProperties clientRegistrationProperties = new ClientRegistrationProperties();
|
||||
clientRegistrationProperties.setClientId("google-client-id");
|
||||
clientRegistrationProperties.setClientSecret("secret");
|
||||
clientRegistrationProperties.setAuthorizedGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
clientRegistrationProperties.setClientName("Google Client");
|
||||
clientRegistrationProperties.setClientAlias(GOOGLE_CLIENT_ALIAS);
|
||||
clientRegistrationProperties.setAuthorizationUri("https://accounts.google.com/o/oauth2/auth");
|
||||
clientRegistrationProperties.setTokenUri("https://accounts.google.com/o/oauth2/token");
|
||||
clientRegistrationProperties.setUserInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
|
||||
clientRegistrationProperties.setRedirectUri(redirectUri);
|
||||
clientRegistrationProperties.setScopes(Arrays.stream(new String[] {"openid", "email", "profile"}).collect(Collectors.toSet()));
|
||||
return new ClientRegistration.Builder(clientRegistrationProperties).build();
|
||||
}
|
||||
|
||||
static ClientRegistration githubClientRegistration() {
|
||||
return githubClientRegistration(DEFAULT_SERVER_URL + AUTHORIZE_BASE_URI + "/" + GITHUB_CLIENT_ALIAS);
|
||||
}
|
||||
|
||||
static ClientRegistration githubClientRegistration(String redirectUri) {
|
||||
ClientRegistrationProperties clientRegistrationProperties = new ClientRegistrationProperties();
|
||||
clientRegistrationProperties.setClientId("github-client-id");
|
||||
clientRegistrationProperties.setClientSecret("secret");
|
||||
clientRegistrationProperties.setAuthorizedGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
clientRegistrationProperties.setClientName("GitHub Client");
|
||||
clientRegistrationProperties.setClientAlias(GITHUB_CLIENT_ALIAS);
|
||||
clientRegistrationProperties.setAuthorizationUri("https://github.com/login/oauth/authorize");
|
||||
clientRegistrationProperties.setTokenUri("https://github.com/login/oauth/access_token");
|
||||
clientRegistrationProperties.setUserInfoUri("https://api.github.com/user");
|
||||
clientRegistrationProperties.setRedirectUri(redirectUri);
|
||||
clientRegistrationProperties.setScopes(Arrays.stream(new String[] {"user"}).collect(Collectors.toSet()));
|
||||
return new ClientRegistration.Builder(clientRegistrationProperties).build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-core</artifactId>
|
||||
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||
<name>spring-security-oauth2-core</name>
|
||||
<description>spring-security-oauth2-core</description>
|
||||
<url>http://spring.io/spring-security</url>
|
||||
<organization>
|
||||
<name>spring.io</name>
|
||||
<url>http://spring.io/</url>
|
||||
</organization>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>The Apache Software License, Version 2.0</name>
|
||||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer>
|
||||
<id>rwinch</id>
|
||||
<name>Rob Winch</name>
|
||||
<email>rwinch@pivotal.io</email>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>jgrandja</id>
|
||||
<name>Joe Grandja</name>
|
||||
<email>jgrandja@pivotal.io</email>
|
||||
</developer>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection>scm:git:git://github.com/spring-projects/spring-security</connection>
|
||||
<developerConnection>scm:git:git://github.com/spring-projects/spring-security</developerConnection>
|
||||
<url>https://github.com/spring-projects/spring-security</url>
|
||||
</scm>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-framework-bom</artifactId>
|
||||
<version>4.3.5.RELEASE</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-core</artifactId>
|
||||
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
<scope>compile</scope>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<groupId>commons-logging</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<version>1.2</version>
|
||||
<scope>compile</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.6.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>1.10.19</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jcl-over-slf4j</artifactId>
|
||||
<version>1.7.7</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshot</id>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,9 @@
|
|||
apply plugin: 'io.spring.convention.spring-module'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-security-core')
|
||||
compile springCoreDependency
|
||||
compile 'org.springframework:spring-web'
|
||||
|
||||
provided 'javax.servlet:javax.servlet-api'
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core;
|
||||
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* Base class for <i>Security Token</i> implementations.
|
||||
*
|
||||
* <p>
|
||||
* It is highly recommended that implementations be immutable.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public abstract class AbstractToken implements Serializable {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||
private final String tokenValue;
|
||||
private final Instant issuedAt;
|
||||
private final Instant expiresAt;
|
||||
|
||||
protected AbstractToken(String tokenValue, Instant issuedAt, Instant expiresAt) {
|
||||
Assert.hasLength(tokenValue, "tokenValue cannot be empty");
|
||||
Assert.notNull(issuedAt, "issuedAt cannot be null");
|
||||
Assert.notNull(expiresAt, "expiresAt cannot be null");
|
||||
this.tokenValue = tokenValue;
|
||||
this.issuedAt = issuedAt;
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public String getTokenValue() {
|
||||
return this.tokenValue;
|
||||
}
|
||||
|
||||
public Instant getIssuedAt() {
|
||||
return this.issuedAt;
|
||||
}
|
||||
|
||||
public Instant getExpiresAt() {
|
||||
return this.expiresAt;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* An implementation of an {@link AbstractToken} representing an <i>OAuth 2.0 Access Token</i>.
|
||||
*
|
||||
* <p>
|
||||
* An access token is a credential that represents an authorization
|
||||
* granted by the resource owner to the client.
|
||||
* It is primarily used by the client to access protected resources on either a
|
||||
* resource server or the authorization server that originally issued the access token.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.4">Section 1.4 Access Token</a>
|
||||
*/
|
||||
public class AccessToken extends AbstractToken {
|
||||
private final TokenType tokenType;
|
||||
private final Set<String> scopes;
|
||||
private final Map<String,Object> additionalParameters;
|
||||
|
||||
public enum TokenType {
|
||||
BEARER("Bearer");
|
||||
|
||||
private final String value;
|
||||
|
||||
TokenType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
public AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt) {
|
||||
this(tokenType, tokenValue, issuedAt, expiresAt, Collections.emptySet());
|
||||
}
|
||||
|
||||
public AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt, Set<String> scopes) {
|
||||
this(tokenType, tokenValue, issuedAt, expiresAt, scopes, Collections.emptyMap());
|
||||
}
|
||||
|
||||
public AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt,
|
||||
Set<String> scopes, Map<String,Object> additionalParameters) {
|
||||
|
||||
super(tokenValue, issuedAt, expiresAt);
|
||||
Assert.notNull(tokenType, "tokenType cannot be null");
|
||||
this.tokenType = tokenType;
|
||||
this.scopes = Collections.unmodifiableSet(
|
||||
scopes != null ? scopes : Collections.emptySet());
|
||||
this.additionalParameters = Collections.unmodifiableMap(
|
||||
additionalParameters != null ? additionalParameters : Collections.emptyMap());
|
||||
}
|
||||
|
||||
public TokenType getTokenType() {
|
||||
return this.tokenType;
|
||||
}
|
||||
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
public Map<String, Object> getAdditionalParameters() {
|
||||
return additionalParameters;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core;
|
||||
|
||||
/**
|
||||
* An authorization grant is a credential representing the resource owner's authorization
|
||||
* (to access it's protected resources) to the client and used by the client to obtain an access token.
|
||||
*
|
||||
* <p>
|
||||
* The <i>OAuth 2.0 Authorization Framework</i> defines four standard grant types:
|
||||
* authorization code, implicit, resource owner password credentials, and client credentials.
|
||||
* It also provides an extensibility mechanism for defining additional grant types.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> "authorization code" is currently the only supported grant type.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-1.3">Section 1.3 Authorization Grant</a>
|
||||
*/
|
||||
public enum AuthorizationGrantType {
|
||||
AUTHORIZATION_CODE("authorization_code");
|
||||
|
||||
private final String value;
|
||||
|
||||
AuthorizationGrantType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core;
|
||||
|
||||
/**
|
||||
* The available authentication methods used when authenticating the client with the authorization server.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-2.3">Section 2.3 Client Authentication</a>
|
||||
*/
|
||||
public enum ClientAuthenticationMethod {
|
||||
HEADER("header"),
|
||||
FORM("form");
|
||||
|
||||
private final String value;
|
||||
|
||||
ClientAuthenticationMethod(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A representation of an <i>OAuth 2.0 Error</i>.
|
||||
*
|
||||
* <p>
|
||||
* At a minimum, an error response will contain an error code.
|
||||
* The error code may be one of the standard codes defined by the specification,
|
||||
* or a <i>new</i> code defined in the <i>OAuth Extensions Error Registry</i>,
|
||||
* for cases where protocol extensions require additional error code(s) above the standard codes.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-11.4">Section 11.4 OAuth Extensions Error Registry</a>
|
||||
*/
|
||||
public class OAuth2Error {
|
||||
// Standard error codes
|
||||
public static final String INVALID_REQUEST_ERROR_CODE = "invalid_request";
|
||||
public static final String INVALID_CLIENT_ERROR_CODE = "invalid_client";
|
||||
public static final String INVALID_GRANT_ERROR_CODE = "invalid_grant";
|
||||
public static final String UNAUTHORIZED_CLIENT_ERROR_CODE = "unauthorized_client";
|
||||
public static final String UNSUPPORTED_GRANT_TYPE_ERROR_CODE = "unsupported_grant_type";
|
||||
public static final String INVALID_SCOPE_ERROR_CODE = "invalid_scope";
|
||||
|
||||
private final String errorCode;
|
||||
private final String description;
|
||||
private final String uri;
|
||||
|
||||
public OAuth2Error(String errorCode) {
|
||||
this(errorCode, null, null);
|
||||
}
|
||||
|
||||
public OAuth2Error(String errorCode, String description, String uri) {
|
||||
Assert.hasText(errorCode, "errorCode cannot be empty");
|
||||
this.errorCode = errorCode;
|
||||
this.description = description;
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return this.errorCode;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return this.uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + this.getErrorCode() + "] " +
|
||||
(this.getDescription() != null ? this.getDescription() : "");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core.endpoint;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A representation of an <i>OAuth 2.0 Authorization Response</i> for the authorization code grant type.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2">Section 4.1.2 Authorization Response</a>
|
||||
*/
|
||||
public final class AuthorizationCodeAuthorizationResponseAttributes {
|
||||
private final String code;
|
||||
private final String state;
|
||||
|
||||
public AuthorizationCodeAuthorizationResponseAttributes(String code, String state) {
|
||||
Assert.notNull(code, "code cannot be null");
|
||||
this.code = code;
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return this.state;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core.endpoint;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A representation of an <i>OAuth 2.0 Access Token Request</i> for the authorization code grant type.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request</a>
|
||||
*/
|
||||
public final class AuthorizationCodeTokenRequestAttributes {
|
||||
private String code;
|
||||
private String clientId;
|
||||
private String redirectUri;
|
||||
|
||||
private AuthorizationCodeTokenRequestAttributes() {
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
public String getRedirectUri() {
|
||||
return this.redirectUri;
|
||||
}
|
||||
|
||||
public static Builder withCode(String code) {
|
||||
return new Builder(code);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final AuthorizationCodeTokenRequestAttributes authorizationCodeTokenRequest;
|
||||
|
||||
private Builder(String code) {
|
||||
Assert.hasText(code, "code cannot be empty");
|
||||
this.authorizationCodeTokenRequest = new AuthorizationCodeTokenRequestAttributes();
|
||||
this.authorizationCodeTokenRequest.code = code;
|
||||
}
|
||||
|
||||
public Builder clientId(String clientId) {
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
this.authorizationCodeTokenRequest.clientId = clientId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder redirectUri(String redirectUri) {
|
||||
Assert.hasText(redirectUri, "redirectUri cannot be empty");
|
||||
this.authorizationCodeTokenRequest.redirectUri = redirectUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthorizationCodeTokenRequestAttributes build() {
|
||||
return this.authorizationCodeTokenRequest;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core.endpoint;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A representation of an <i>OAuth 2.0 Authorization Request</i>
|
||||
* for the authorization code grant type or implicit grant type.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see AuthorizationGrantType
|
||||
* @see ResponseType
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.1">Section 4.1.1 Authorization Code Grant Request</a>
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.2.1">Section 4.2.1 Implicit Grant Request</a>
|
||||
*/
|
||||
public final class AuthorizationRequestAttributes implements Serializable {
|
||||
private String authorizeUri;
|
||||
private AuthorizationGrantType authorizationGrantType;
|
||||
private ResponseType responseType;
|
||||
private String clientId;
|
||||
private String redirectUri;
|
||||
private Set<String> scopes;
|
||||
private String state;
|
||||
|
||||
private AuthorizationRequestAttributes() {
|
||||
}
|
||||
|
||||
public String getAuthorizeUri() {
|
||||
return this.authorizeUri;
|
||||
}
|
||||
|
||||
public AuthorizationGrantType getGrantType() {
|
||||
return this.authorizationGrantType;
|
||||
}
|
||||
|
||||
public ResponseType getResponseType() {
|
||||
return this.responseType;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
public String getRedirectUri() {
|
||||
return this.redirectUri;
|
||||
}
|
||||
|
||||
public Set<String> getScopes() {
|
||||
return this.scopes;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public static Builder withAuthorizationCode() {
|
||||
return new Builder(AuthorizationGrantType.AUTHORIZATION_CODE);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final AuthorizationRequestAttributes authorizationRequest;
|
||||
|
||||
private Builder(AuthorizationGrantType authorizationGrantType) {
|
||||
Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
|
||||
this.authorizationRequest = new AuthorizationRequestAttributes();
|
||||
this.authorizationRequest.authorizationGrantType = authorizationGrantType;
|
||||
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationGrantType)) {
|
||||
this.authorizationRequest.responseType = ResponseType.CODE;
|
||||
}
|
||||
}
|
||||
|
||||
public Builder authorizeUri(String authorizeUri) {
|
||||
Assert.hasText(authorizeUri, "authorizeUri cannot be empty");
|
||||
this.authorizationRequest.authorizeUri = authorizeUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder clientId(String clientId) {
|
||||
Assert.hasText(clientId, "clientId cannot be empty");
|
||||
this.authorizationRequest.clientId = clientId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder redirectUri(String redirectUri) {
|
||||
Assert.hasText(redirectUri, "redirectUri cannot be empty");
|
||||
this.authorizationRequest.redirectUri = redirectUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder scopes(Set<String> scopes) {
|
||||
this.authorizationRequest.scopes = Collections.unmodifiableSet(
|
||||
CollectionUtils.isEmpty(scopes) ? Collections.emptySet() : new LinkedHashSet<>(scopes));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder state(String state) {
|
||||
this.authorizationRequest.state = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthorizationRequestAttributes build() {
|
||||
return this.authorizationRequest;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core.endpoint;
|
||||
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A representation of an <i>OAuth 2.0 Error Response</i>.
|
||||
*
|
||||
* <p>
|
||||
* An error response may be returned from either of the following locations:
|
||||
* <ul>
|
||||
* <li><a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.1.2.1">Section 4.1.2.1</a> Authorization Code Grant Response</li>
|
||||
* <li><a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-4.2.2.1">Section 4.2.2.1</a> Implicit Grant Response</li>
|
||||
* <li><a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-5.2">Section 5.2</a> Access Token Response</li>
|
||||
* <li><a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-7.2">Section 7.2</a> Protected Resource Response</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
*/
|
||||
public final class ErrorResponseAttributes {
|
||||
private OAuth2Error errorObject;
|
||||
private String state;
|
||||
|
||||
private ErrorResponseAttributes() {
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return this.errorObject.getErrorCode();
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.errorObject.getDescription();
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return this.errorObject.getUri();
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public static Builder withErrorCode(String errorCode) {
|
||||
return new Builder(errorCode);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String errorCode;
|
||||
private String description;
|
||||
private String uri;
|
||||
private String state;
|
||||
|
||||
private Builder(String errorCode) {
|
||||
Assert.hasText(errorCode, "errorCode cannot be empty");
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public Builder description(String description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder uri(String uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder state(String state) {
|
||||
this.state = state;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ErrorResponseAttributes build() {
|
||||
ErrorResponseAttributes errorResponse = new ErrorResponseAttributes();
|
||||
errorResponse.errorObject = new OAuth2Error(this.errorCode, this.description, this.uri);
|
||||
errorResponse.state = this.state;
|
||||
return errorResponse;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core.endpoint;
|
||||
|
||||
/**
|
||||
* Standard parameters defined in the OAuth Parameters Registry
|
||||
* and used by the authorization endpoint and token endpoint.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-11.2">11.2 OAuth Parameters Registry</a>
|
||||
*/
|
||||
public interface OAuth2Parameter {
|
||||
|
||||
String RESPONSE_TYPE = "response_type";
|
||||
|
||||
String CLIENT_ID = "client_id";
|
||||
|
||||
String REDIRECT_URI = "redirect_uri";
|
||||
|
||||
String SCOPE = "scope";
|
||||
|
||||
String STATE = "state";
|
||||
|
||||
String CODE = "code";
|
||||
|
||||
String ERROR = "error";
|
||||
|
||||
String ERROR_DESCRIPTION = "error_description";
|
||||
|
||||
String ERROR_URI = "error_uri";
|
||||
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core.endpoint;
|
||||
|
||||
/**
|
||||
* The <i>response_type</i> parameter is consumed by the authorization endpoint which
|
||||
* is used by the authorization code grant type and implicit grant type flows.
|
||||
* The client sets the <i>response_type</i> parameter with the desired grant type before initiating the authorization request.
|
||||
*
|
||||
* <p>
|
||||
* The <i>response_type</i> parameter value may be one of "code" for requesting an authorization code or
|
||||
* "token" for requesting an access token (implicit grant).
|
||||
|
||||
* <p>
|
||||
* <b>NOTE:</b> "code" is currently the only supported response type.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-3.1.1">Section 3.1.1 Response Type</a>
|
||||
*/
|
||||
public enum ResponseType {
|
||||
CODE("code");
|
||||
|
||||
private final String value;
|
||||
|
||||
ResponseType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core.endpoint;
|
||||
|
||||
import org.springframework.security.oauth2.core.AccessToken;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A representation of an <i>OAuth 2.0 Access Token Response</i>.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see AccessToken
|
||||
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-5.1">Section 5.1 Access Token Response</a>
|
||||
*/
|
||||
public final class TokenResponseAttributes {
|
||||
private AccessToken accessToken;
|
||||
|
||||
private TokenResponseAttributes() {
|
||||
}
|
||||
|
||||
public String getTokenValue() {
|
||||
return this.accessToken.getTokenValue();
|
||||
}
|
||||
|
||||
public AccessToken.TokenType getTokenType() {
|
||||
return this.accessToken.getTokenType();
|
||||
}
|
||||
|
||||
public Instant getIssuedAt() {
|
||||
return this.accessToken.getIssuedAt();
|
||||
}
|
||||
|
||||
public Instant getExpiresAt() {
|
||||
return this.accessToken.getExpiresAt();
|
||||
}
|
||||
|
||||
public Set<String> getScopes() {
|
||||
return this.accessToken.getScopes();
|
||||
}
|
||||
|
||||
public Map<String, Object> getAdditionalParameters() {
|
||||
return this.accessToken.getAdditionalParameters();
|
||||
}
|
||||
|
||||
public static Builder withToken(String tokenValue) {
|
||||
return new Builder(tokenValue);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String tokenValue;
|
||||
private AccessToken.TokenType tokenType;
|
||||
private long expiresIn;
|
||||
private Set<String> scopes;
|
||||
private Map<String,Object> additionalParameters;
|
||||
|
||||
private Builder(String tokenValue) {
|
||||
this.tokenValue = tokenValue;
|
||||
}
|
||||
|
||||
public Builder tokenType(AccessToken.TokenType tokenType) {
|
||||
this.tokenType = tokenType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder expiresIn(long expiresIn) {
|
||||
this.expiresIn = expiresIn;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder scopes(Set<String> scopes) {
|
||||
this.scopes = scopes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder additionalParameters(Map<String,Object> additionalParameters) {
|
||||
this.additionalParameters = additionalParameters;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TokenResponseAttributes build() {
|
||||
Assert.isTrue(this.expiresIn >= 0, "expiresIn must be a positive number");
|
||||
Instant issuedAt = Instant.now();
|
||||
AccessToken accessToken = new AccessToken(this.tokenType, this.tokenValue, issuedAt,
|
||||
issuedAt.plusSeconds(this.expiresIn), this.scopes, this.additionalParameters);
|
||||
|
||||
TokenResponseAttributes tokenResponse = new TokenResponseAttributes();
|
||||
tokenResponse.accessToken = accessToken;
|
||||
return tokenResponse;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.
|
||||
*/
|
||||
/**
|
||||
* Support classes that model the request/response messages from the
|
||||
* <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-3.1">Authorization Endpoint</a>
|
||||
* and <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-3.2">Token Endpoint</a>.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.endpoint;
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.
|
||||
*/
|
||||
/**
|
||||
* Core classes and interfaces providing support for the <i>OAuth 2.0 Authorization Framework</i>.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core;
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core.user;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.SpringSecurityCoreVersion;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The default implementation of an {@link OAuth2User}.
|
||||
*
|
||||
* <p>
|
||||
* User attribute names are <b><i>not</i></b> standardized between providers
|
||||
* and therefore it is required that the user supply the <i>key</i>
|
||||
* for the user's "name" attribute to one of the constructors.
|
||||
* The <i>key</i> will be used for accessing the "name" of the
|
||||
* <code>Principal</code> (user) via {@link #getAttributes()}
|
||||
* and returning it from {@link #getName()}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see OAuth2User
|
||||
*/
|
||||
public class DefaultOAuth2User implements OAuth2User {
|
||||
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
|
||||
private final Set<GrantedAuthority> authorities;
|
||||
private final Map<String, Object> attributes;
|
||||
private final String nameAttributeKey;
|
||||
|
||||
public DefaultOAuth2User(Map<String, Object> attributes, String nameAttributeKey) {
|
||||
this(Collections.emptySet(), attributes, nameAttributeKey);
|
||||
}
|
||||
|
||||
public DefaultOAuth2User(Set<GrantedAuthority> authorities, Map<String, Object> attributes, String nameAttributeKey) {
|
||||
Assert.notNull(authorities, "authorities cannot be null");
|
||||
Assert.notEmpty(attributes, "attributes cannot be empty");
|
||||
Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty");
|
||||
if (!attributes.containsKey(nameAttributeKey)) {
|
||||
throw new IllegalArgumentException("Invalid nameAttributeKey: " + nameAttributeKey);
|
||||
}
|
||||
this.authorities = Collections.unmodifiableSet(this.sortAuthorities(authorities));
|
||||
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
|
||||
this.nameAttributeKey = nameAttributeKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.getAttributes().get(this.nameAttributeKey).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return this.authorities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
return this.attributes;
|
||||
}
|
||||
|
||||
protected String getAttributeAsString(String key) {
|
||||
Object value = this.getAttributes().get(key);
|
||||
return (value != null ? value.toString() : null);
|
||||
}
|
||||
|
||||
protected Boolean getAttributeAsBoolean(String key) {
|
||||
String value = this.getAttributeAsString(key);
|
||||
return (value != null ? Boolean.valueOf(value) : null);
|
||||
}
|
||||
|
||||
protected Instant getAttributeAsInstant(String key) {
|
||||
String value = this.getAttributeAsString(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Instant.ofEpochSecond(Long.valueOf(value));
|
||||
} catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException("Invalid long value: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<GrantedAuthority> sortAuthorities(Set<GrantedAuthority> authorities) {
|
||||
if (CollectionUtils.isEmpty(authorities)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
SortedSet<GrantedAuthority> sortedAuthorities =
|
||||
new TreeSet<>((g1, g2) -> g1.getAuthority().compareTo(g2.getAuthority()));
|
||||
authorities.stream().forEach(sortedAuthorities::add);
|
||||
|
||||
return sortedAuthorities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || this.getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DefaultOAuth2User that = (DefaultOAuth2User) obj;
|
||||
|
||||
if (!this.getName().equals(that.getName())) {
|
||||
return false;
|
||||
}
|
||||
if (!this.getAuthorities().equals(that.getAuthorities())) {
|
||||
return false;
|
||||
}
|
||||
return this.getAttributes().equals(that.getAttributes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = this.getName().hashCode();
|
||||
result = 31 * result + this.getAuthorities().hashCode();
|
||||
result = 31 * result + this.getAttributes().hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Name: [");
|
||||
sb.append(this.getName());
|
||||
sb.append("], Granted Authorities: [");
|
||||
sb.append(this.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.joining(", ")));
|
||||
sb.append("], User Attributes: [");
|
||||
sb.append(this.getAttributes().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining(", ")));
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.core.user;
|
||||
|
||||
import org.springframework.security.core.AuthenticatedPrincipal;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A representation of a user <code>Principal</code>
|
||||
* that is registered with a standard <i>OAuth 2.0 Provider</i>.
|
||||
*
|
||||
* <p>
|
||||
* An OAuth 2.0 user is composed of one or more attributes, for example,
|
||||
* first name, middle name, last name, email, phone number, address, etc.
|
||||
* Each user attribute has a "name" and "value" and
|
||||
* is keyed by the "name" in {@link #getAttributes()}.
|
||||
*
|
||||
* <p>
|
||||
* <b>NOTE:</b> Attribute names are <b><i>not</i></b> standardized between providers and therefore will vary.
|
||||
* Please consult the provider's API documentation for the set of supported user attribute names.
|
||||
*
|
||||
* <p>
|
||||
* Implementation instances of this interface represent an {@link AuthenticatedPrincipal}
|
||||
* which is associated to an {@link Authentication} object
|
||||
* and may be accessed via {@link Authentication#getPrincipal()}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see DefaultOAuth2User
|
||||
* @see AuthenticatedPrincipal
|
||||
*/
|
||||
public interface OAuth2User extends AuthenticatedPrincipal, Serializable {
|
||||
|
||||
Collection<? extends GrantedAuthority> getAuthorities();
|
||||
|
||||
Map<String, Object> getAttributes();
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.
|
||||
*/
|
||||
/**
|
||||
* Provides a model for an <i>OAuth 2.0</i> representation of a user <code>Principal</code>.
|
||||
*/
|
||||
package org.springframework.security.oauth2.core.user;
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.oidc;
|
||||
|
||||
/**
|
||||
* The Standard Claims defined by the <i>OpenID Connect Core 1.0</i> specification
|
||||
* and returned in either the <i>UserInfo Response</i> or in the <i>ID Token</i>.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
|
||||
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse">UserInfo Response</a>
|
||||
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#IDToken">ID Token</a>
|
||||
*/
|
||||
public interface StandardClaimName {
|
||||
|
||||
String SUB = "sub";
|
||||
|
||||
String NAME = "name";
|
||||
|
||||
String GIVEN_NAME = "given_name";
|
||||
|
||||
String FAMILY_NAME = "family_name";
|
||||
|
||||
String MIDDLE_NAME = "middle_name";
|
||||
|
||||
String NICKNAME = "nickname";
|
||||
|
||||
String PREFERRED_USERNAME = "preferred_username";
|
||||
|
||||
String PROFILE = "profile";
|
||||
|
||||
String PICTURE = "picture";
|
||||
|
||||
String WEBSITE = "website";
|
||||
|
||||
String EMAIL = "email";
|
||||
|
||||
String EMAIL_VERIFIED = "email_verified";
|
||||
|
||||
String GENDER = "gender";
|
||||
|
||||
String BIRTHDATE = "birthdate";
|
||||
|
||||
String ZONEINFO = "zoneinfo";
|
||||
|
||||
String LOCALE = "locale";
|
||||
|
||||
String PHONE_NUMBER = "phone_number";
|
||||
|
||||
String PHONE_NUMBER_VERIFIED = "phone_number_verified";
|
||||
|
||||
String ADDRESS = "address";
|
||||
|
||||
String UPDATED_AT = "updated_at";
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.
|
||||
*/
|
||||
/**
|
||||
* Core classes and interfaces providing support for <i>OpenID Connect Core 1.0</i>.
|
||||
*/
|
||||
package org.springframework.security.oauth2.oidc;
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.oidc.user;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
||||
import org.springframework.security.oauth2.oidc.StandardClaimName;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.springframework.security.oauth2.oidc.StandardClaimName.*;
|
||||
|
||||
/**
|
||||
* The default implementation of a {@link UserInfo}.
|
||||
*
|
||||
* <p>
|
||||
* The <i>key</i> used for accessing the "name" of the
|
||||
* <code>Principal</code> (user) via {@link #getAttributes()}
|
||||
* is {@link StandardClaimName#NAME} or if not available
|
||||
* will default to {@link StandardClaimName#SUB}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see UserInfo
|
||||
* @see DefaultOAuth2User
|
||||
*/
|
||||
public class DefaultUserInfo extends DefaultOAuth2User implements UserInfo {
|
||||
|
||||
public DefaultUserInfo(Map<String, Object> attributes) {
|
||||
this(Collections.emptySet(), attributes);
|
||||
}
|
||||
|
||||
public DefaultUserInfo(Set<GrantedAuthority> authorities, Map<String, Object> attributes) {
|
||||
super(authorities, attributes, SUB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSubject() {
|
||||
return this.getAttributeAsString(SUB);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
String name = this.getAttributeAsString(NAME);
|
||||
return (name != null ? name : super.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGivenName() {
|
||||
return this.getAttributeAsString(GIVEN_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFamilyName() {
|
||||
return this.getAttributeAsString(FAMILY_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMiddleName() {
|
||||
return this.getAttributeAsString(MIDDLE_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNickName() {
|
||||
return this.getAttributeAsString(NICKNAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPreferredUsername() {
|
||||
return this.getAttributeAsString(PREFERRED_USERNAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProfile() {
|
||||
return this.getAttributeAsString(PROFILE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPicture() {
|
||||
return this.getAttributeAsString(PICTURE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWebsite() {
|
||||
return this.getAttributeAsString(WEBSITE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEmail() {
|
||||
return this.getAttributeAsString(EMAIL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getEmailVerified() {
|
||||
return this.getAttributeAsBoolean(EMAIL_VERIFIED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGender() {
|
||||
return this.getAttributeAsString(GENDER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBirthdate() {
|
||||
return this.getAttributeAsString(BIRTHDATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getZoneInfo() {
|
||||
return this.getAttributeAsString(ZONEINFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLocale() {
|
||||
return this.getAttributeAsString(LOCALE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPhoneNumber() {
|
||||
return this.getAttributeAsString(PHONE_NUMBER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getPhoneNumberVerified() {
|
||||
return this.getAttributeAsBoolean(PHONE_NUMBER_VERIFIED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress() {
|
||||
// TODO Impl
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getUpdatedAt() {
|
||||
return this.getAttributeAsInstant(UPDATED_AT);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.oidc.user;
|
||||
|
||||
import org.springframework.security.core.AuthenticatedPrincipal;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* A representation of a user <code>Principal</code>
|
||||
* that is registered with an <i>OpenID Connect 1.0 Provider</i>.
|
||||
*
|
||||
* <p>
|
||||
* The structure of the user <code>Principal</code> is defined by the
|
||||
* <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo">UserInfo Endpoint</a>,
|
||||
* which is an <i>OAuth 2.0 Protected Resource</i> that returns a set of
|
||||
* <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Claims</a>
|
||||
* about the authenticated End-User.
|
||||
*
|
||||
* <p>
|
||||
* Implementation instances of this interface represent an {@link AuthenticatedPrincipal}
|
||||
* which is associated to an {@link Authentication} object
|
||||
* and may be accessed via {@link Authentication#getPrincipal()}.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @since 5.0
|
||||
* @see DefaultUserInfo
|
||||
* @see AuthenticatedPrincipal
|
||||
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html">OpenID Connect Core 1.0</a>
|
||||
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#UserInfo">UserInfo Endpoint</a>
|
||||
* @see <a target="_blank" href="http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims">Standard Claims</a>
|
||||
*/
|
||||
public interface UserInfo extends OAuth2User {
|
||||
|
||||
String getSubject();
|
||||
|
||||
String getGivenName();
|
||||
|
||||
String getFamilyName();
|
||||
|
||||
String getMiddleName();
|
||||
|
||||
String getNickName();
|
||||
|
||||
String getPreferredUsername();
|
||||
|
||||
String getProfile();
|
||||
|
||||
String getPicture();
|
||||
|
||||
String getWebsite();
|
||||
|
||||
String getEmail();
|
||||
|
||||
Boolean getEmailVerified();
|
||||
|
||||
String getGender();
|
||||
|
||||
String getBirthdate();
|
||||
|
||||
String getZoneInfo();
|
||||
|
||||
String getLocale();
|
||||
|
||||
String getPhoneNumber();
|
||||
|
||||
Boolean getPhoneNumberVerified();
|
||||
|
||||
Address getAddress();
|
||||
|
||||
Instant getUpdatedAt();
|
||||
|
||||
|
||||
interface Address {
|
||||
|
||||
String getFormatted();
|
||||
|
||||
String getStreetAddress();
|
||||
|
||||
String getLocality();
|
||||
|
||||
String getRegion();
|
||||
|
||||
String getPostalCode();
|
||||
|
||||
String getCountry();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.
|
||||
*/
|
||||
/**
|
||||
* Provides a model for an <i>OpenID Connect Core 1.0</i> representation of a user <code>Principal</code>.
|
||||
*/
|
||||
package org.springframework.security.oauth2.oidc.user;
|
|
@ -44,6 +44,13 @@
|
|||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>1.5.0.BUILD-SNAPSHOT</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
|
|
|
@ -44,6 +44,13 @@
|
|||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>1.5.0.BUILD-SNAPSHOT</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
|
|
|
@ -0,0 +1,342 @@
|
|||
= OAuth 2.0 Login Sample
|
||||
Joe Grandja
|
||||
:toc:
|
||||
:security-site-url: https://projects.spring.io/spring-security/
|
||||
|
||||
[.lead]
|
||||
This guide will walk you through the steps for setting up the sample application with OAuth 2.0 Login using an external _OAuth 2.0_ or _OpenID Connect 1.0_ Provider.
|
||||
The sample application is built with *Spring Boot 1.5* and the *spring-security-oauth2-client* module that is new in {security-site-url}[Spring Security 5.0].
|
||||
|
||||
The following sections outline detailed steps for setting up OAuth 2.0 Login with these Providers:
|
||||
|
||||
* <<google-login, Google>>
|
||||
* <<github-login, GitHub>>
|
||||
* <<facebook-login, Facebook>>
|
||||
* <<okta-login, Okta>>
|
||||
|
||||
NOTE: The _"authentication flow"_ is realized using the *Authorization Code Grant*, as specified in the https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0 Authorization Framework].
|
||||
|
||||
[[sample-app-content]]
|
||||
== Sample application content
|
||||
|
||||
The sample application contains the following package structure and artifacts:
|
||||
|
||||
*org.springframework.security.samples*
|
||||
|
||||
[circle]
|
||||
* _OAuth2LoginApplication_ - the main class for the _Spring application_.
|
||||
** *user*
|
||||
*** _GitHubOAuth2User_ - a custom _UserInfo_ type for <<github-login, GitHub Login>>.
|
||||
** *web*
|
||||
*** _MainController_ - the root controller that displays user information after a successful login.
|
||||
|
||||
*org.springframework.boot.autoconfigure.security.oauth2.client*
|
||||
|
||||
[circle]
|
||||
* <<client-registration-auto-configuration-class, _ClientRegistrationAutoConfiguration_>> - a Spring Boot auto-configuration class
|
||||
that automatically registers a _ClientRegistrationRepository_ bean in the _ApplicationContext_.
|
||||
* <<oauth2-login-auto-configuration-class, _OAuth2LoginAutoConfiguration_>> - a Spring Boot auto-configuration class that automatically enables OAuth 2.0 Login.
|
||||
|
||||
WARNING: The Spring Boot auto-configuration classes (and dependent resources) will eventually _live_ in the *Spring Boot Security Starter*.
|
||||
|
||||
NOTE: See <<oauth2-login-auto-configuration, OAuth 2.0 Login auto-configuration>> for a detailed overview of the auto-configuration classes.
|
||||
|
||||
[[google-login]]
|
||||
== Setting up *_Login with Google_*
|
||||
|
||||
The goal for this section of the guide is to setup login using Google as the _Authentication Provider_.
|
||||
|
||||
NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the
|
||||
http://openid.net/connect/[OpenID Connect] specification and is http://openid.net/certification/[OpenID Certified].
|
||||
|
||||
[[google-login-register-credentials]]
|
||||
=== Register OAuth 2.0 credentials
|
||||
|
||||
In order to use Google's OAuth 2.0 authentication system for login, you must set up a project in the *Google API Console* to obtain OAuth 2.0 credentials.
|
||||
|
||||
Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page starting in the section *_"Setting up OAuth 2.0"_*.
|
||||
|
||||
After completing the sub-section, *_"Obtain OAuth 2.0 credentials"_*, you should have created a new *OAuth Client* with credentials consisting of a *Client ID* and *Client Secret*.
|
||||
|
||||
[[google-login-redirect-uri]]
|
||||
=== Setting the redirect URI
|
||||
|
||||
The redirect URI is the path in the sample application that the end-user's user-agent is redirected back to after they have authenticated with Google
|
||||
and have granted access to the OAuth Client _(created from the <<google-login-register-credentials, previous step>>)_ on the *Consent screen* page.
|
||||
|
||||
For the sub-section, *_"Set a redirect URI"_*, ensure the *Authorised redirect URIs* is set to *http://localhost:8080/oauth2/authorize/code/google*
|
||||
|
||||
TIP: The default redirect URI is *_"{scheme}://{serverName}:{serverPort}/oauth2/authorize/code/{clientAlias}"_*.
|
||||
See <<oauth2-client-properties, OAuth client properties>> for more details on this default.
|
||||
|
||||
[[google-login-configure-application-yml]]
|
||||
=== Configuring application.yml
|
||||
|
||||
Now that we have created a new OAuth Client with Google, we need to configure the sample application to use this OAuth Client for the _authentication flow_.
|
||||
|
||||
Go to *_src/main/resources_* and edit *application.yml*. Add the following configuration:
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
google:
|
||||
client-id: ${client-id}
|
||||
client-secret: ${client-secret}
|
||||
----
|
||||
|
||||
Replace *${client-id}* and *${client-secret}* with the OAuth 2.0 credentials created in the previous section <<google-login-register-credentials, Register OAuth 2.0 credentials>>.
|
||||
|
||||
[TIP]
|
||||
.OAuth client properties
|
||||
====
|
||||
. *security.oauth2.client* is the *_base property prefix_* for OAuth client properties.
|
||||
. Just below the *_base property prefix_* is the *_client property key_*, for example *security.oauth2.client.google*.
|
||||
. At the base of the *_client property key_* are the properties for specifying the configuration for an OAuth Client.
|
||||
A list of these properties are detailed in <<oauth2-client-properties, OAuth client properties>>.
|
||||
====
|
||||
|
||||
[[google-login-run-sample]]
|
||||
=== Running the sample
|
||||
|
||||
Launch the Spring Boot application by running *_org.springframework.security.samples.OAuth2LoginApplication_*.
|
||||
|
||||
After the application successfully starts up, go to http://localhost:8080. You will be redirected to http://localhost:8080/login, which will display an _auto-generated login page_ with an anchor link for *Google*.
|
||||
|
||||
Click through on the Google link and you'll be redirected to Google for authentication.
|
||||
|
||||
After you authenticate using your Google credentials, the next page presented to you will be the *Consent screen*.
|
||||
The Consent screen will ask you to either *_Allow_* or *_Deny_* access to the OAuth Client you created in the previous step <<google-login-register-credentials, Register OAuth 2.0 credentials>>.
|
||||
Click *_Allow_* to authorize the OAuth Client to access your _email address_ and _basic profile_ information.
|
||||
|
||||
At this point, the OAuth Client will retrieve your email address and basic profile information from the http://openid.net/specs/openid-connect-core-1_0.html#UserInfo[*UserInfo Endpoint*] and establish an _authenticated session_.
|
||||
The home page will then be displayed showing the user attributes retrieved from the *UserInfo Endpoint*, for example, name, email, profile, sub, etc.
|
||||
|
||||
[[oauth2-login-auto-configuration]]
|
||||
== OAuth 2.0 Login auto-configuration
|
||||
|
||||
As you worked through this guide and setup OAuth 2.0 Login with one of the Providers,
|
||||
we hope you noticed the ease in configuration and setup required in getting the sample up and running?
|
||||
And you may be asking, how does this all work? Thanks to some Spring Boot auto-configuration _magic_,
|
||||
we were able to automatically register the OAuth Client(s) configured in the `Environment`,
|
||||
as well, provide a minimal security configuration for OAuth 2.0 Login for these registered OAuth Client(s).
|
||||
|
||||
The following provides an overview of the Spring Boot auto-configuration classes:
|
||||
|
||||
[[client-registration-auto-configuration-class]]
|
||||
*_org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration_*::
|
||||
`ClientRegistrationAutoConfiguration` is responsible for registering a `ClientRegistrationRepository` _bean_ with the `ApplicationContext`.
|
||||
The `ClientRegistrationRepository` is composed of one or more `ClientRegistration` instances, which are created from the OAuth client properties
|
||||
configured in the `Environment` that are prefixed with `security.oauth2.client.[client-alias]`, for example, `security.oauth2.client.google`.
|
||||
|
||||
NOTE: `ClientRegistrationAutoConfiguration` also loads a _resource_ named *oauth2-clients-defaults.yml*,
|
||||
which provides a set of default client property values for a number of _well-known_ Providers.
|
||||
More on this in the later section <<oauth2-default-client-properties, Default client property values>>.
|
||||
|
||||
[[oauth2-login-auto-configuration-class]]
|
||||
*_org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2LoginAutoConfiguration_*::
|
||||
`OAuth2LoginAutoConfiguration` is responsible for enabling OAuth 2.0 Login,
|
||||
only if there is a `ClientRegistrationRepository` _bean_ available in the `ApplicationContext`.
|
||||
|
||||
WARNING: The auto-configuration classes (and dependent resources) will eventually _live_ in the *Spring Boot Security Starter*.
|
||||
|
||||
[[oauth2-client-properties]]
|
||||
=== OAuth client properties
|
||||
|
||||
The following specifies the common set of properties available for configuring an OAuth Client.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
- *security.oauth2.client* is the *_base property prefix_* for OAuth client properties.
|
||||
- Just below the *_base property prefix_* is the *_client property key_*, for example *security.oauth2.client.google*.
|
||||
- At the base of the *_client property key_* are the properties for specifying the configuration for an OAuth Client.
|
||||
====
|
||||
|
||||
- *client-authentication-method* - the method used to authenticate the _Client_ with the _Provider_. Supported values are *header* and *form*.
|
||||
- *authorized-grant-type* - the OAuth 2.0 Authorization Framework defines the https://tools.ietf.org/html/rfc6749#section-1.3.1[Authorization Code] grant type,
|
||||
which is used to realize the _"authentication flow"_. Currently, this is the only supported grant type.
|
||||
- *redirect-uri* - this is the client's _registered_ redirect URI that the _Authorization Server_ redirects the end-user's user-agent
|
||||
to after the end-user has authenticated and authorized access for the client.
|
||||
|
||||
NOTE: The default redirect URI is _"{scheme}://{serverName}:{serverPort}/oauth2/authorize/code/{clientAlias}"_, which leverages *URI template variables*.
|
||||
|
||||
- *scopes* - a comma-delimited string of scope(s) requested during the _Authorization Request_ flow, for example: _openid, email, profile_
|
||||
|
||||
NOTE: _OpenID Connect 1.0_ defines these http://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims[standard scopes]: _profile, email, address, phone_
|
||||
|
||||
NOTE: Non-standard scopes may be defined by a standard _OAuth 2.0 Provider_. Please consult the Provider's OAuth API documentation to learn which scopes are supported.
|
||||
|
||||
- *authorization-uri* - the URI used by the client to redirect the end-user's user-agent to the _Authorization Server_ in order to obtain authorization from the end-user (the _Resource Owner_).
|
||||
- *token-uri* - the URI used by the client when exchanging an _Authorization Grant_ (for example, Authorization Code) for an _Access Token_ at the _Authorization Server_.
|
||||
- *user-info-uri* - the URI used by the client to access the protected resource *UserInfo Endpoint*, in order to obtain attributes of the end-user.
|
||||
- *user-info-converter* - the `Converter` implementation class used to convert the *UserInfo Response* to a `UserInfo` (_OpenID Connect 1.0 Provider_) or `OAuth2User` instance (_Standard OAuth 2.0 Provider_).
|
||||
|
||||
TIP: The `Converter` implementation class for an _OpenID Connect 1.0 Provider_ is *org.springframework.security.oauth2.client.user.converter.UserInfoConverter*
|
||||
and for a standard _OAuth 2.0 Provider_ it's *org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter*.
|
||||
|
||||
- *user-info-name-attribute-key* - the _key_ used to retrieve the *Name* of the end-user from the `Map` of available attributes in `UserInfo` or `OAuth2User`.
|
||||
|
||||
NOTE: _OpenID Connect 1.0_ defines the http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims[*"name"* Claim], which is the end-user's full name and is the default used for `UserInfo`.
|
||||
|
||||
IMPORTANT: Standard _OAuth 2.0 Provider's_ may vary the naming of their *Name* attribute. Please consult the Provider's *UserInfo* API documentation.
|
||||
This is a *_required_* property when *user-info-converter* is set to `OAuth2UserConverter`.
|
||||
|
||||
- *client-name* - this is a descriptive name used for the client. The name may be used in certain scenarios, for example, when displaying the name of the client in the _auto-generated login page_.
|
||||
- *client-alias* - an _alias_ which uniquely identifies the client. It *must be* unique within a `ClientRegistrationRepository`.
|
||||
|
||||
[[oauth2-default-client-properties]]
|
||||
=== Default client property values
|
||||
|
||||
As noted previously, <<client-registration-auto-configuration-class, `ClientRegistrationAutoConfiguration`>> loads a _resource_ named *oauth2-clients-defaults.yml*,
|
||||
which provides a set of default client property values for a number of _well-known_ Providers.
|
||||
|
||||
For example, the *authorization-uri*, *token-uri*, *user-info-uri* rarely change for a Provider and therefore it makes sense to
|
||||
provide a set of defaults in order to reduce the configuration required by the user.
|
||||
|
||||
Below are the current set of default client property values:
|
||||
|
||||
.oauth2-clients-defaults.yml
|
||||
[source,yaml]
|
||||
----
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
google:
|
||||
client-authentication-method: header
|
||||
authorized-grant-type: authorization_code
|
||||
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
|
||||
scopes: openid, email, profile
|
||||
authorization-uri: "https://accounts.google.com/o/oauth2/auth"
|
||||
token-uri: "https://accounts.google.com/o/oauth2/token"
|
||||
user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo"
|
||||
user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
|
||||
client-name: Google
|
||||
client-alias: google
|
||||
github:
|
||||
client-authentication-method: header
|
||||
authorized-grant-type: authorization_code
|
||||
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
|
||||
scopes: user
|
||||
authorization-uri: "https://github.com/login/oauth/authorize"
|
||||
token-uri: "https://github.com/login/oauth/access_token"
|
||||
user-info-uri: "https://api.github.com/user"
|
||||
user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
|
||||
client-name: GitHub
|
||||
client-alias: github
|
||||
facebook:
|
||||
client-authentication-method: form
|
||||
authorized-grant-type: authorization_code
|
||||
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
|
||||
scopes: public_profile, email
|
||||
authorization-uri: "https://www.facebook.com/v2.8/dialog/oauth"
|
||||
token-uri: "https://graph.facebook.com/v2.8/oauth/access_token"
|
||||
user-info-uri: "https://graph.facebook.com/me"
|
||||
user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
|
||||
client-name: Facebook
|
||||
client-alias: facebook
|
||||
okta:
|
||||
client-authentication-method: header
|
||||
authorized-grant-type: authorization_code
|
||||
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
|
||||
scopes: openid, email, profile
|
||||
user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
|
||||
client-name: Okta
|
||||
client-alias: okta
|
||||
----
|
||||
|
||||
= Appendix
|
||||
'''
|
||||
|
||||
[[configure-non-spring-boot-app]]
|
||||
== Configuring a _Non-Spring-Boot_ application
|
||||
|
||||
If you are not using Spring Boot for your application, you will not be able to leverage the auto-configuration features for OAuth 2.0 Login.
|
||||
You will be required to provide your own _security configuration_ in order to enable OAuth 2.0 Login.
|
||||
|
||||
The following sample code demonstrates a minimal security configuration for enabling OAuth 2.0 Login.
|
||||
|
||||
Assuming we have a _properties file_ named *oauth2-clients.properties* on the _classpath_ and it specifies all the _required_ properties for an OAuth Client, specifically _"Google"_:
|
||||
|
||||
.oauth2-clients.properties
|
||||
[source,properties]
|
||||
----
|
||||
security.oauth2.client.google.client-id=${client-id}
|
||||
security.oauth2.client.google.client-secret=${client-secret}
|
||||
security.oauth2.client.google.client-authentication-method=header
|
||||
security.oauth2.client.google.authorized-grant-type=authorization_code
|
||||
security.oauth2.client.google.redirect-uri=http://localhost:8080/oauth2/authorize/code/google
|
||||
security.oauth2.client.google.scopes=openid,email,profile
|
||||
security.oauth2.client.google.authorization-uri=https://accounts.google.com/o/oauth2/auth
|
||||
security.oauth2.client.google.token-uri=https://accounts.google.com/o/oauth2/token
|
||||
security.oauth2.client.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
|
||||
security.oauth2.client.google.user-info-converter=org.springframework.security.oauth2.client.user.converter.UserInfoConverter
|
||||
security.oauth2.client.google.client-name=Google
|
||||
security.oauth2.client.google.client-alias=google
|
||||
----
|
||||
|
||||
The following _security configuration_ will enable OAuth 2.0 Login using _"Google"_ as the _Authentication Provider_:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
@PropertySource("classpath:oauth2-clients.properties")
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
private Environment environment;
|
||||
|
||||
public SecurityConfig(Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.oauth2Login()
|
||||
.clients(clientRegistrationRepository())
|
||||
.userInfoEndpoint()
|
||||
.userInfoTypeConverter(
|
||||
new UserInfoConverter(),
|
||||
new URI("https://www.googleapis.com/oauth2/v3/userinfo"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||
List<ClientRegistration> clientRegistrations = Collections.singletonList(
|
||||
clientRegistration("security.oauth2.client.google."));
|
||||
|
||||
return new InMemoryClientRegistrationRepository(clientRegistrations);
|
||||
}
|
||||
|
||||
private ClientRegistration clientRegistration(String clientPropertyKey) {
|
||||
String clientId = this.environment.getProperty(clientPropertyKey + "client-id");
|
||||
String clientSecret = this.environment.getProperty(clientPropertyKey + "client-secret");
|
||||
ClientAuthenticationMethod clientAuthenticationMethod = ClientAuthenticationMethod.valueOf(
|
||||
this.environment.getProperty(clientPropertyKey + "client-authentication-method").toUpperCase());
|
||||
AuthorizationGrantType authorizationGrantType = AuthorizationGrantType.valueOf(
|
||||
this.environment.getProperty(clientPropertyKey + "authorized-grant-type").toUpperCase());
|
||||
String redirectUri = this.environment.getProperty(clientPropertyKey + "redirect-uri");
|
||||
String[] scopes = this.environment.getProperty(clientPropertyKey + "scopes").split(",");
|
||||
String authorizationUri = this.environment.getProperty(clientPropertyKey + "authorization-uri");
|
||||
String tokenUri = this.environment.getProperty(clientPropertyKey + "token-uri");
|
||||
String userInfoUri = this.environment.getProperty(clientPropertyKey + "user-info-uri");
|
||||
String clientName = this.environment.getProperty(clientPropertyKey + "client-name");
|
||||
String clientAlias = this.environment.getProperty(clientPropertyKey + "client-alias");
|
||||
|
||||
return new ClientRegistration.Builder(clientId)
|
||||
.clientSecret(clientSecret)
|
||||
.clientAuthenticationMethod(clientAuthenticationMethod)
|
||||
.authorizedGrantType(authorizationGrantType)
|
||||
.redirectUri(redirectUri)
|
||||
.scopes(scopes)
|
||||
.authorizationUri(authorizationUri)
|
||||
.tokenUri(tokenUri)
|
||||
.userInfoUri(userInfoUri)
|
||||
.clientName(clientName)
|
||||
.clientAlias(clientAlias)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
----
|
|
@ -0,0 +1,173 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-samples-boot-oauth2login</artifactId>
|
||||
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||
<name>spring-security-samples-boot-oauth2login</name>
|
||||
<description>spring-security-samples-boot-oauth2login</description>
|
||||
<url>http://spring.io/spring-security</url>
|
||||
<organization>
|
||||
<name>spring.io</name>
|
||||
<url>http://spring.io/</url>
|
||||
</organization>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>The Apache Software License, Version 2.0</name>
|
||||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer>
|
||||
<id>rwinch</id>
|
||||
<name>Rob Winch</name>
|
||||
<email>rwinch@pivotal.io</email>
|
||||
</developer>
|
||||
<developer>
|
||||
<id>jgrandja</id>
|
||||
<name>Joe Grandja</name>
|
||||
<email>jgrandja@pivotal.io</email>
|
||||
</developer>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection>scm:git:git://github.com/spring-projects/spring-security</connection>
|
||||
<developerConnection>scm:git:git://github.com/spring-projects/spring-security</developerConnection>
|
||||
<url>https://github.com/spring-projects/spring-security</url>
|
||||
</scm>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-framework-bom</artifactId>
|
||||
<version>4.3.5.RELEASE</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>1.5.0.BUILD-SNAPSHOT</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-config</artifactId>
|
||||
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-client</artifactId>
|
||||
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-web</artifactId>
|
||||
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.thymeleaf.extras</groupId>
|
||||
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
|
||||
<version>2.1.3.RELEASE</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
<version>1.2</version>
|
||||
<scope>compile</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>1.1.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.htmlunit</groupId>
|
||||
<artifactId>htmlunit</artifactId>
|
||||
<version>2.24</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>3.6.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>1.10.19</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>jcl-over-slf4j</artifactId>
|
||||
<version>1.7.7</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<version>5.0.0.BUILD-SNAPSHOT</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshot</id>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,15 @@
|
|||
apply plugin: 'io.spring.convention.spring-sample-boot'
|
||||
|
||||
dependencies {
|
||||
compile project(':spring-security-config')
|
||||
compile project(':spring-security-oauth2-client')
|
||||
compile project(':spring-security-web')
|
||||
compile 'org.springframework.boot:spring-boot-starter-security'
|
||||
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
compile 'org.springframework.boot:spring-boot-starter-web'
|
||||
compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4'
|
||||
|
||||
testCompile project(':spring-security-test')
|
||||
testCompile 'net.sourceforge.htmlunit:htmlunit'
|
||||
testCompile 'org.springframework.boot:spring-boot-starter-test'
|
||||
}
|
|
@ -0,0 +1,405 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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 com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
|
||||
import com.gargoylesoftware.htmlunit.WebClient;
|
||||
import com.gargoylesoftware.htmlunit.WebResponse;
|
||||
import com.gargoylesoftware.htmlunit.html.DomNodeList;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlElement;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import org.junit.Before;
|
||||
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.http.HttpStatus;
|
||||
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.authentication.AuthorizationCodeAuthenticationProcessingFilter;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter;
|
||||
import org.springframework.security.oauth2.client.authentication.AuthorizationGrantTokenExchanger;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.user.OAuth2UserService;
|
||||
import org.springframework.security.oauth2.core.AccessToken;
|
||||
import org.springframework.security.oauth2.core.OAuth2Error;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2Parameter;
|
||||
import org.springframework.security.oauth2.core.endpoint.ResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.TokenResponseAttributes;
|
||||
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Integration tests for the OAuth 2.0 client filters {@link AuthorizationCodeRequestRedirectFilter}
|
||||
* and {@link AuthorizationCodeAuthenticationProcessingFilter}.
|
||||
* These filters work together to realize the Authorization Code Grant flow.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
public class OAuth2LoginApplicationTests {
|
||||
private static final String AUTHORIZATION_BASE_URI = "/oauth2/authorization/code";
|
||||
private static final String AUTHORIZE_BASE_URL = "http://localhost:8080/oauth2/authorize/code";
|
||||
|
||||
@Autowired
|
||||
private WebClient webClient;
|
||||
|
||||
@Autowired
|
||||
private ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
private ClientRegistration googleClientRegistration;
|
||||
private ClientRegistration githubClientRegistration;
|
||||
private ClientRegistration facebookClientRegistration;
|
||||
private ClientRegistration oktaClientRegistration;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.webClient.getCookieManager().clearCookies();
|
||||
this.googleClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("google");
|
||||
this.githubClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("github");
|
||||
this.facebookClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("facebook");
|
||||
this.oktaClientRegistration = this.clientRegistrationRepository.getRegistrationByClientAlias("okta");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestRootPageWhenNotAuthenticatedThenDisplayLoginPage() throws Exception {
|
||||
HtmlPage page = this.webClient.getPage("/");
|
||||
this.assertLoginPage(page);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestOtherPageWhenNotAuthenticatedThenDisplayLoginPage() throws Exception {
|
||||
HtmlPage page = this.webClient.getPage("/other-page");
|
||||
this.assertLoginPage(page);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestAuthorizeGitHubClientWhenLinkClickedThenStatusRedirectForAuthorization() throws Exception {
|
||||
HtmlPage page = this.webClient.getPage("/");
|
||||
|
||||
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.githubClientRegistration);
|
||||
assertThat(clientAnchorElement).isNotNull();
|
||||
|
||||
WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.MOVED_PERMANENTLY.value());
|
||||
|
||||
String authorizeRedirectUri = response.getResponseHeaderValue("Location");
|
||||
assertThat(authorizeRedirectUri).isNotNull();
|
||||
|
||||
UriComponents uriComponents = UriComponentsBuilder.fromUri(URI.create(authorizeRedirectUri)).build();
|
||||
|
||||
String requestUri = uriComponents.getScheme() + "://" + uriComponents.getHost() + uriComponents.getPath();
|
||||
assertThat(requestUri).isEqualTo(this.githubClientRegistration.getProviderDetails().getAuthorizationUri().toString());
|
||||
|
||||
Map<String, String> params = uriComponents.getQueryParams().toSingleValueMap();
|
||||
|
||||
assertThat(params.get(OAuth2Parameter.RESPONSE_TYPE)).isEqualTo(ResponseType.CODE.value());
|
||||
assertThat(params.get(OAuth2Parameter.CLIENT_ID)).isEqualTo(this.githubClientRegistration.getClientId());
|
||||
String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias();
|
||||
assertThat(URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8")).isEqualTo(redirectUri);
|
||||
assertThat(URLDecoder.decode(params.get(OAuth2Parameter.SCOPE), "UTF-8"))
|
||||
.isEqualTo(this.githubClientRegistration.getScopes().stream().collect(Collectors.joining(" ")));
|
||||
assertThat(params.get(OAuth2Parameter.STATE)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestAuthorizeClientWhenInvalidClientThenStatusBadRequest() throws Exception {
|
||||
HtmlPage page = this.webClient.getPage("/");
|
||||
|
||||
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration);
|
||||
assertThat(clientAnchorElement).isNotNull();
|
||||
clientAnchorElement.setAttribute("href", clientAnchorElement.getHrefAttribute() + "-invalid");
|
||||
|
||||
WebResponse response = null;
|
||||
try {
|
||||
clientAnchorElement.click();
|
||||
} catch (FailingHttpStatusCodeException ex) {
|
||||
response = ex.getResponse();
|
||||
}
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestAuthorizationCodeGrantWhenValidAuthorizationResponseThenDisplayUserInfoPage() throws Exception {
|
||||
HtmlPage page = this.webClient.getPage("/");
|
||||
|
||||
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.githubClientRegistration);
|
||||
assertThat(clientAnchorElement).isNotNull();
|
||||
|
||||
WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
|
||||
|
||||
UriComponents authorizeRequestUriComponents = UriComponentsBuilder.fromUri(
|
||||
URI.create(response.getResponseHeaderValue("Location"))).build();
|
||||
|
||||
Map<String, String> params = authorizeRequestUriComponents.getQueryParams().toSingleValueMap();
|
||||
String code = "auth-code";
|
||||
String state = URLDecoder.decode(params.get(OAuth2Parameter.STATE), "UTF-8");
|
||||
String redirectUri = URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8");
|
||||
|
||||
String authorizationResponseUri =
|
||||
UriComponentsBuilder.fromHttpUrl(redirectUri)
|
||||
.queryParam(OAuth2Parameter.CODE, code)
|
||||
.queryParam(OAuth2Parameter.STATE, state)
|
||||
.build().encode().toUriString();
|
||||
|
||||
page = this.webClient.getPage(new URL(authorizationResponseUri));
|
||||
this.assertUserInfoPage(page);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestAuthorizationCodeGrantWhenNoMatchingAuthorizationRequestThenDisplayLoginPageWithError() throws Exception {
|
||||
HtmlPage page = this.webClient.getPage("/");
|
||||
URL loginPageUrl = page.getBaseURL();
|
||||
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
|
||||
|
||||
String code = "auth-code";
|
||||
String state = "state";
|
||||
String redirectUri = AUTHORIZE_BASE_URL + "/" + this.googleClientRegistration.getClientAlias();
|
||||
|
||||
String authorizationResponseUri =
|
||||
UriComponentsBuilder.fromHttpUrl(redirectUri)
|
||||
.queryParam(OAuth2Parameter.CODE, code)
|
||||
.queryParam(OAuth2Parameter.STATE, state)
|
||||
.build().encode().toUriString();
|
||||
|
||||
// Clear session cookie will ensure the 'session-saved'
|
||||
// Authorization Request (from previous request) is not found
|
||||
this.webClient.getCookieManager().clearCookies();
|
||||
|
||||
page = this.webClient.getPage(new URL(authorizationResponseUri));
|
||||
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
|
||||
|
||||
HtmlElement errorElement = page.getBody().getFirstByXPath("p");
|
||||
assertThat(errorElement).isNotNull();
|
||||
assertThat(errorElement.asText()).contains("authorization_request_not_found");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestAuthorizationCodeGrantWhenInvalidStateParamThenDisplayLoginPageWithError() throws Exception {
|
||||
HtmlPage page = this.webClient.getPage("/");
|
||||
URL loginPageUrl = page.getBaseURL();
|
||||
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
|
||||
|
||||
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration);
|
||||
assertThat(clientAnchorElement).isNotNull();
|
||||
this.followLinkDisableRedirects(clientAnchorElement);
|
||||
|
||||
String code = "auth-code";
|
||||
String state = "invalid-state";
|
||||
String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias();
|
||||
|
||||
String authorizationResponseUri =
|
||||
UriComponentsBuilder.fromHttpUrl(redirectUri)
|
||||
.queryParam(OAuth2Parameter.CODE, code)
|
||||
.queryParam(OAuth2Parameter.STATE, state)
|
||||
.build().encode().toUriString();
|
||||
|
||||
page = this.webClient.getPage(new URL(authorizationResponseUri));
|
||||
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
|
||||
|
||||
HtmlElement errorElement = page.getBody().getFirstByXPath("p");
|
||||
assertThat(errorElement).isNotNull();
|
||||
assertThat(errorElement.asText()).contains("invalid_state_parameter");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestAuthorizationCodeGrantWhenInvalidRedirectUriThenDisplayLoginPageWithError() throws Exception {
|
||||
HtmlPage page = this.webClient.getPage("/");
|
||||
URL loginPageUrl = page.getBaseURL();
|
||||
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
|
||||
|
||||
HtmlAnchor clientAnchorElement = this.getClientAnchorElement(page, this.googleClientRegistration);
|
||||
assertThat(clientAnchorElement).isNotNull();
|
||||
|
||||
WebResponse response = this.followLinkDisableRedirects(clientAnchorElement);
|
||||
|
||||
UriComponents authorizeRequestUriComponents = UriComponentsBuilder.fromUri(
|
||||
URI.create(response.getResponseHeaderValue("Location"))).build();
|
||||
|
||||
Map<String, String> params = authorizeRequestUriComponents.getQueryParams().toSingleValueMap();
|
||||
String code = "auth-code";
|
||||
String state = URLDecoder.decode(params.get(OAuth2Parameter.STATE), "UTF-8");
|
||||
String redirectUri = URLDecoder.decode(params.get(OAuth2Parameter.REDIRECT_URI), "UTF-8");
|
||||
redirectUri += "-invalid";
|
||||
|
||||
String authorizationResponseUri =
|
||||
UriComponentsBuilder.fromHttpUrl(redirectUri)
|
||||
.queryParam(OAuth2Parameter.CODE, code)
|
||||
.queryParam(OAuth2Parameter.STATE, state)
|
||||
.build().encode().toUriString();
|
||||
|
||||
page = this.webClient.getPage(new URL(authorizationResponseUri));
|
||||
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
|
||||
|
||||
HtmlElement errorElement = page.getBody().getFirstByXPath("p");
|
||||
assertThat(errorElement).isNotNull();
|
||||
assertThat(errorElement.asText()).contains("invalid_redirect_uri_parameter");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestAuthorizationCodeGrantWhenStandardErrorCodeResponseThenDisplayLoginPageWithError() throws Exception {
|
||||
HtmlPage page = this.webClient.getPage("/");
|
||||
URL loginPageUrl = page.getBaseURL();
|
||||
URL loginErrorPageUrl = new URL(loginPageUrl.toString() + "?error");
|
||||
|
||||
String error = OAuth2Error.INVALID_CLIENT_ERROR_CODE;
|
||||
String state = "state";
|
||||
String redirectUri = AUTHORIZE_BASE_URL + "/" + this.githubClientRegistration.getClientAlias();
|
||||
|
||||
String authorizationResponseUri =
|
||||
UriComponentsBuilder.fromHttpUrl(redirectUri)
|
||||
.queryParam(OAuth2Parameter.ERROR, error)
|
||||
.queryParam(OAuth2Parameter.STATE, state)
|
||||
.build().encode().toUriString();
|
||||
|
||||
page = this.webClient.getPage(new URL(authorizationResponseUri));
|
||||
assertThat(page.getBaseURL()).isEqualTo(loginErrorPageUrl);
|
||||
|
||||
HtmlElement errorElement = page.getBody().getFirstByXPath("p");
|
||||
assertThat(errorElement).isNotNull();
|
||||
assertThat(errorElement.asText()).contains(error);
|
||||
}
|
||||
|
||||
private void assertLoginPage(HtmlPage page) throws Exception {
|
||||
assertThat(page.getTitleText()).isEqualTo("Login Page");
|
||||
|
||||
int expectedClients = 4;
|
||||
|
||||
List<HtmlAnchor> clientAnchorElements = page.getAnchors();
|
||||
assertThat(clientAnchorElements.size()).isEqualTo(expectedClients);
|
||||
|
||||
String baseAuthorizeUri = AUTHORIZATION_BASE_URI + "/";
|
||||
String googleClientAuthorizeUri = baseAuthorizeUri + this.googleClientRegistration.getClientAlias();
|
||||
String githubClientAuthorizeUri = baseAuthorizeUri + this.githubClientRegistration.getClientAlias();
|
||||
String facebookClientAuthorizeUri = baseAuthorizeUri + this.facebookClientRegistration.getClientAlias();
|
||||
String oktaClientAuthorizeUri = baseAuthorizeUri + this.oktaClientRegistration.getClientAlias();
|
||||
|
||||
for (int i=0; i<expectedClients; i++) {
|
||||
assertThat(clientAnchorElements.get(i).getAttribute("href")).isIn(
|
||||
googleClientAuthorizeUri, githubClientAuthorizeUri,
|
||||
facebookClientAuthorizeUri, oktaClientAuthorizeUri);
|
||||
assertThat(clientAnchorElements.get(i).asText()).isIn(
|
||||
this.googleClientRegistration.getClientName(),
|
||||
this.githubClientRegistration.getClientName(),
|
||||
this.facebookClientRegistration.getClientName(),
|
||||
this.oktaClientRegistration.getClientName());
|
||||
}
|
||||
}
|
||||
|
||||
private void assertUserInfoPage(HtmlPage page) throws Exception {
|
||||
assertThat(page.getTitleText()).isEqualTo("Spring Security - OAuth2 User Info");
|
||||
|
||||
DomNodeList<HtmlElement> divElements = page.getBody().getElementsByTagName("div");
|
||||
assertThat(divElements.get(1).asText()).contains("User: joeg@springsecurity.io");
|
||||
assertThat(divElements.get(4).asText()).contains("Name: joeg@springsecurity.io");
|
||||
}
|
||||
|
||||
private HtmlAnchor getClientAnchorElement(HtmlPage page, ClientRegistration clientRegistration) {
|
||||
Optional<HtmlAnchor> clientAnchorElement = page.getAnchors().stream()
|
||||
.filter(e -> e.asText().equals(clientRegistration.getClientName())).findFirst();
|
||||
|
||||
return (clientAnchorElement.isPresent() ? clientAnchorElement.get() : null);
|
||||
}
|
||||
|
||||
private WebResponse followLinkDisableRedirects(HtmlAnchor anchorElement) throws Exception {
|
||||
WebResponse response = null;
|
||||
try {
|
||||
// Disable the automatic redirection (which will trigger
|
||||
// an exception) so that we can capture the response
|
||||
this.webClient.getOptions().setRedirectEnabled(false);
|
||||
anchorElement.click();
|
||||
} catch (FailingHttpStatusCodeException ex) {
|
||||
response = ex.getResponse();
|
||||
this.webClient.getOptions().setRedirectEnabled(true);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
public static class SecurityTestConfig extends WebSecurityConfigurerAdapter {
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.oauth2Login()
|
||||
.authorizationCodeTokenExchanger(this.mockAuthorizationCodeTokenExchanger())
|
||||
.userInfoEndpoint()
|
||||
.userInfoService(this.mockUserInfoService());
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
private AuthorizationGrantTokenExchanger<AuthorizationCodeAuthenticationToken> mockAuthorizationCodeTokenExchanger() {
|
||||
TokenResponseAttributes tokenResponse = TokenResponseAttributes.withToken("access-token-1234")
|
||||
.tokenType(AccessToken.TokenType.BEARER)
|
||||
.expiresIn(60 * 1000)
|
||||
.scopes(Collections.singleton("openid"))
|
||||
.build();
|
||||
|
||||
AuthorizationGrantTokenExchanger mock = mock(AuthorizationGrantTokenExchanger.class);
|
||||
when(mock.exchange(any())).thenReturn(tokenResponse);
|
||||
return mock;
|
||||
}
|
||||
|
||||
private OAuth2UserService mockUserInfoService() {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put("id", "joeg");
|
||||
attributes.put("first-name", "Joe");
|
||||
attributes.put("last-name", "Grandja");
|
||||
attributes.put("email", "joeg@springsecurity.io");
|
||||
|
||||
DefaultOAuth2User user = new DefaultOAuth2User(attributes, "email");
|
||||
|
||||
OAuth2UserService mock = mock(OAuth2UserService.class);
|
||||
when(mock.loadUser(any())).thenReturn(user);
|
||||
return mock;
|
||||
}
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan(basePackages = "org.springframework.security.samples.web")
|
||||
public static class SpringBootApplicationTestConfig {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.boot.autoconfigure.security.oauth2.client;
|
||||
|
||||
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.*;
|
||||
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
|
||||
import org.springframework.boot.bind.PropertySourcesBinder;
|
||||
import org.springframework.boot.bind.RelaxedPropertyResolver;
|
||||
import org.springframework.context.annotation.*;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.MutablePropertySources;
|
||||
import org.springframework.core.env.PropertiesPropertySource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationProperties;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnWebApplication
|
||||
@ConditionalOnClass(ClientRegistrationRepository.class)
|
||||
@ConditionalOnMissingBean(ClientRegistrationRepository.class)
|
||||
@AutoConfigureBefore(SecurityAutoConfiguration.class)
|
||||
public class ClientRegistrationAutoConfiguration {
|
||||
private static final String CLIENT_ID_PROPERTY = "client-id";
|
||||
private static final String CLIENTS_DEFAULTS_RESOURCE = "META-INF/oauth2-clients-defaults.yml";
|
||||
static final String CLIENT_PROPERTY_PREFIX = "security.oauth2.client.";
|
||||
|
||||
@Configuration
|
||||
@Conditional(ClientPropertiesAvailableCondition.class)
|
||||
protected static class ClientRegistrationConfiguration {
|
||||
private final Environment environment;
|
||||
|
||||
protected ClientRegistrationConfiguration(Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ClientRegistrationRepository clientRegistrationRepository() {
|
||||
MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();
|
||||
Properties clientsDefaultProperties = this.getClientsDefaultProperties();
|
||||
if (clientsDefaultProperties != null) {
|
||||
propertySources.addLast(new PropertiesPropertySource("oauth2ClientsDefaults", clientsDefaultProperties));
|
||||
}
|
||||
PropertySourcesBinder binder = new PropertySourcesBinder(propertySources);
|
||||
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(this.environment, CLIENT_PROPERTY_PREFIX);
|
||||
|
||||
List<ClientRegistration> clientRegistrations = new ArrayList<>();
|
||||
|
||||
Set<String> clientPropertyKeys = resolveClientPropertyKeys(this.environment);
|
||||
for (String clientPropertyKey : clientPropertyKeys) {
|
||||
if (!resolver.containsProperty(clientPropertyKey + "." + CLIENT_ID_PROPERTY)) {
|
||||
continue;
|
||||
}
|
||||
ClientRegistrationProperties clientRegistrationProperties = new ClientRegistrationProperties();
|
||||
binder.bindTo(CLIENT_PROPERTY_PREFIX + clientPropertyKey, clientRegistrationProperties);
|
||||
ClientRegistration clientRegistration = new ClientRegistration.Builder(clientRegistrationProperties).build();
|
||||
clientRegistrations.add(clientRegistration);
|
||||
}
|
||||
|
||||
return new InMemoryClientRegistrationRepository(clientRegistrations);
|
||||
}
|
||||
|
||||
private Properties getClientsDefaultProperties() {
|
||||
ClassPathResource clientsDefaultsResource = new ClassPathResource(CLIENTS_DEFAULTS_RESOURCE);
|
||||
if (!clientsDefaultsResource.exists()) {
|
||||
return null;
|
||||
}
|
||||
YamlPropertiesFactoryBean yamlPropertiesFactory = new YamlPropertiesFactoryBean();
|
||||
yamlPropertiesFactory.setResources(clientsDefaultsResource);
|
||||
return yamlPropertiesFactory.getObject();
|
||||
}
|
||||
}
|
||||
|
||||
static Set<String> resolveClientPropertyKeys(Environment environment) {
|
||||
Set<String> clientPropertyKeys = new LinkedHashSet<>();
|
||||
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(environment, CLIENT_PROPERTY_PREFIX);
|
||||
resolver.getSubProperties("").keySet().forEach(key -> {
|
||||
int endIndex = key.indexOf('.');
|
||||
if (endIndex != -1) {
|
||||
clientPropertyKeys.add(key.substring(0, endIndex));
|
||||
}
|
||||
});
|
||||
return clientPropertyKeys;
|
||||
}
|
||||
|
||||
private static class ClientPropertiesAvailableCondition extends SpringBootCondition implements ConfigurationCondition {
|
||||
|
||||
@Override
|
||||
public ConfigurationCondition.ConfigurationPhase getConfigurationPhase() {
|
||||
return ConfigurationPhase.PARSE_CONFIGURATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
ConditionMessage.Builder message = ConditionMessage.forCondition("OAuth2 Client Properties");
|
||||
Set<String> clientPropertyKeys = resolveClientPropertyKeys(context.getEnvironment());
|
||||
if (!CollectionUtils.isEmpty(clientPropertyKeys)) {
|
||||
return ConditionOutcome.match(message.foundExactly("OAuth2 Client(s) -> " +
|
||||
clientPropertyKeys.stream().collect(Collectors.joining(", "))));
|
||||
}
|
||||
return ConditionOutcome.noMatch(message.notAvailable("OAuth2 Client(s)"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.boot.autoconfigure.security.oauth2.client;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
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.WebSecurityConfiguration;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.user.converter.AbstractOAuth2UserConverter;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.net.URI;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration.CLIENT_PROPERTY_PREFIX;
|
||||
import static org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration.resolveClientPropertyKeys;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnWebApplication
|
||||
@ConditionalOnClass(EnableWebSecurity.class)
|
||||
@ConditionalOnMissingBean(WebSecurityConfiguration.class)
|
||||
@ConditionalOnBean(ClientRegistrationRepository.class)
|
||||
@AutoConfigureBefore(SecurityAutoConfiguration.class)
|
||||
@AutoConfigureAfter(ClientRegistrationAutoConfiguration.class)
|
||||
public class OAuth2LoginAutoConfiguration {
|
||||
private static final String USER_INFO_URI_PROPERTY = "user-info-uri";
|
||||
private static final String USER_INFO_CONVERTER_PROPERTY = "user-info-converter";
|
||||
private static final String USER_INFO_NAME_ATTR_KEY_PROPERTY = "user-info-name-attribute-key";
|
||||
|
||||
@EnableWebSecurity
|
||||
protected static class OAuth2LoginSecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
private final Environment environment;
|
||||
|
||||
protected OAuth2LoginSecurityConfiguration(Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.antMatchers("/favicon.ico").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.oauth2Login();
|
||||
|
||||
this.registerUserInfoTypeConverters(http.oauth2Login());
|
||||
}
|
||||
// @formatter:on
|
||||
|
||||
private void registerUserInfoTypeConverters(OAuth2LoginConfigurer<HttpSecurity> oauth2LoginConfigurer) throws Exception {
|
||||
Set<String> clientPropertyKeys = resolveClientPropertyKeys(this.environment);
|
||||
for (String clientPropertyKey : clientPropertyKeys) {
|
||||
String fullClientPropertyKey = CLIENT_PROPERTY_PREFIX + clientPropertyKey + ".";
|
||||
String userInfoUriValue = this.environment.getProperty(fullClientPropertyKey + USER_INFO_URI_PROPERTY);
|
||||
String userInfoConverterTypeValue = this.environment.getProperty(fullClientPropertyKey + USER_INFO_CONVERTER_PROPERTY);
|
||||
if (userInfoUriValue != null && userInfoConverterTypeValue != null) {
|
||||
Class<? extends Converter> userInfoConverterType = ClassUtils.resolveClassName(
|
||||
userInfoConverterTypeValue, this.getClass().getClassLoader()).asSubclass(Converter.class);
|
||||
Converter<ClientHttpResponse, ? extends OAuth2User> userInfoConverter = null;
|
||||
if (AbstractOAuth2UserConverter.class.isAssignableFrom(userInfoConverterType)) {
|
||||
Constructor<? extends Converter> oauth2UserConverterConstructor = ClassUtils.getConstructorIfAvailable(userInfoConverterType, String.class);
|
||||
if (oauth2UserConverterConstructor != null) {
|
||||
String userInfoNameAttributeKey = this.environment.getProperty(fullClientPropertyKey + USER_INFO_NAME_ATTR_KEY_PROPERTY);
|
||||
userInfoConverter = (Converter<ClientHttpResponse, ? extends OAuth2User>)oauth2UserConverterConstructor.newInstance(userInfoNameAttributeKey);
|
||||
}
|
||||
}
|
||||
if (userInfoConverter == null) {
|
||||
userInfoConverter = (Converter<ClientHttpResponse, ? extends OAuth2User>)userInfoConverterType.newInstance();
|
||||
}
|
||||
oauth2LoginConfigurer.userInfoEndpoint().userInfoTypeConverter(userInfoConverter, new URI(userInfoUriValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class OAuth2LoginApplication {
|
||||
|
||||
public OAuth2LoginApplication() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(OAuth2LoginApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.user;
|
||||
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public class GitHubOAuth2User implements OAuth2User {
|
||||
private String id;
|
||||
private String name;
|
||||
private String login;
|
||||
private String email;
|
||||
|
||||
public GitHubOAuth2User() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAttributes() {
|
||||
Map<String, Object> attributes = new HashMap<>();
|
||||
attributes.put("id", this.getId());
|
||||
attributes.put("name", this.getName());
|
||||
attributes.put("login", this.getLogin());
|
||||
attributes.put("email", this.getEmail());
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getLogin() {
|
||||
return this.login;
|
||||
}
|
||||
|
||||
public void setLogin(String login) {
|
||||
this.login = login;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return this.email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.web;
|
||||
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
@Controller
|
||||
public class MainController {
|
||||
|
||||
@RequestMapping("/")
|
||||
public String index(Model model, @AuthenticationPrincipal OAuth2User user) {
|
||||
model.addAttribute("userName", user.getName());
|
||||
model.addAttribute("userAttributes", user.getAttributes());
|
||||
return "index";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
security:
|
||||
oauth2:
|
||||
client:
|
||||
google:
|
||||
client-authentication-method: header
|
||||
authorized-grant-type: authorization_code
|
||||
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
|
||||
scopes: openid, email, profile
|
||||
authorization-uri: "https://accounts.google.com/o/oauth2/auth"
|
||||
token-uri: "https://accounts.google.com/o/oauth2/token"
|
||||
user-info-uri: "https://www.googleapis.com/oauth2/v3/userinfo"
|
||||
user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
|
||||
client-name: Google
|
||||
client-alias: google
|
||||
github:
|
||||
client-authentication-method: header
|
||||
authorized-grant-type: authorization_code
|
||||
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
|
||||
scopes: user
|
||||
authorization-uri: "https://github.com/login/oauth/authorize"
|
||||
token-uri: "https://github.com/login/oauth/access_token"
|
||||
user-info-uri: "https://api.github.com/user"
|
||||
user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
|
||||
client-name: GitHub
|
||||
client-alias: github
|
||||
facebook:
|
||||
client-authentication-method: form
|
||||
authorized-grant-type: authorization_code
|
||||
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
|
||||
scopes: public_profile, email
|
||||
authorization-uri: "https://www.facebook.com/v2.8/dialog/oauth"
|
||||
token-uri: "https://graph.facebook.com/v2.8/oauth/access_token"
|
||||
user-info-uri: "https://graph.facebook.com/me"
|
||||
user-info-converter: "org.springframework.security.oauth2.client.user.converter.OAuth2UserConverter"
|
||||
client-name: Facebook
|
||||
client-alias: facebook
|
||||
okta:
|
||||
client-authentication-method: header
|
||||
authorized-grant-type: authorization_code
|
||||
redirect-uri: "{scheme}://{serverName}:{serverPort}{baseAuthorizeUri}/{clientAlias}"
|
||||
scopes: openid, email, profile
|
||||
user-info-converter: "org.springframework.security.oauth2.client.user.converter.UserInfoConverter"
|
||||
client-name: Okta
|
||||
client-alias: okta
|
|
@ -0,0 +1,4 @@
|
|||
# Spring Boot Auto Configurations
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.boot.autoconfigure.security.oauth2.client.ClientRegistrationAutoConfiguration,\
|
||||
org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2LoginAutoConfiguration
|
|
@ -0,0 +1,34 @@
|
|||
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:
|
||||
google:
|
||||
client-id: your-app-client-id
|
||||
client-secret: your-app-client-secret
|
||||
github:
|
||||
client-id: your-app-client-id
|
||||
client-secret: your-app-client-secret
|
||||
user-info-name-attribute-key: "name"
|
||||
facebook:
|
||||
client-id: your-app-client-id
|
||||
client-secret: your-app-client-secret
|
||||
user-info-name-attribute-key: "name"
|
||||
okta:
|
||||
client-id: your-app-client-id
|
||||
client-secret: your-app-client-secret
|
||||
authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize
|
||||
token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token
|
||||
user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo
|
|
@ -0,0 +1,33 @@
|
|||
<!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 - OAuth2 User Info</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>OAuth2 User Info</h1>
|
||||
<div>
|
||||
<span style="font-weight:bold">Name: </span><span th:text="${userName}"></span>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<span style="font-weight:bold">Attributes:</span>
|
||||
<ul>
|
||||
<li th:each="userAttribute : ${userAttributes}">
|
||||
<span style="font-weight:bold" th:text="${userAttribute.key}"></span>: <span th:text="${userAttribute.value}"></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -15,7 +15,13 @@
|
|||
*/
|
||||
package org.springframework.security.web.authentication.ui;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.WebAttributes;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.web.filter.GenericFilterBean;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
|
@ -24,14 +30,8 @@ import javax.servlet.ServletResponse;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.WebAttributes;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.web.filter.GenericFilterBean;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* For internal use with namespace configuration in the case where a user doesn't
|
||||
|
@ -51,6 +51,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
private String failureUrl;
|
||||
private boolean formLoginEnabled;
|
||||
private boolean openIdEnabled;
|
||||
private boolean oauth2LoginEnabled;
|
||||
private String authenticationUrl;
|
||||
private String usernameParameter;
|
||||
private String passwordParameter;
|
||||
|
@ -58,6 +59,8 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
private String openIDauthenticationUrl;
|
||||
private String openIDusernameParameter;
|
||||
private String openIDrememberMeParameter;
|
||||
private Map<String, String> oauth2AuthenticationUrlToClientName;
|
||||
|
||||
|
||||
public DefaultLoginPageGeneratingFilter() {
|
||||
}
|
||||
|
@ -105,7 +108,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return formLoginEnabled || openIdEnabled;
|
||||
return formLoginEnabled || openIdEnabled || oauth2LoginEnabled;
|
||||
}
|
||||
|
||||
public void setLogoutSuccessUrl(String logoutSuccessUrl) {
|
||||
|
@ -132,6 +135,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
this.openIdEnabled = openIdEnabled;
|
||||
}
|
||||
|
||||
public void setOauth2LoginEnabled(boolean oauth2LoginEnabled) {
|
||||
this.oauth2LoginEnabled = oauth2LoginEnabled;
|
||||
}
|
||||
|
||||
public void setAuthenticationUrl(String authenticationUrl) {
|
||||
this.authenticationUrl = authenticationUrl;
|
||||
}
|
||||
|
@ -157,6 +164,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
this.openIDusernameParameter = openIDusernameParameter;
|
||||
}
|
||||
|
||||
public void setOauth2AuthenticationUrlToClientName(Map<String, String> oauth2AuthenticationUrlToClientName) {
|
||||
this.oauth2AuthenticationUrlToClientName = oauth2AuthenticationUrlToClientName;
|
||||
}
|
||||
|
||||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest request = (HttpServletRequest) req;
|
||||
|
@ -201,13 +212,13 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
}
|
||||
|
||||
if (loginError) {
|
||||
sb.append("<p><font color='red'>Your login attempt was not successful, try again.<br/><br/>Reason: ");
|
||||
sb.append("<p style='color:red;'>Your login attempt was not successful, try again.<br/><br/>Reason: ");
|
||||
sb.append(errorMsg);
|
||||
sb.append("</font></p>");
|
||||
sb.append("</p>");
|
||||
}
|
||||
|
||||
if (logoutSuccess) {
|
||||
sb.append("<p><font color='green'>You have been logged out</font></p>");
|
||||
sb.append("<p style='color:green;'>You have been logged out</p>");
|
||||
}
|
||||
|
||||
if (formLoginEnabled) {
|
||||
|
@ -252,6 +263,19 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
|
|||
sb.append("</form>");
|
||||
}
|
||||
|
||||
if (oauth2LoginEnabled) {
|
||||
sb.append("<h3>Login with OAuth 2.0</h3>");
|
||||
sb.append("<table>\n");
|
||||
for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
|
||||
sb.append(" <tr><td>");
|
||||
sb.append("<a href=\"").append(clientAuthenticationUrlToClientName.getKey()).append("\">");
|
||||
sb.append(clientAuthenticationUrlToClientName.getValue());
|
||||
sb.append("</a>");
|
||||
sb.append("</td></tr>\n");
|
||||
}
|
||||
sb.append("</table>\n");
|
||||
}
|
||||
|
||||
sb.append("</body></html>");
|
||||
|
||||
return sb.toString();
|
||||
|
|
Loading…
Reference in New Issue