Add support for OAuth 2.0 Login

Fixes gh-3907
This commit is contained in:
Joe Grandja 2017-03-20 16:18:08 -04:00
parent a38352c4cc
commit 829c386756
81 changed files with 6483 additions and 48 deletions

View File

@ -113,6 +113,13 @@
<scope>compile</scope> <scope>compile</scope>
<optional>true</optional> <optional>true</optional>
</dependency> </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> <dependency>
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-openid</artifactId> <artifactId>spring-security-openid</artifactId>

View File

@ -12,6 +12,7 @@ dependencies {
optional project(':spring-security-ldap') optional project(':spring-security-ldap')
optional project(':spring-security-messaging') optional project(':spring-security-messaging')
optional project(':spring-security-oauth2-client')
optional project(':spring-security-openid') optional project(':spring-security-openid')
optional project(':spring-security-web') optional project(':spring-security-web')
optional 'org.aspectj:aspectjweaver' optional 'org.aspectj:aspectjweaver'

View File

@ -77,6 +77,10 @@ final class FilterComparator implements Comparator<Filter>, Serializable {
order += STEP; order += STEP;
put(LogoutFilter.class, order); put(LogoutFilter.class, order);
order += STEP; order += STEP;
filterToOrder.put(
"org.springframework.security.oauth2.client.authentication.AuthorizationCodeRequestRedirectFilter",
order);
order += STEP;
put(X509AuthenticationFilter.class, order); put(X509AuthenticationFilter.class, order);
order += STEP; order += STEP;
put(AbstractPreAuthenticatedProcessingFilter.class, order); put(AbstractPreAuthenticatedProcessingFilter.class, order);
@ -84,6 +88,10 @@ final class FilterComparator implements Comparator<Filter>, Serializable {
filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter",
order); order);
order += STEP; order += STEP;
filterToOrder.put(
"org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProcessingFilter",
order);
order += STEP;
put(UsernamePasswordAuthenticationFilter.class, order); put(UsernamePasswordAuthenticationFilter.class, order);
order += STEP; order += STEP;
put(ConcurrentSessionFilter.class, order); put(ConcurrentSessionFilter.class, order);

View File

@ -61,6 +61,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.PortMapper; import org.springframework.security.web.PortMapper;
import org.springframework.security.web.PortMapperImpl; import org.springframework.security.web.PortMapperImpl;
@ -896,6 +897,158 @@ public final class HttpSecurity extends
return getOrApply(new FormLoginConfigurer<HttpSecurity>()); 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>&quot;authentication flow&quot;</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>&quot;Logging in&quot;</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>&quot;/login&quot;</code> and
* redirects to <code>&quot;/login?error&quot;</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>&quot;/oauth2/authorization/code/{clientAlias}&quot;</code>.
* Clicking through the link will initiate the <i>&quot;Authorization Request&quot;</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>&quot;/oauth2/authorize/code/{clientAlias}&quot;</code>.
*
* <p>
* At this point in the <i>&quot;authentication flow&quot;</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>&quot;authenticated&quot;</i> session.
*
* <h2>Example Configurations</h2>
*
* The minimal configuration defaults to automatically generating a login page at <code>&quot;/login&quot;</code>
* and redirecting to <code>&quot;/login?error&quot;</code> when an authentication error occurs or redirecting to
* <code>&quot;/&quot;</code> when an authenticated session is established.
*
* <pre>
* &#064;EnableWebSecurity
* public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
*
* &#064;Override
* protected void configure(HttpSecurity http) throws Exception {
* http
* .authorizeRequests()
* .anyRequest().authenticated()
* .and()
* .oauth2Login();
* }
*
* &#064;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>
* &#064;EnableWebSecurity
* public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
*
* &#064;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"));
* }
*
* &#064;Bean
* public ClientRegistrationRepository clientRegistrationRepository() {
* // ClientRegistrationRepositoryImpl must be composed of at least one ClientRegistration instance
* return new ClientRegistrationRepositoryImpl();
* }
*
* &#064;Bean
* public AuthorizationRequestUriBuilder authorizationRequestBuilder() {
* // Custom URI builder for the &quot;Authorization Request&quot;
* return new AuthorizationRequestUriBuilderImpl();
* }
*
* &#064;Bean
* public AuthorizationGrantTokenExchanger&lt;AuthorizationCodeAuthenticationToken&gt; authorizationCodeTokenExchanger() {
* // Custom implementation that exchanges an &quot;Authorization Code Grant&quot; for an &quot;Access Token&quot;
* return new AuthorizationCodeTokenExchangerImpl();
* }
*
* &#064;Bean
* public OAuth2UserService userInfoService() {
* // Custom implementation that retrieves the details of the authenticated user at the &quot;UserInfo Endpoint&quot;
* return new OAuth2UserServiceImpl();
* }
*
* &#064;Bean
* public Converter&lt;ClientHttpResponse, UserInfo&gt; 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 * Configures channel security. In order for this configuration to be useful at least
* one mapping to a required channel must be provided. * one mapping to a required channel must be provided.

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}
}
}

View File

@ -78,7 +78,7 @@ public class DefaultLoginPageConfigurerTests extends BaseSpringSpec {
springSecurityFilterChain.doFilter(request,response,chain) springSecurityFilterChain.doFilter(request,response,chain)
then: then:
response.getContentAsString() == """<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'> 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> <table>
<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr> <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> <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) springSecurityFilterChain.doFilter(request,response,chain)
then: "sent to default success page" then: "sent to default success page"
response.getContentAsString() == """<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'> 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> <table>
<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr> <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> <tr><td>Password:</td><td><input type='password' name='password'/></td></tr>

View File

@ -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. This will give you access to the entire project history (including all releases and branches) on your local machine.
[[new]] [[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. Spring Security 5.0 provides a number of new features as well as 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. You can find the change log at https://github.com/spring-projects/spring-security/milestone/90?closed=1[5.0.0.M1].
The overwhelming majority of these features were contributed by the community. Below are the highlights of this milestone release.
Below you can find the highlights of this release.
=== Web Improvements === New Features
* 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
* 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]]
== Samples and Guides (Start Here) == Samples and Guides (Start Here)

View File

@ -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>

View File

@ -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'
}

View File

@ -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>&quot;authenticated&quot;</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>&quot;authentication flow&quot;</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());
}
}
}

View File

@ -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>&quot;authenticated&quot;</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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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>&quot;exchanging&quot;</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;
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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()));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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>&quot;authenticated&quot;</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;
}
}

View File

@ -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>&quot;exchanges&quot;</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);
}
}
}

View File

@ -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");
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

138
oauth2/oauth2-core/pom.xml Normal file
View File

@ -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>

View File

@ -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'
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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> &quot;authorization code&quot; 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;
}
}

View File

@ -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;
}
}

View File

@ -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() : "");
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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";
}

View File

@ -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 &quot;code&quot; for requesting an authorization code or
* &quot;token&quot; for requesting an access token (implicit grant).
* <p>
* <b>NOTE:</b> &quot;code&quot; 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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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 &quot;name&quot; attribute to one of the constructors.
* The <i>key</i> will be used for accessing the &quot;name&quot; 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();
}
}

View File

@ -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 &quot;name&quot; and &quot;value&quot; and
* is keyed by the &quot;name&quot; 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();
}

View File

@ -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;

View File

@ -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";
}

View File

@ -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;

View File

@ -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 &quot;name&quot; 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);
}
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -44,6 +44,13 @@
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </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> </dependencies>
</dependencyManagement> </dependencyManagement>
<dependencies> <dependencies>

View File

@ -44,6 +44,13 @@
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </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> </dependencies>
</dependencyManagement> </dependencyManagement>
<dependencies> <dependencies>

View File

@ -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();
}
}
----

View File

@ -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>

View File

@ -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'
}

View File

@ -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 {
}
}

View File

@ -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)"));
}
}
}

View File

@ -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));
}
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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";
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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">&nbsp;</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>&nbsp;</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>

View File

@ -15,7 +15,13 @@
*/ */
package org.springframework.security.web.authentication.ui; 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.FilterChain;
import javax.servlet.ServletException; import javax.servlet.ServletException;
@ -24,14 +30,8 @@ import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import java.io.IOException;
import org.springframework.security.core.AuthenticationException; import java.util.Map;
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;
/** /**
* For internal use with namespace configuration in the case where a user doesn't * 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 String failureUrl;
private boolean formLoginEnabled; private boolean formLoginEnabled;
private boolean openIdEnabled; private boolean openIdEnabled;
private boolean oauth2LoginEnabled;
private String authenticationUrl; private String authenticationUrl;
private String usernameParameter; private String usernameParameter;
private String passwordParameter; private String passwordParameter;
@ -58,6 +59,8 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
private String openIDauthenticationUrl; private String openIDauthenticationUrl;
private String openIDusernameParameter; private String openIDusernameParameter;
private String openIDrememberMeParameter; private String openIDrememberMeParameter;
private Map<String, String> oauth2AuthenticationUrlToClientName;
public DefaultLoginPageGeneratingFilter() { public DefaultLoginPageGeneratingFilter() {
} }
@ -105,7 +108,7 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
} }
public boolean isEnabled() { public boolean isEnabled() {
return formLoginEnabled || openIdEnabled; return formLoginEnabled || openIdEnabled || oauth2LoginEnabled;
} }
public void setLogoutSuccessUrl(String logoutSuccessUrl) { public void setLogoutSuccessUrl(String logoutSuccessUrl) {
@ -132,6 +135,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
this.openIdEnabled = openIdEnabled; this.openIdEnabled = openIdEnabled;
} }
public void setOauth2LoginEnabled(boolean oauth2LoginEnabled) {
this.oauth2LoginEnabled = oauth2LoginEnabled;
}
public void setAuthenticationUrl(String authenticationUrl) { public void setAuthenticationUrl(String authenticationUrl) {
this.authenticationUrl = authenticationUrl; this.authenticationUrl = authenticationUrl;
} }
@ -157,6 +164,10 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
this.openIDusernameParameter = openIDusernameParameter; this.openIDusernameParameter = openIDusernameParameter;
} }
public void setOauth2AuthenticationUrlToClientName(Map<String, String> oauth2AuthenticationUrlToClientName) {
this.oauth2AuthenticationUrlToClientName = oauth2AuthenticationUrlToClientName;
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException { throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req; HttpServletRequest request = (HttpServletRequest) req;
@ -201,13 +212,13 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
} }
if (loginError) { 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(errorMsg);
sb.append("</font></p>"); sb.append("</p>");
} }
if (logoutSuccess) { 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) { if (formLoginEnabled) {
@ -252,6 +263,19 @@ public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
sb.append("</form>"); 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>"); sb.append("</body></html>");
return sb.toString(); return sb.toString();