diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/NimbusUserInfoResponseClient.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/NimbusUserInfoResponseClient.java new file mode 100644 index 0000000000..175ec74a78 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/NimbusUserInfoResponseClient.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-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.oidc.userinfo; + +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.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.client.AbstractClientHttpResponse; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.util.Assert; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.Charset; + +/** + * NOTE: This is a straight copy of org.springframework.security.oauth2.client.userinfo.NimbusUserInfoResponseClient + * + * @author Joe Grandja + * @since 5.0 + */ +final class NimbusUserInfoResponseClient { + private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response"; + private final GenericHttpMessageConverter genericHttpMessageConverter = new MappingJackson2HttpMessageConverter(); + + T getUserInfoResponse(OAuth2UserRequest userInfoRequest, Class returnType) throws OAuth2AuthenticationException { + ClientHttpResponse userInfoResponse = this.getUserInfoResponse( + userInfoRequest.getClientRegistration(), userInfoRequest.getAccessToken()); + try { + return (T) this.genericHttpMessageConverter.read(returnType, userInfoResponse); + } 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); + } + } + + T getUserInfoResponse(OAuth2UserRequest userInfoRequest, ParameterizedTypeReference typeReference) throws OAuth2AuthenticationException { + ClientHttpResponse userInfoResponse = this.getUserInfoResponse( + userInfoRequest.getClientRegistration(), userInfoRequest.getAccessToken()); + try { + return (T) this.genericHttpMessageConverter.read(typeReference.getType(), null, userInfoResponse); + } 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); + } + } + + private ClientHttpResponse getUserInfoResponse(ClientRegistration clientRegistration, + OAuth2AccessToken oauth2AccessToken) throws OAuth2AuthenticationException { + URI userInfoUri = URI.create(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()); + BearerAccessToken accessToken = new BearerAccessToken(oauth2AccessToken.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) { + return new NimbusClientHttpResponse(httpResponse); + } + + 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()); + } + + private static class NimbusClientHttpResponse extends AbstractClientHttpResponse { + private final HTTPResponse httpResponse; + private final HttpHeaders headers; + + private 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; + } + } +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java index ca49e11ae0..1067a2f954 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/userinfo/OidcUserService.java @@ -15,10 +15,9 @@ */ package org.springframework.security.oauth2.client.oidc.userinfo; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.client.userinfo.NimbusUserInfoRetriever; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.client.userinfo.UserInfoRetriever; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; @@ -27,7 +26,6 @@ import org.springframework.security.oauth2.core.oidc.OidcUserInfo; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; -import org.springframework.util.Assert; import org.springframework.util.StringUtils; import java.util.Arrays; @@ -37,10 +35,6 @@ import java.util.Set; /** * An implementation of an {@link OAuth2UserService} that supports OpenID Connect 1.0 Provider's. - *

- * This implementation uses a {@link UserInfoRetriever} to obtain the user attributes - * of the End-User (resource owner) from the UserInfo Endpoint - * and constructs a {@link OidcUserInfo} instance. * * @author Joe Grandja * @since 5.0 @@ -49,19 +43,20 @@ import java.util.Set; * @see OidcUser * @see DefaultOidcUser * @see OidcUserInfo - * @see UserInfoRetriever */ public class OidcUserService implements OAuth2UserService { private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response"; - private UserInfoRetriever userInfoRetriever = new NimbusUserInfoRetriever(); private final Set userInfoScopes = new HashSet<>( Arrays.asList(OidcScopes.PROFILE, OidcScopes.EMAIL, OidcScopes.ADDRESS, OidcScopes.PHONE)); + private NimbusUserInfoResponseClient userInfoResponseClient = new NimbusUserInfoResponseClient(); @Override public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { OidcUserInfo userInfo = null; if (this.shouldRetrieveUserInfo(userRequest)) { - Map userAttributes = this.userInfoRetriever.retrieve(userRequest, Map.class); + ParameterizedTypeReference> typeReference = + new ParameterizedTypeReference>() {}; + Map userAttributes = this.userInfoResponseClient.getUserInfoResponse(userRequest, typeReference); userInfo = new OidcUserInfo(userAttributes); // http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse @@ -84,11 +79,6 @@ public class OidcUserService implements OAuth2UserServiceMap of {@link OAuth2User} type keyed by String, * which represents the {@link ClientRegistration#getRegistrationId() Registration Id} of the Client. - *

- * This implementation uses a {@link UserInfoRetriever} to obtain the user attributes - * of the End-User (Resource Owner) from the UserInfo Endpoint. * * @author Joe Grandja * @since 5.0 * @see OAuth2UserService * @see OAuth2UserRequest * @see OAuth2User - * @see UserInfoRetriever * @see ClientRegistration */ public class CustomUserTypesOAuth2UserService implements OAuth2UserService { private final Map> customUserTypes; - private UserInfoRetriever userInfoRetriever = new NimbusUserInfoRetriever(); + private NimbusUserInfoResponseClient userInfoResponseClient = new NimbusUserInfoResponseClient(); public CustomUserTypesOAuth2UserService(Map> customUserTypes) { Assert.notEmpty(customUserTypes, "customUserTypes cannot be empty"); @@ -58,12 +54,6 @@ public class CustomUserTypesOAuth2UserService implements OAuth2UserService * NOTE: Attribute names are not standardized between providers and therefore will vary. * Please consult the provider's API documentation for the set of supported user attribute names. - *

- * This implementation uses a {@link UserInfoRetriever} to obtain the user attributes - * of the End-User (resource owner) from the UserInfo Endpoint. * * @author Joe Grandja * @since 5.0 @@ -47,10 +44,9 @@ import java.util.Set; * @see OAuth2UserRequest * @see OAuth2User * @see DefaultOAuth2User - * @see UserInfoRetriever */ public class DefaultOAuth2UserService implements OAuth2UserService { - private UserInfoRetriever userInfoRetriever = new NimbusUserInfoRetriever(); + private NimbusUserInfoResponseClient userInfoResponseClient = new NimbusUserInfoResponseClient(); @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { @@ -61,16 +57,13 @@ public class DefaultOAuth2UserService implements OAuth2UserService userAttributes = this.userInfoRetriever.retrieve(userRequest, Map.class); + ParameterizedTypeReference> typeReference = + new ParameterizedTypeReference>() {}; + Map userAttributes = this.userInfoResponseClient.getUserInfoResponse(userRequest, typeReference); GrantedAuthority authority = new OAuth2UserAuthority(userAttributes); Set authorities = new HashSet<>(); authorities.add(authority); return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName); } - - public final void setUserInfoRetriever(UserInfoRetriever userInfoRetriever) { - Assert.notNull(userInfoRetriever, "userInfoRetriever cannot be null"); - this.userInfoRetriever = userInfoRetriever; - } } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoRetriever.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java similarity index 83% rename from oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoRetriever.java rename to oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java index 458dbb51aa..82c0e33d28 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoRetriever.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/NimbusUserInfoResponseClient.java @@ -29,6 +29,8 @@ import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.util.Assert; @@ -40,22 +42,18 @@ import java.net.URI; import java.nio.charset.Charset; /** - * An implementation of a {@link UserInfoRetriever} that uses the Nimbus OAuth 2.0 SDK internally. - * * @author Joe Grandja * @since 5.0 - * @see UserInfoRetriever - * @see Nimbus OAuth 2.0 SDK */ -public class NimbusUserInfoRetriever implements UserInfoRetriever { +final class NimbusUserInfoResponseClient { private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response"; private final GenericHttpMessageConverter genericHttpMessageConverter = new MappingJackson2HttpMessageConverter(); - @Override - public T retrieve(OAuth2UserRequest userRequest, Class returnType) throws OAuth2AuthenticationException { + T getUserInfoResponse(OAuth2UserRequest userInfoRequest, Class returnType) throws OAuth2AuthenticationException { + ClientHttpResponse userInfoResponse = this.getUserInfoResponse( + userInfoRequest.getClientRegistration(), userInfoRequest.getAccessToken()); try { - ClientHttpResponse userResponse = this.retrieveResponse(userRequest); - return (T) this.genericHttpMessageConverter.read(returnType, userResponse); + return (T) this.genericHttpMessageConverter.read(returnType, userInfoResponse); } catch (IOException ex) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, "An error occurred reading the UserInfo Success response: " + ex.getMessage(), null); @@ -63,11 +61,11 @@ public class NimbusUserInfoRetriever implements UserInfoRetriever { } } - @Override - public T retrieve(OAuth2UserRequest userRequest, ParameterizedTypeReference typeReference) throws OAuth2AuthenticationException { + T getUserInfoResponse(OAuth2UserRequest userInfoRequest, ParameterizedTypeReference typeReference) throws OAuth2AuthenticationException { + ClientHttpResponse userInfoResponse = this.getUserInfoResponse( + userInfoRequest.getClientRegistration(), userInfoRequest.getAccessToken()); try { - ClientHttpResponse userResponse = this.retrieveResponse(userRequest); - return (T) this.genericHttpMessageConverter.read(typeReference.getType(), null, userResponse); + return (T) this.genericHttpMessageConverter.read(typeReference.getType(), null, userInfoResponse); } catch (IOException ex) { OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE, "An error occurred reading the UserInfo Success response: " + ex.getMessage(), null); @@ -75,9 +73,10 @@ public class NimbusUserInfoRetriever implements UserInfoRetriever { } } - private ClientHttpResponse retrieveResponse(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - URI userInfoUri = URI.create(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri()); - BearerAccessToken accessToken = new BearerAccessToken(userRequest.getAccessToken().getTokenValue()); + private ClientHttpResponse getUserInfoResponse(ClientRegistration clientRegistration, + OAuth2AccessToken oauth2AccessToken) throws OAuth2AuthenticationException { + URI userInfoUri = URI.create(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()); + BearerAccessToken accessToken = new BearerAccessToken(oauth2AccessToken.getTokenValue()); UserInfoRequest userInfoRequest = new UserInfoRequest(userInfoUri, accessToken); HTTPRequest httpRequest = userInfoRequest.toHTTPRequest(); diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/UserInfoRetriever.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/UserInfoRetriever.java deleted file mode 100644 index 577a8c036e..0000000000 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/UserInfoRetriever.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2002-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.userinfo; - -import org.springframework.core.ParameterizedTypeReference; -import org.springframework.security.oauth2.core.OAuth2AuthenticationException; - -/** - * A strategy for retrieving the user attributes - * of the End-User (Resource Owner) from the UserInfo Endpoint - * using the provided {@link OAuth2UserRequest#getAccessToken() Access Token}. - * - * @author Joe Grandja - * @author Rob Winch - * @since 5.0 - * @see OAuth2UserRequest - * @see OAuth2UserService - */ -public interface UserInfoRetriever { - - T retrieve(OAuth2UserRequest userRequest, Class responseType) throws OAuth2AuthenticationException; - - T retrieve(OAuth2UserRequest userRequest, ParameterizedTypeReference typeReference) throws OAuth2AuthenticationException; - -}