Skip to content

Commit 229b69d

Browse files
committed
Add DefaultAuthorizationCodeTokenResponseClient
Fixes gh-5547
1 parent f7cb53e commit 229b69d

File tree

13 files changed

+1484
-4
lines changed

13 files changed

+1484
-4
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2ClientConfigurer.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
2121
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
2222
import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationProvider;
23-
import org.springframework.security.oauth2.client.endpoint.NimbusAuthorizationCodeTokenResponseClient;
23+
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
2424
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
2525
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
2626
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
@@ -250,7 +250,7 @@ private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> get
250250
if (this.accessTokenResponseClient != null) {
251251
return this.accessTokenResponseClient;
252252
}
253-
return new NimbusAuthorizationCodeTokenResponseClient();
253+
return new DefaultAuthorizationCodeTokenResponseClient();
254254
}
255255
}
256256

config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
3030
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider;
3131
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
32-
import org.springframework.security.oauth2.client.endpoint.NimbusAuthorizationCodeTokenResponseClient;
32+
import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
3333
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
3434
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
3535
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider;
@@ -450,7 +450,7 @@ public void init(B http) throws Exception {
450450
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient =
451451
this.tokenEndpointConfig.accessTokenResponseClient;
452452
if (accessTokenResponseClient == null) {
453-
accessTokenResponseClient = new NimbusAuthorizationCodeTokenResponseClient();
453+
accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
454454
}
455455

456456
OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = this.userInfoEndpointConfig.userService;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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.endpoint;
17+
18+
import org.springframework.core.convert.converter.Converter;
19+
import org.springframework.http.RequestEntity;
20+
import org.springframework.http.ResponseEntity;
21+
import org.springframework.http.converter.FormHttpMessageConverter;
22+
import org.springframework.http.converter.HttpMessageConverter;
23+
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
24+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
25+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
26+
import org.springframework.security.oauth2.core.OAuth2Error;
27+
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
28+
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
29+
import org.springframework.util.Assert;
30+
import org.springframework.util.CollectionUtils;
31+
import org.springframework.web.client.ResponseErrorHandler;
32+
import org.springframework.web.client.RestClientException;
33+
import org.springframework.web.client.RestOperations;
34+
import org.springframework.web.client.RestTemplate;
35+
36+
import java.util.Arrays;
37+
38+
/**
39+
* The default implementation of an {@link OAuth2AccessTokenResponseClient}
40+
* for the {@link AuthorizationGrantType#AUTHORIZATION_CODE authorization_code} grant.
41+
* This implementation uses a {@link RestOperations} when requesting
42+
* an access token credential at the Authorization Server's Token Endpoint.
43+
*
44+
* @author Joe Grandja
45+
* @since 5.1
46+
* @see OAuth2AccessTokenResponseClient
47+
* @see OAuth2AuthorizationCodeGrantRequest
48+
* @see OAuth2AccessTokenResponse
49+
* @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>
50+
* @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>
51+
*/
52+
public final class DefaultAuthorizationCodeTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
53+
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
54+
55+
private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter =
56+
new OAuth2AuthorizationCodeGrantRequestEntityConverter();
57+
58+
private RestOperations restOperations;
59+
60+
public DefaultAuthorizationCodeTokenResponseClient() {
61+
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
62+
new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
63+
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
64+
this.restOperations = restTemplate;
65+
}
66+
67+
@Override
68+
public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) throws OAuth2AuthenticationException {
69+
Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");
70+
71+
RequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest);
72+
73+
ResponseEntity<OAuth2AccessTokenResponse> response;
74+
try {
75+
response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
76+
} catch (RestClientException ex) {
77+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
78+
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
79+
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
80+
}
81+
82+
OAuth2AccessTokenResponse tokenResponse = response.getBody();
83+
84+
if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
85+
// As per spec, in Section 5.1 Successful Access Token Response
86+
// https://tools.ietf.org/html/rfc6749#section-5.1
87+
// If AccessTokenResponse.scope is empty, then default to the scope
88+
// originally requested by the client in the Token Request
89+
tokenResponse = OAuth2AccessTokenResponse.withResponse(tokenResponse)
90+
.scopes(authorizationCodeGrantRequest.getClientRegistration().getScopes())
91+
.build();
92+
}
93+
94+
return tokenResponse;
95+
}
96+
97+
/**
98+
* Sets the {@link Converter} used for converting the {@link OAuth2AuthorizationCodeGrantRequest}
99+
* to a {@link RequestEntity} representation of the OAuth 2.0 Access Token Request.
100+
*
101+
* @param requestEntityConverter the {@link Converter} used for converting to a {@link RequestEntity} representation of the Access Token Request
102+
*/
103+
public void setRequestEntityConverter(Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter) {
104+
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
105+
this.requestEntityConverter = requestEntityConverter;
106+
}
107+
108+
/**
109+
* Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token Response.
110+
*
111+
* <p>
112+
* <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured with the following:
113+
* <ol>
114+
* <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and {@link OAuth2AccessTokenResponseHttpMessageConverter}</li>
115+
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
116+
* </ol>
117+
*
118+
* @param restOperations the {@link RestOperations} used when requesting the Access Token Response
119+
*/
120+
public void setRestOperations(RestOperations restOperations) {
121+
Assert.notNull(restOperations, "restOperations cannot be null");
122+
this.restOperations = restOperations;
123+
}
124+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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.endpoint;
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.endpoint.OAuth2AuthorizationCodeGrantRequest;
24+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
25+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
26+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
27+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
28+
import org.springframework.util.LinkedMultiValueMap;
29+
import org.springframework.util.MultiValueMap;
30+
import org.springframework.web.util.UriComponentsBuilder;
31+
32+
import java.net.URI;
33+
import java.util.Collections;
34+
35+
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
36+
37+
/**
38+
* A {@link Converter} that converts the provided {@link OAuth2AuthorizationCodeGrantRequest}
39+
* to a {@link RequestEntity} representation of an OAuth 2.0 Access Token Request
40+
* for the Authorization Code Grant.
41+
*
42+
* @author Joe Grandja
43+
* @since 5.1
44+
* @see Converter
45+
* @see OAuth2AuthorizationCodeGrantRequest
46+
* @see RequestEntity
47+
*/
48+
public class OAuth2AuthorizationCodeGrantRequestEntityConverter implements Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {
49+
50+
/**
51+
* Returns the {@link RequestEntity} used for the Access Token Request.
52+
*
53+
* @param authorizationCodeGrantRequest the authorization code grant request
54+
* @return the {@link RequestEntity} used for the Access Token Request
55+
*/
56+
@Override
57+
public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
58+
ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
59+
60+
HttpHeaders headers = this.buildHeaders(authorizationCodeGrantRequest);
61+
MultiValueMap<String, String> formParameters = this.buildFormParameters(authorizationCodeGrantRequest);
62+
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri())
63+
.build()
64+
.toUri();
65+
66+
return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri);
67+
}
68+
69+
/**
70+
* Returns the {@link HttpHeaders} used for the Access Token Request.
71+
*
72+
* @param authorizationCodeGrantRequest the authorization code grant request
73+
* @return the {@link HttpHeaders} used for the Access Token Request
74+
*/
75+
private HttpHeaders buildHeaders(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
76+
ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
77+
78+
HttpHeaders headers = new HttpHeaders();
79+
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
80+
final MediaType contentType = MediaType.valueOf(APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8");
81+
headers.setContentType(contentType);
82+
if (ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
83+
headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret());
84+
}
85+
86+
return headers;
87+
}
88+
89+
/**
90+
* Returns a {@link MultiValueMap} of the form parameters used for the Access Token Request body.
91+
*
92+
* @param authorizationCodeGrantRequest the authorization code grant request
93+
* @return a {@link MultiValueMap} of the form parameters used for the Access Token Request body
94+
*/
95+
private MultiValueMap<String, String> buildFormParameters(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
96+
ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
97+
OAuth2AuthorizationExchange authorizationExchange = authorizationCodeGrantRequest.getAuthorizationExchange();
98+
99+
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
100+
formParameters.add(OAuth2ParameterNames.GRANT_TYPE, authorizationCodeGrantRequest.getGrantType().getValue());
101+
formParameters.add(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode());
102+
formParameters.add(OAuth2ParameterNames.REDIRECT_URI, authorizationExchange.getAuthorizationRequest().getRedirectUri());
103+
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
104+
formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
105+
formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
106+
}
107+
108+
return formParameters;
109+
}
110+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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.http;
17+
18+
import org.springframework.http.HttpStatus;
19+
import org.springframework.http.client.ClientHttpResponse;
20+
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
21+
import org.springframework.security.oauth2.core.OAuth2Error;
22+
import org.springframework.security.oauth2.core.http.converter.OAuth2ErrorHttpMessageConverter;
23+
import org.springframework.web.client.DefaultResponseErrorHandler;
24+
import org.springframework.web.client.ResponseErrorHandler;
25+
26+
import java.io.IOException;
27+
28+
/**
29+
* A {@link ResponseErrorHandler} that handles an {@link OAuth2Error OAuth 2.0 Error}.
30+
*
31+
* @see ResponseErrorHandler
32+
* @see OAuth2Error
33+
* @author Joe Grandja
34+
* @since 5.1
35+
*/
36+
public class OAuth2ErrorResponseErrorHandler implements ResponseErrorHandler {
37+
private final OAuth2ErrorHttpMessageConverter oauth2ErrorConverter = new OAuth2ErrorHttpMessageConverter();
38+
private final ResponseErrorHandler defaultErrorHandler = new DefaultResponseErrorHandler();
39+
40+
@Override
41+
public boolean hasError(ClientHttpResponse response) throws IOException {
42+
return this.defaultErrorHandler.hasError(response);
43+
}
44+
45+
@Override
46+
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());
50+
}
51+
this.defaultErrorHandler.handleError(response);
52+
}
53+
}

0 commit comments

Comments
 (0)