Split up NimbusOAuth2UserService

Fixes gh-4447
This commit is contained in:
Joe Grandja 2017-09-26 11:19:00 -04:00
parent 71b1720cfe
commit 0e9b2807bf
16 changed files with 519 additions and 286 deletions

View File

@ -20,23 +20,26 @@ import org.springframework.security.config.annotation.web.configurers.AbstractAu
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.jwt.JwtDecoder;
import org.springframework.security.jwt.nimbus.NimbusJwtDecoderJwkSupport;
import org.springframework.security.oauth2.client.web.AuthorizationCodeAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationProvider;
import org.springframework.security.oauth2.client.authentication.AuthorizationCodeAuthenticationToken;
import org.springframework.security.oauth2.client.web.AuthorizationGrantTokenExchanger;
import org.springframework.security.oauth2.client.authentication.jwt.DefaultProviderJwtDecoderRegistry;
import org.springframework.security.oauth2.client.authentication.jwt.ProviderJwtDecoderRegistry;
import org.springframework.security.oauth2.client.web.nimbus.NimbusAuthorizationCodeTokenExchanger;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.token.InMemoryAccessTokenRepository;
import org.springframework.security.oauth2.client.token.SecurityTokenRepository;
import org.springframework.security.oauth2.client.user.CustomUserTypesOAuth2UserService;
import org.springframework.security.oauth2.client.user.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.user.DelegatingOAuth2UserService;
import org.springframework.security.oauth2.client.user.OAuth2UserService;
import org.springframework.security.oauth2.client.user.web.nimbus.NimbusOAuth2UserService;
import org.springframework.security.oauth2.client.web.AuthorizationCodeAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.web.AuthorizationGrantTokenExchanger;
import org.springframework.security.oauth2.client.web.nimbus.NimbusAuthorizationCodeTokenExchanger;
import org.springframework.security.oauth2.core.AccessToken;
import org.springframework.security.oauth2.core.provider.DefaultProviderMetadata;
import org.springframework.security.oauth2.core.provider.ProviderMetadata;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.oidc.client.user.OidcUserService;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestVariablesExtractor;
import org.springframework.util.Assert;
@ -46,7 +49,9 @@ import org.springframework.web.util.UriComponentsBuilder;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@ -197,16 +202,28 @@ final class AuthorizationCodeAuthenticationFilterConfigurer<H extends HttpSecuri
return new DefaultProviderJwtDecoderRegistry(jwtDecoders);
}
private boolean isOidcClientRegistered() {
ClientRegistrationRepository clientRegistrationRepository = OAuth2LoginConfigurer.getClientRegistrationRepository(this.getBuilder());
return clientRegistrationRepository.getRegistrations()
.stream()
.anyMatch(registration ->
registration.getScope().stream().anyMatch(scope -> scope.equalsIgnoreCase("openid")));
}
private OAuth2UserService getUserInfoService() {
if (this.userInfoService == null) {
NimbusOAuth2UserService nimbusOAuth2UserService = new NimbusOAuth2UserService();
if (!this.customUserTypes.isEmpty()) {
nimbusOAuth2UserService.setCustomUserTypes(this.customUserTypes);
}
List<OAuth2UserService> oauth2UserServices = new ArrayList<>();
if (!this.userNameAttributeNames.isEmpty()) {
nimbusOAuth2UserService.setUserNameAttributeNames(this.userNameAttributeNames);
oauth2UserServices.add(new DefaultOAuth2UserService(this.userNameAttributeNames));
}
this.userInfoService = nimbusOAuth2UserService;
if (this.isOidcClientRegistered()) {
oauth2UserServices.add(new OidcUserService());
}
if (!this.customUserTypes.isEmpty()) {
oauth2UserServices.add(new CustomUserTypesOAuth2UserService(this.customUserTypes));
}
this.userInfoService = new DelegatingOAuth2UserService(oauth2UserServices);
}
return this.userInfoService;
}

View File

@ -3,11 +3,12 @@ apply plugin: 'io.spring.convention.spring-module'
dependencies {
compile project(':spring-security-core')
compile project(':spring-security-oauth2-core')
compile project(':spring-security-jwt-jose')
compile project(':spring-security-web')
compile springCoreDependency
compile 'org.springframework:spring-web'
compile 'com.nimbusds:oauth2-oidc-sdk'
optional project(':spring-security-jwt-jose')
provided 'javax.servlet:javax.servlet-api'
}

View File

@ -0,0 +1,97 @@
/*
* 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.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
import org.springframework.security.oauth2.client.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.client.user.nimbus.NimbusUserInfoRetriever;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.oidc.client.authentication.OidcClientAuthenticationToken;
import org.springframework.util.Assert;
import java.net.URI;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* An implementation of an {@link OAuth2UserService} that supports custom {@link OAuth2User} types.
* <p>
* The custom user type(s) is supplied via the constructor,
* using a <code>Map</code> of {@link OAuth2User} type <i>keyed</i> by <code>URI</code>,
* representing the <i>UserInfo Endpoint</i> address.
* <p>
* This implementation uses a {@link UserInfoRetriever} to obtain the user attributes
* of the <i>End-User</i> (resource owner) from the <i>UserInfo Endpoint</i>.
*
* @author Joe Grandja
* @since 5.0
* @see OAuth2UserService
* @see OAuth2User
* @see UserInfoRetriever
*/
public class CustomUserTypesOAuth2UserService implements OAuth2UserService {
private final Map<URI, Class<? extends OAuth2User>> customUserTypes;
private UserInfoRetriever userInfoRetriever = new NimbusUserInfoRetriever();
public CustomUserTypesOAuth2UserService(Map<URI, Class<? extends OAuth2User>> customUserTypes) {
Assert.notEmpty(customUserTypes, "customUserTypes cannot be empty");
this.customUserTypes = Collections.unmodifiableMap(new LinkedHashMap<>(customUserTypes));
}
@Override
public OAuth2User loadUser(OAuth2ClientAuthenticationToken clientAuthentication) throws OAuth2AuthenticationException {
URI userInfoUri = URI.create(clientAuthentication.getClientRegistration().getProviderDetails().getUserInfoUri());
Class<? extends OAuth2User> customUserType;
if ((customUserType = this.getCustomUserTypes().get(userInfoUri)) == null) {
return null;
}
OAuth2User customUser;
try {
customUser = customUserType.newInstance();
} catch (ReflectiveOperationException ex) {
throw new IllegalArgumentException("An error occurred while attempting to instantiate the custom OAuth2User \"" +
customUserType.getName() + "\": " + ex.getMessage(), ex);
}
Map<String, Object> userAttributes = this.userInfoRetriever.retrieve(clientAuthentication);
if (OidcClientAuthenticationToken.class.isAssignableFrom(clientAuthentication.getClass())) {
userAttributes.putAll(((OidcClientAuthenticationToken)clientAuthentication).getIdToken().getClaims());
}
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(customUser);
wrapper.setAutoGrowNestedPaths(true);
wrapper.setPropertyValues(userAttributes);
return customUser;
}
protected Map<URI, Class<? extends OAuth2User>> getCustomUserTypes() {
return this.customUserTypes;
}
protected UserInfoRetriever getUserInfoRetriever() {
return this.userInfoRetriever;
}
public final void setUserInfoRetriever(UserInfoRetriever userInfoRetriever) {
Assert.notNull(userInfoRetriever, "userInfoRetriever cannot be null");
this.userInfoRetriever = userInfoRetriever;
}
}

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.client.user;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
import org.springframework.security.oauth2.client.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.client.user.nimbus.NimbusUserInfoRetriever;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.oidc.client.authentication.OidcClientAuthenticationToken;
import org.springframework.util.Assert;
import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* An implementation of an {@link OAuth2UserService} that supports standard <i>OAuth 2.0 Provider's</i>.
* <p>
* For standard <i>OAuth 2.0 Provider's</i>, the attribute name (from the <i>UserInfo Response</i>)
* for the <i>&quot;user's name&quot;</i> is required. This is supplied via the constructor,
* mapped by <code>URI</code>, which represents the <i>UserInfo Endpoint</i> address.
* <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>
* This implementation uses a {@link UserInfoRetriever} to obtain the user attributes
* of the <i>End-User</i> (resource owner) from the <i>UserInfo Endpoint</i>.
*
* @author Joe Grandja
* @since 5.0
* @see OAuth2UserService
* @see DefaultOAuth2User
* @see UserInfoRetriever
*/
public class DefaultOAuth2UserService implements OAuth2UserService {
private final Map<URI, String> userNameAttributeNames;
private UserInfoRetriever userInfoRetriever = new NimbusUserInfoRetriever();
public DefaultOAuth2UserService(Map<URI, String> userNameAttributeNames) {
Assert.notEmpty(userNameAttributeNames, "userNameAttributeNames cannot be empty");
this.userNameAttributeNames = Collections.unmodifiableMap(new LinkedHashMap<>(userNameAttributeNames));
}
@Override
public OAuth2User loadUser(OAuth2ClientAuthenticationToken clientAuthentication) throws OAuth2AuthenticationException {
if (OidcClientAuthenticationToken.class.isAssignableFrom(clientAuthentication.getClass())) {
return null;
}
URI userInfoUri = URI.create(clientAuthentication.getClientRegistration().getProviderDetails().getUserInfoUri());
if (!this.getUserNameAttributeNames().containsKey(userInfoUri)) {
throw new IllegalArgumentException(
"Missing required \"user name\" attribute name for UserInfo Endpoint: " + userInfoUri.toString());
}
String userNameAttributeName = this.getUserNameAttributeNames().get(userInfoUri);
Map<String, Object> userAttributes = this.getUserInfoRetriever().retrieve(clientAuthentication);
GrantedAuthority authority = new OAuth2UserAuthority(userAttributes);
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(authority);
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
}
protected Map<URI, String> getUserNameAttributeNames() {
return this.userNameAttributeNames;
}
protected UserInfoRetriever getUserInfoRetriever() {
return this.userInfoRetriever;
}
public final void setUserInfoRetriever(UserInfoRetriever userInfoRetriever) {
Assert.notNull(userInfoRetriever, "userInfoRetriever cannot be null");
this.userInfoRetriever = userInfoRetriever;
}
}

View File

@ -0,0 +1,56 @@
/*
* 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.oauth2.client.authentication.OAuth2AuthenticationException;
import org.springframework.security.oauth2.client.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
import java.util.List;
import java.util.Objects;
/**
* An implementation of an {@link OAuth2UserService} that simply delegates
* to it's internal <code>List</code> of {@link OAuth2UserService}'s.
* <p>
* Each {@link OAuth2UserService} is given a chance to
* {@link OAuth2UserService#loadUser(OAuth2ClientAuthenticationToken) load} an {@link OAuth2User}
* with the first <code>non-null</code> {@link OAuth2User} being returned.
*
* @author Joe Grandja
* @since 5.0
* @see OAuth2UserService
* @see OAuth2User
*/
public class DelegatingOAuth2UserService implements OAuth2UserService {
private final List<OAuth2UserService> oauth2UserServices;
public DelegatingOAuth2UserService(List<OAuth2UserService> oauth2UserServices) {
Assert.notEmpty(oauth2UserServices, "oauth2UserServices cannot be empty");
this.oauth2UserServices = oauth2UserServices;
}
@Override
public OAuth2User loadUser(OAuth2ClientAuthenticationToken clientAuthentication) throws OAuth2AuthenticationException {
OAuth2User oauth2User = this.oauth2UserServices.stream()
.map(oauth2UserService -> oauth2UserService.loadUser(clientAuthentication))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
return oauth2User;
}
}

View File

@ -19,12 +19,10 @@ import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
import org.springframework.security.oauth2.client.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.oidc.core.UserInfo;
import org.springframework.security.oauth2.oidc.core.user.OidcUser;
/**
* Implementations of this interface are responsible for obtaining
* the end-user's (resource owner) attributes from the <i>UserInfo Endpoint</i>
* Implementations of this interface are responsible for obtaining the user attributes
* of the <i>End-User</i> (resource owner) from the <i>UserInfo Endpoint</i>
* using the provided {@link OAuth2ClientAuthenticationToken#getAccessToken()}
* and returning an {@link AuthenticatedPrincipal} in the form of an {@link OAuth2User}.
*
@ -33,11 +31,9 @@ import org.springframework.security.oauth2.oidc.core.user.OidcUser;
* @see OAuth2ClientAuthenticationToken
* @see AuthenticatedPrincipal
* @see OAuth2User
* @see OidcUser
* @see UserInfo
*/
public interface OAuth2UserService {
OAuth2User loadUser(OAuth2ClientAuthenticationToken token) throws OAuth2AuthenticationException;
OAuth2User loadUser(OAuth2ClientAuthenticationToken clientAuthentication) throws OAuth2AuthenticationException;
}

View File

@ -0,0 +1,37 @@
/*
* 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.oauth2.client.authentication.OAuth2AuthenticationException;
import org.springframework.security.oauth2.client.authentication.OAuth2ClientAuthenticationToken;
import java.util.Map;
/**
* A strategy for retrieving the user attributes
* of the <i>End-User</i> (resource owner) from the <i>UserInfo Endpoint</i>
* using the provided {@link OAuth2ClientAuthenticationToken#getAccessToken()}.
*
* @author Joe Grandja
* @since 5.0
* @see OAuth2ClientAuthenticationToken
* @see OAuth2UserService
*/
public interface UserInfoRetriever {
Map<String, Object> retrieve(OAuth2ClientAuthenticationToken clientAuthentication) throws OAuth2AuthenticationException;
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.client.user.web.nimbus;
package org.springframework.security.oauth2.client.user.nimbus;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import org.springframework.http.HttpHeaders;
@ -27,7 +27,7 @@ import java.io.InputStream;
import java.nio.charset.Charset;
/**
* An implementation of a {@link ClientHttpResponse} which is used by {@link NimbusOAuth2UserService}.
* An implementation of a {@link ClientHttpResponse} which is used by {@link NimbusUserInfoRetriever}.
*
* <p>
* <b>NOTE:</b> This class is intended for internal use only.

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.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.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
import org.springframework.security.oauth2.client.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.client.user.UserInfoRetriever;
import org.springframework.security.oauth2.core.OAuth2Error;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
/**
* An implementation of a {@link UserInfoRetriever} that uses the <b>Nimbus OAuth 2.0 SDK</b> internally.
*
* @author Joe Grandja
* @since 5.0
* @see UserInfoRetriever
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-oauth-openid-connect-sdk">Nimbus OAuth 2.0 SDK</a>
*/
public class NimbusUserInfoRetriever implements UserInfoRetriever {
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
private final HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
@Override
public Map<String, Object> retrieve(OAuth2ClientAuthenticationToken clientAuthentication) throws OAuth2AuthenticationException {
URI userInfoUri = URI.create(clientAuthentication.getClientRegistration().getProviderDetails().getUserInfoUri());
BearerAccessToken accessToken = new BearerAccessToken(clientAuthentication.getAccessToken().getTokenValue());
UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken);
HTTPRequest httpRequest = userInfoRequest.toHTTPRequest();
httpRequest.setConnectTimeout(30000);
httpRequest.setReadTimeout(30000);
HTTPResponse httpResponse;
try {
httpResponse = httpRequest.send();
} catch (IOException ex) {
throw new AuthenticationServiceException("An error occurred while sending the UserInfo Request: " +
ex.getMessage(), ex);
}
if (httpResponse.getStatusCode() != HTTPResponse.SC_OK) {
UserInfoErrorResponse userInfoErrorResponse;
try {
userInfoErrorResponse = UserInfoErrorResponse.parse(httpResponse);
} catch (ParseException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred parsing the UserInfo Error response: " + ex.getMessage(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
}
ErrorObject errorObject = userInfoErrorResponse.getErrorObject();
StringBuilder errorDescription = new StringBuilder();
errorDescription.append("An error occurred while attempting to access the UserInfo Endpoint -> ");
errorDescription.append("Error details: [");
errorDescription.append("UserInfo Uri: ").append(userInfoUri.toString());
errorDescription.append(", Http Status: ").append(errorObject.getHTTPStatusCode());
if (errorObject.getCode() != null) {
errorDescription.append(", Error Code: ").append(errorObject.getCode());
}
if (errorObject.getDescription() != null) {
errorDescription.append(", Error Description: ").append(errorObject.getDescription());
}
errorDescription.append("]");
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, errorDescription.toString(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
try {
return (Map<String, Object>) this.jackson2HttpMessageConverter.read(Map.class, new NimbusClientHttpResponse(httpResponse));
} catch (IOException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred reading the UserInfo Success response: " + ex.getMessage(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
}
}
}

View File

@ -1,232 +0,0 @@
/*
* 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.web.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.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
import org.springframework.security.oauth2.client.authentication.OAuth2ClientAuthenticationToken;
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.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.oidc.client.authentication.OidcClientAuthenticationToken;
import org.springframework.security.oauth2.oidc.core.UserInfo;
import org.springframework.security.oauth2.oidc.core.user.DefaultOidcUser;
import org.springframework.security.oauth2.oidc.core.user.OidcUser;
import org.springframework.security.oauth2.oidc.core.user.OidcUserAuthority;
import org.springframework.util.Assert;
import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* An implementation of an {@link OAuth2UserService} that uses the <b>Nimbus OAuth 2.0 SDK</b> internally.
*
* <p>
* This implementation may be configured with a <code>Map</code> of custom {@link OAuth2User} types
* <i>keyed</i> by <code>URI</code>, which represents the <i>UserInfo Endpoint</i> address.
*
* <p>
* For {@link OAuth2User}'s registered at a standard <i>OAuth 2.0 Provider</i>, the attribute name
* for the &quot;user's name&quot; is required. This can be supplied via {@link #setUserNameAttributeNames(Map)},
* <i>keyed</i> by <code>URI</code>, which represents the <i>UserInfo Endpoint</i> address.
*
* @author Joe Grandja
* @since 5.0
* @see OAuth2ClientAuthenticationToken
* @see OidcClientAuthenticationToken
* @see OAuth2User
* @see OidcUser
* @see UserInfo
* @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 HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
private Map<URI, String> userNameAttributeNames = Collections.unmodifiableMap(Collections.emptyMap());
private Map<URI, Class<? extends OAuth2User>> customUserTypes = Collections.unmodifiableMap(Collections.emptyMap());
public NimbusOAuth2UserService() {
}
@Override
public final OAuth2User loadUser(OAuth2ClientAuthenticationToken token) throws OAuth2AuthenticationException {
URI userInfoUri = this.getUserInfoUri(token);
if (this.getCustomUserTypes().containsKey(userInfoUri)) {
return this.loadCustomUser(token);
}
if (OidcClientAuthenticationToken.class.isAssignableFrom(token.getClass())) {
return this.loadOidcUser((OidcClientAuthenticationToken)token);
}
return this.loadOAuth2User(token);
}
protected OidcUser loadOidcUser(OidcClientAuthenticationToken token) throws OAuth2AuthenticationException {
// TODO Retrieving the UserInfo should be optional. Need to add the capability for opting in/out
Map<String, Object> userAttributes = this.getUserInfo(token);
UserInfo userInfo = new UserInfo(userAttributes);
GrantedAuthority authority = new OidcUserAuthority(token.getIdToken(), userInfo);
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(authority);
return new DefaultOidcUser(authorities, token.getIdToken(), userInfo);
}
protected OAuth2User loadOAuth2User(OAuth2ClientAuthenticationToken token) throws OAuth2AuthenticationException {
URI userInfoUri = this.getUserInfoUri(token);
if (!this.getUserNameAttributeNames().containsKey(userInfoUri)) {
throw new IllegalArgumentException("The attribute name for the \"user's name\" is required for the OAuth2User " +
" retrieved from the UserInfo Endpoint -> " + userInfoUri.toString());
}
String userNameAttributeName = this.getUserNameAttributeNames().get(userInfoUri);
Map<String, Object> userAttributes = this.getUserInfo(token);
GrantedAuthority authority = new OAuth2UserAuthority(userAttributes);
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(authority);
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
}
protected OAuth2User loadCustomUser(OAuth2ClientAuthenticationToken token) throws OAuth2AuthenticationException {
URI userInfoUri = this.getUserInfoUri(token);
Class<? extends OAuth2User> customUserType = this.getCustomUserTypes().get(userInfoUri);
OAuth2User user;
try {
user = customUserType.newInstance();
} catch (ReflectiveOperationException ex) {
throw new IllegalArgumentException("An error occurred while attempting to instantiate the custom OAuth2User \"" +
customUserType.getName() + "\" -> " + ex.getMessage(), ex);
}
Map<String, Object> userAttributes = this.getUserInfo(token);
if (OidcClientAuthenticationToken.class.isAssignableFrom(token.getClass())) {
userAttributes.putAll(((OidcClientAuthenticationToken)token).getIdToken().getClaims());
}
BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(user);
wrapper.setAutoGrowNestedPaths(true);
wrapper.setPropertyValues(userAttributes);
return user;
}
protected Map<String, Object> getUserInfo(OAuth2ClientAuthenticationToken token) throws OAuth2AuthenticationException {
URI userInfoUri = this.getUserInfoUri(token);
BearerAccessToken accessToken = new BearerAccessToken(token.getAccessToken().getTokenValue());
UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken);
HTTPRequest httpRequest = userInfoRequest.toHTTPRequest();
httpRequest.setConnectTimeout(30000);
httpRequest.setReadTimeout(30000);
HTTPResponse httpResponse;
try {
httpResponse = httpRequest.send();
} catch (IOException ex) {
throw new AuthenticationServiceException("An error occurred while sending the UserInfo Request: " +
ex.getMessage(), ex);
}
if (httpResponse.getStatusCode() != HTTPResponse.SC_OK) {
UserInfoErrorResponse userInfoErrorResponse;
try {
userInfoErrorResponse = UserInfoErrorResponse.parse(httpResponse);
} catch (ParseException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred parsing the UserInfo Error response: " + ex.getMessage(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
}
ErrorObject errorObject = userInfoErrorResponse.getErrorObject();
StringBuilder errorDescription = new StringBuilder();
errorDescription.append("An error occurred while attempting to access the UserInfo Endpoint -> ");
errorDescription.append("Error details: [");
errorDescription.append("UserInfo Uri: ").append(userInfoUri.toString());
errorDescription.append(", Http Status: ").append(errorObject.getHTTPStatusCode());
if (errorObject.getCode() != null) {
errorDescription.append(", Error Code: ").append(errorObject.getCode());
}
if (errorObject.getDescription() != null) {
errorDescription.append(", Error Description: ").append(errorObject.getDescription());
}
errorDescription.append("]");
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, errorDescription.toString(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
try {
return (Map<String, Object>) this.jackson2HttpMessageConverter.read(Map.class, new NimbusClientHttpResponse(httpResponse));
} catch (IOException ex) {
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
"An error occurred reading the UserInfo Success response: " + ex.getMessage(), null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
}
}
protected Map<URI, String> getUserNameAttributeNames() {
return this.userNameAttributeNames;
}
public final void setUserNameAttributeNames(Map<URI, String> userNameAttributeNames) {
Assert.notEmpty(userNameAttributeNames, "userNameAttributeNames cannot be empty");
this.userNameAttributeNames = Collections.unmodifiableMap(new HashMap<>(userNameAttributeNames));
}
protected Map<URI, Class<? extends OAuth2User>> getCustomUserTypes() {
return this.customUserTypes;
}
public final void setCustomUserTypes(Map<URI, Class<? extends OAuth2User>> customUserTypes) {
Assert.notEmpty(customUserTypes, "customUserTypes cannot be empty");
this.customUserTypes = Collections.unmodifiableMap(new HashMap<>(customUserTypes));
}
private URI getUserInfoUri(OAuth2ClientAuthenticationToken token) {
ClientRegistration clientRegistration = token.getClientRegistration();
try {
return new URI(clientRegistration.getProviderDetails().getUserInfoUri());
} catch (Exception ex) {
throw new IllegalArgumentException("An error occurred parsing the UserInfo URI: " +
clientRegistration.getProviderDetails().getUserInfoUri(), ex);
}
}
}

View File

@ -0,0 +1,78 @@
/*
* 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.client.user;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationException;
import org.springframework.security.oauth2.client.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.client.user.OAuth2UserService;
import org.springframework.security.oauth2.client.user.UserInfoRetriever;
import org.springframework.security.oauth2.client.user.nimbus.NimbusUserInfoRetriever;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.oauth2.oidc.client.authentication.OidcClientAuthenticationToken;
import org.springframework.security.oauth2.oidc.core.UserInfo;
import org.springframework.security.oauth2.oidc.core.user.DefaultOidcUser;
import org.springframework.security.oauth2.oidc.core.user.OidcUserAuthority;
import org.springframework.util.Assert;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* An implementation of an {@link OAuth2UserService} that supports <i>OpenID Connect 1.0 Provider's</i>.
* <p>
* This implementation uses a {@link UserInfoRetriever} to obtain the user attributes
* of the <i>End-User</i> (resource owner) from the <i>UserInfo Endpoint</i>
* and constructs a {@link UserInfo} instance.
*
* @author Joe Grandja
* @since 5.0
* @see OAuth2UserService
* @see OidcClientAuthenticationToken
* @see DefaultOidcUser
* @see UserInfo
* @see UserInfoRetriever
*/
public class OidcUserService implements OAuth2UserService {
private UserInfoRetriever userInfoRetriever = new NimbusUserInfoRetriever();
@Override
public OAuth2User loadUser(OAuth2ClientAuthenticationToken clientAuthentication) throws OAuth2AuthenticationException {
if (!OidcClientAuthenticationToken.class.isAssignableFrom(clientAuthentication.getClass())) {
return null;
}
OidcClientAuthenticationToken oidcClientAuthentication = (OidcClientAuthenticationToken)clientAuthentication;
Map<String, Object> userAttributes = this.getUserInfoRetriever().retrieve(oidcClientAuthentication);
UserInfo userInfo = new UserInfo(userAttributes);
GrantedAuthority authority = new OidcUserAuthority(oidcClientAuthentication.getIdToken(), userInfo);
Set<GrantedAuthority> authorities = new HashSet<>();
authorities.add(authority);
return new DefaultOidcUser(authorities, oidcClientAuthentication.getIdToken(), userInfo);
}
protected UserInfoRetriever getUserInfoRetriever() {
return this.userInfoRetriever;
}
public final void setUserInfoRetriever(UserInfoRetriever userInfoRetriever) {
Assert.notNull(userInfoRetriever, "userInfoRetriever cannot be null");
this.userInfoRetriever = userInfoRetriever;
}
}

View File

@ -43,7 +43,7 @@ public class OAuth2UserAuthority implements GrantedAuthority {
Assert.hasText(authority, "authority cannot be empty");
Assert.notEmpty(attributes, "attributes cannot be empty");
this.authority = authority;
this.setAttributes(attributes);
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
}
@Override
@ -55,11 +55,6 @@ public class OAuth2UserAuthority implements GrantedAuthority {
return this.attributes;
}
protected final void setAttributes(Map<String, Object> attributes) {
Assert.notEmpty(attributes, "attributes cannot be empty");
this.attributes = Collections.unmodifiableMap(new LinkedHashMap<>(attributes));
}
@Override
public boolean equals(Object obj) {
if (this == obj) {

View File

@ -16,16 +16,14 @@
package org.springframework.security.oauth2.oidc.core.user;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.oidc.core.IdToken;
import org.springframework.security.oauth2.oidc.core.IdTokenClaim;
import org.springframework.security.oauth2.oidc.core.UserInfo;
import org.springframework.util.Assert;
import java.util.Map;
import java.util.Set;
/**
* The default implementation of an {@link OidcUser}.
@ -61,7 +59,7 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
public DefaultOidcUser(Set<GrantedAuthority> authorities, IdToken idToken, UserInfo userInfo,
String nameAttributeKey) {
super(authorities, resolveAttributes(idToken, userInfo), nameAttributeKey);
super(authorities, OidcUser.collectClaims(idToken, userInfo), nameAttributeKey);
this.idToken = idToken;
this.userInfo = userInfo;
}
@ -78,14 +76,4 @@ public class DefaultOidcUser extends DefaultOAuth2User implements OidcUser {
public UserInfo getUserInfo() {
return this.userInfo;
}
private static Map<String, Object> resolveAttributes(IdToken idToken, UserInfo userInfo) {
Assert.notNull(idToken, "idToken cannot be null");
Map<String, Object> attributes = new HashMap<>();
attributes.putAll(idToken.getClaims());
if (userInfo != null) {
attributes.putAll(userInfo.getClaims());
}
return attributes;
}
}

View File

@ -22,7 +22,9 @@ import org.springframework.security.oauth2.oidc.core.IdToken;
import org.springframework.security.oauth2.oidc.core.IdTokenClaimAccessor;
import org.springframework.security.oauth2.oidc.core.StandardClaimAccessor;
import org.springframework.security.oauth2.oidc.core.UserInfo;
import org.springframework.util.Assert;
import java.util.HashMap;
import java.util.Map;
/**
@ -54,4 +56,13 @@ public interface OidcUser extends OAuth2User, IdTokenClaimAccessor {
Map<String, Object> getClaims();
static Map<String, Object> collectClaims(IdToken idToken, UserInfo userInfo) {
Assert.notNull(idToken, "idToken cannot be null");
Map<String, Object> claims = new HashMap<>();
if (userInfo != null) {
claims.putAll(userInfo.getClaims());
}
claims.putAll(idToken.getClaims());
return claims;
}
}

View File

@ -21,10 +21,6 @@ import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.oidc.core.IdToken;
import org.springframework.security.oauth2.oidc.core.UserInfo;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* A {@link GrantedAuthority} that is associated with an {@link OidcUser}.
*
@ -46,16 +42,9 @@ public class OidcUserAuthority extends OAuth2UserAuthority {
}
public OidcUserAuthority(String authority, IdToken idToken, UserInfo userInfo) {
super(authority, idToken.getClaims());
super(authority, OidcUser.collectClaims(idToken, userInfo));
this.idToken = idToken;
this.userInfo = userInfo;
if (userInfo != null) {
this.setAttributes(
Stream.of(this.getAttributes(), userInfo.getClaims())
.flatMap(m -> m.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (k1, k2) -> k1))
);
}
}
public IdToken getIdToken() {

View File

@ -3,6 +3,7 @@ apply plugin: 'io.spring.convention.spring-sample-boot'
dependencies {
compile project(':spring-security-config')
compile project(':spring-security-oauth2-client')
compile project(':spring-security-jwt-jose')
compile 'org.springframework:spring-webflux'
compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
compile 'org.springframework.boot:spring-boot-starter-web'