Skip to content

Commit b85a42f

Browse files
committed
Provide RestOperations in DefaultOAuth2UserService
Fixes gh-5600
1 parent 8e615d0 commit b85a42f

File tree

7 files changed

+497
-202
lines changed

7 files changed

+497
-202
lines changed

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandler.java

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@
1515
*/
1616
package org.springframework.security.oauth2.client.http;
1717

18+
import com.nimbusds.oauth2.sdk.token.BearerTokenError;
19+
import org.springframework.http.HttpHeaders;
1820
import org.springframework.http.HttpStatus;
1921
import org.springframework.http.client.ClientHttpResponse;
2022
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
2123
import org.springframework.security.oauth2.core.OAuth2Error;
24+
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
2225
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
26+
import org.springframework.util.StringUtils;
2327
import org.springframework.web.client.DefaultResponseErrorHandler;
2428
import org.springframework.web.client.ResponseErrorHandler;
2529

@@ -44,10 +48,39 @@ public boolean hasError(ClientHttpResponse response) throws IOException {
4448

4549
@Override
4650
public void handleError(ClientHttpResponse response) throws IOException {
47-
if (HttpStatus.BAD_REQUEST.equals(response.getStatusCode())) {
48-
OAuth2Error oauth2Error = this.oauth2ErrorConverter.read(OAuth2Error.class, response);
49-
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
51+
if (!HttpStatus.BAD_REQUEST.equals(response.getStatusCode())) {
52+
this.defaultErrorHandler.handleError(response);
5053
}
51-
this.defaultErrorHandler.handleError(response);
54+
55+
// A Bearer Token Error may be in the WWW-Authenticate response header
56+
// See https://tools.ietf.org/html/rfc6750#section-3
57+
OAuth2Error oauth2Error = this.readErrorFromWwwAuthenticate(response.getHeaders());
58+
if (oauth2Error == null) {
59+
oauth2Error = this.oauth2ErrorConverter.read(OAuth2Error.class, response);
60+
}
61+
62+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
63+
}
64+
65+
private OAuth2Error readErrorFromWwwAuthenticate(HttpHeaders headers) {
66+
String wwwAuthenticateHeader = headers.getFirst(HttpHeaders.WWW_AUTHENTICATE);
67+
if (!StringUtils.hasText(wwwAuthenticateHeader)) {
68+
return null;
69+
}
70+
71+
BearerTokenError bearerTokenError;
72+
try {
73+
bearerTokenError = BearerTokenError.parse(wwwAuthenticateHeader);
74+
} catch (Exception ex) {
75+
return null;
76+
}
77+
78+
String errorCode = bearerTokenError.getCode() != null ?
79+
bearerTokenError.getCode() : OAuth2ErrorCodes.SERVER_ERROR;
80+
String errorDescription = bearerTokenError.getDescription();
81+
String errorUri = bearerTokenError.getURI() != null ?
82+
bearerTokenError.getURI().toString() : null;
83+
84+
return new OAuth2Error(errorCode, errorDescription, errorUri);
5285
}
5386
}

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/userinfo/DefaultOAuth2UserService.java

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,16 +16,25 @@
1616
package org.springframework.security.oauth2.client.userinfo;
1717

1818
import org.springframework.core.ParameterizedTypeReference;
19+
import org.springframework.core.convert.converter.Converter;
20+
import org.springframework.http.RequestEntity;
21+
import org.springframework.http.ResponseEntity;
1922
import org.springframework.security.core.GrantedAuthority;
23+
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
24+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
2025
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
2126
import org.springframework.security.oauth2.core.OAuth2Error;
2227
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
2328
import org.springframework.security.oauth2.core.user.OAuth2User;
2429
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
2530
import org.springframework.util.Assert;
2631
import org.springframework.util.StringUtils;
32+
import org.springframework.web.client.ResponseErrorHandler;
33+
import org.springframework.web.client.RestClientException;
34+
import org.springframework.web.client.RestOperations;
35+
import org.springframework.web.client.RestTemplate;
2736

28-
import java.util.HashSet;
37+
import java.util.Collections;
2938
import java.util.Map;
3039
import java.util.Set;
3140

@@ -34,7 +43,7 @@
3443
* <p>
3544
* For standard OAuth 2.0 Provider's, the attribute name used to access the user's name
3645
* from the UserInfo response is required and therefore must be available via
37-
* {@link org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails.UserInfoEndpoint#getUserNameAttributeName() UserInfoEndpoint.getUserNameAttributeName()}.
46+
* {@link ClientRegistration.ProviderDetails.UserInfoEndpoint#getUserNameAttributeName() UserInfoEndpoint.getUserNameAttributeName()}.
3847
* <p>
3948
* <b>NOTE:</b> Attribute names are <b>not</b> standardized between providers and therefore will vary.
4049
* Please consult the provider's API documentation for the set of supported user attribute names.
@@ -48,8 +57,23 @@
4857
*/
4958
public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
5059
private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";
60+
5161
private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";
52-
private NimbusUserInfoResponseClient userInfoResponseClient = new NimbusUserInfoResponseClient();
62+
63+
private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";
64+
65+
private static final ParameterizedTypeReference<Map<String, Object>> PARAMETERIZED_RESPONSE_TYPE =
66+
new ParameterizedTypeReference<Map<String, Object>>() {};
67+
68+
private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();
69+
70+
private RestOperations restOperations;
71+
72+
public DefaultOAuth2UserService() {
73+
RestTemplate restTemplate = new RestTemplate();
74+
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
75+
this.restOperations = restTemplate;
76+
}
5377

5478
@Override
5579
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
@@ -64,7 +88,8 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
6488
);
6589
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
6690
}
67-
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
91+
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
92+
.getUserInfoEndpoint().getUserNameAttributeName();
6893
if (!StringUtils.hasText(userNameAttributeName)) {
6994
OAuth2Error oauth2Error = new OAuth2Error(
7095
MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
@@ -75,13 +100,63 @@ public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2Authentic
75100
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
76101
}
77102

78-
ParameterizedTypeReference<Map<String, Object>> typeReference =
79-
new ParameterizedTypeReference<Map<String, Object>>() {};
80-
Map<String, Object> userAttributes = this.userInfoResponseClient.getUserInfoResponse(userRequest, typeReference);
81-
GrantedAuthority authority = new OAuth2UserAuthority(userAttributes);
82-
Set<GrantedAuthority> authorities = new HashSet<>();
83-
authorities.add(authority);
103+
RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
104+
105+
ResponseEntity<Map<String, Object>> response;
106+
try {
107+
response = this.restOperations.exchange(request, PARAMETERIZED_RESPONSE_TYPE);
108+
} catch (OAuth2AuthenticationException ex) {
109+
OAuth2Error oauth2Error = ex.getError();
110+
StringBuilder errorDetails = new StringBuilder();
111+
errorDetails.append("Error details: [");
112+
errorDetails.append("UserInfo Uri: ").append(
113+
userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri());
114+
errorDetails.append(", Error Code: ").append(oauth2Error.getErrorCode());
115+
if (oauth2Error.getDescription() != null) {
116+
errorDetails.append(", Error Description: ").append(oauth2Error.getDescription());
117+
}
118+
errorDetails.append("]");
119+
oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
120+
"An error occurred while attempting to retrieve the UserInfo Resource: " + errorDetails.toString(), null);
121+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
122+
} catch (RestClientException ex) {
123+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
124+
"An error occurred while attempting to retrieve the UserInfo Resource: " + ex.getMessage(), null);
125+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
126+
}
127+
128+
Map<String, Object> userAttributes = response.getBody();
129+
Set<GrantedAuthority> authorities = Collections.singleton(new OAuth2UserAuthority(userAttributes));
84130

85131
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
86132
}
133+
134+
/**
135+
* Sets the {@link Converter} used for converting the {@link OAuth2UserRequest}
136+
* to a {@link RequestEntity} representation of the UserInfo Request.
137+
*
138+
* @since 5.1
139+
* @param requestEntityConverter the {@link Converter} used for converting to a {@link RequestEntity} representation of the UserInfo Request
140+
*/
141+
public final void setRequestEntityConverter(Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter) {
142+
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
143+
this.requestEntityConverter = requestEntityConverter;
144+
}
145+
146+
/**
147+
* Sets the {@link RestOperations} used when requesting the UserInfo resource.
148+
*
149+
* <p>
150+
* <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured with the following:
151+
* <ol>
152+
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
153+
* </ol>
154+
*
155+
* @since 5.1
156+
* @param restOperations the {@link RestOperations} used when requesting the UserInfo resource
157+
*/
158+
public final void setRestOperations(RestOperations restOperations) {
159+
Assert.notNull(restOperations, "restOperations cannot be null");
160+
this.restOperations = restOperations;
161+
}
87162
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2002-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.security.oauth2.client.userinfo;
17+
18+
import org.springframework.core.convert.converter.Converter;
19+
import org.springframework.http.HttpHeaders;
20+
import org.springframework.http.HttpMethod;
21+
import org.springframework.http.MediaType;
22+
import org.springframework.http.RequestEntity;
23+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
24+
import org.springframework.security.oauth2.core.AuthenticationMethod;
25+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
26+
import org.springframework.util.LinkedMultiValueMap;
27+
import org.springframework.util.MultiValueMap;
28+
import org.springframework.web.util.UriComponentsBuilder;
29+
30+
import java.net.URI;
31+
import java.util.Collections;
32+
33+
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
34+
35+
/**
36+
* A {@link Converter} that converts the provided {@link OAuth2UserRequest}
37+
* to a {@link RequestEntity} representation of a request for the UserInfo Endpoint.
38+
*
39+
* @author Joe Grandja
40+
* @since 5.1
41+
* @see Converter
42+
* @see OAuth2UserRequest
43+
* @see RequestEntity
44+
*/
45+
public class OAuth2UserRequestEntityConverter implements Converter<OAuth2UserRequest, RequestEntity<?>> {
46+
private static final MediaType DEFAULT_CONTENT_TYPE = MediaType.valueOf(APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
47+
48+
/**
49+
* Returns the {@link RequestEntity} used for the UserInfo Request.
50+
*
51+
* @param userRequest the user request
52+
* @return the {@link RequestEntity} used for the UserInfo Request
53+
*/
54+
@Override
55+
public RequestEntity<?> convert(OAuth2UserRequest userRequest) {
56+
ClientRegistration clientRegistration = userRequest.getClientRegistration();
57+
58+
HttpMethod httpMethod = HttpMethod.GET;
59+
if (AuthenticationMethod.FORM.equals(clientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod())) {
60+
httpMethod = HttpMethod.POST;
61+
}
62+
HttpHeaders headers = new HttpHeaders();
63+
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
64+
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri())
65+
.build()
66+
.toUri();
67+
68+
RequestEntity<?> request;
69+
if (HttpMethod.POST.equals(httpMethod)) {
70+
headers.setContentType(DEFAULT_CONTENT_TYPE);
71+
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
72+
formParameters.add(OAuth2ParameterNames.ACCESS_TOKEN, userRequest.getAccessToken().getTokenValue());
73+
request = new RequestEntity<>(formParameters, headers, httpMethod, uri);
74+
} else {
75+
headers.setBearerAuth(userRequest.getAccessToken().getTokenValue());
76+
request = new RequestEntity<>(headers, httpMethod, uri);
77+
}
78+
79+
return request;
80+
}
81+
}

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/http/OAuth2ErrorResponseErrorHandlerTests.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.security.oauth2.client.http;
1717

1818
import org.junit.Test;
19+
import org.springframework.http.HttpHeaders;
1920
import org.springframework.http.HttpStatus;
2021
import org.springframework.mock.http.client.MockClientHttpResponse;
2122
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
@@ -31,7 +32,7 @@ public class OAuth2ErrorResponseErrorHandlerTests {
3132
private OAuth2ErrorResponseErrorHandler errorHandler = new OAuth2ErrorResponseErrorHandler();
3233

3334
@Test
34-
public void handleErrorWhenStatusBadRequestThenHandled() {
35+
public void handleErrorWhenErrorResponseBodyThenHandled() {
3536
String errorResponse = "{\n" +
3637
" \"error\": \"unauthorized_client\",\n" +
3738
" \"error_description\": \"The client is not authorized\"\n" +
@@ -44,4 +45,17 @@ public void handleErrorWhenStatusBadRequestThenHandled() {
4445
.isInstanceOf(OAuth2AuthenticationException.class)
4546
.hasMessage("[unauthorized_client] The client is not authorized");
4647
}
48+
49+
@Test
50+
public void handleErrorWhenErrorResponseWwwAuthenticateHeaderThenHandled() {
51+
String wwwAuthenticateHeader = "Bearer realm=\"auth-realm\" error=\"insufficient_scope\" error_description=\"The access token expired\"";
52+
53+
MockClientHttpResponse response = new MockClientHttpResponse(
54+
new byte[0], HttpStatus.BAD_REQUEST);
55+
response.getHeaders().add(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticateHeader);
56+
57+
assertThatThrownBy(() -> this.errorHandler.handleError(response))
58+
.isInstanceOf(OAuth2AuthenticationException.class)
59+
.hasMessage("[insufficient_scope] The access token expired");
60+
}
4761
}

0 commit comments

Comments
 (0)