Skip to content

Commit e3875ed

Browse files
committed
Add refresh_token OAuth2AccessTokenResponseClient
1 parent c84990b commit e3875ed

File tree

6 files changed

+680
-0
lines changed

6 files changed

+680
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright 2002-2019 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+
* https://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.OAuth2AuthorizationException;
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#REFRESH_TOKEN refresh_token} 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.2
46+
* @see OAuth2AccessTokenResponseClient
47+
* @see OAuth2RefreshTokenGrantRequest
48+
* @see OAuth2AccessTokenResponse
49+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-6">Section 6 Refreshing an Access Token</a>
50+
*/
51+
public final class DefaultRefreshTokenTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> {
52+
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
53+
54+
private Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>> requestEntityConverter =
55+
new OAuth2RefreshTokenGrantRequestEntityConverter();
56+
57+
private RestOperations restOperations;
58+
59+
public DefaultRefreshTokenTokenResponseClient() {
60+
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
61+
new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
62+
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
63+
this.restOperations = restTemplate;
64+
}
65+
66+
@Override
67+
public OAuth2AccessTokenResponse getTokenResponse(OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest) {
68+
Assert.notNull(refreshTokenGrantRequest, "refreshTokenGrantRequest cannot be null");
69+
70+
RequestEntity<?> request = this.requestEntityConverter.convert(refreshTokenGrantRequest);
71+
72+
ResponseEntity<OAuth2AccessTokenResponse> response;
73+
try {
74+
response = this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
75+
} catch (RestClientException ex) {
76+
OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
77+
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + ex.getMessage(), null);
78+
throw new OAuth2AuthorizationException(oauth2Error, ex);
79+
}
80+
81+
OAuth2AccessTokenResponse tokenResponse = response.getBody();
82+
83+
if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes()) ||
84+
tokenResponse.getRefreshToken() == null) {
85+
OAuth2AccessTokenResponse.Builder tokenResponseBuilder = OAuth2AccessTokenResponse.withResponse(tokenResponse);
86+
87+
if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
88+
// As per spec, in Section 5.1 Successful Access Token Response
89+
// https://tools.ietf.org/html/rfc6749#section-5.1
90+
// If AccessTokenResponse.scope is empty, then default to the scope
91+
// originally requested by the client in the Token Request
92+
tokenResponseBuilder.scopes(refreshTokenGrantRequest.getAuthorizedClient().getAccessToken().getScopes());
93+
}
94+
95+
if (tokenResponse.getRefreshToken() == null) {
96+
// Reuse existing refresh token
97+
tokenResponseBuilder.refreshToken(refreshTokenGrantRequest.getAuthorizedClient().getRefreshToken().getTokenValue());
98+
}
99+
100+
tokenResponse = tokenResponseBuilder.build();
101+
}
102+
103+
return tokenResponse;
104+
}
105+
106+
/**
107+
* Sets the {@link Converter} used for converting the {@link OAuth2RefreshTokenGrantRequest}
108+
* to a {@link RequestEntity} representation of the OAuth 2.0 Access Token Request.
109+
*
110+
* @param requestEntityConverter the {@link Converter} used for converting to a {@link RequestEntity} representation of the Access Token Request
111+
*/
112+
public void setRequestEntityConverter(Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>> requestEntityConverter) {
113+
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
114+
this.requestEntityConverter = requestEntityConverter;
115+
}
116+
117+
/**
118+
* Sets the {@link RestOperations} used when requesting the OAuth 2.0 Access Token Response.
119+
*
120+
* <p>
121+
* <b>NOTE:</b> At a minimum, the supplied {@code restOperations} must be configured with the following:
122+
* <ol>
123+
* <li>{@link HttpMessageConverter}'s - {@link FormHttpMessageConverter} and {@link OAuth2AccessTokenResponseHttpMessageConverter}</li>
124+
* <li>{@link ResponseErrorHandler} - {@link OAuth2ErrorResponseErrorHandler}</li>
125+
* </ol>
126+
*
127+
* @param restOperations the {@link RestOperations} used when requesting the Access Token Response
128+
*/
129+
public void setRestOperations(RestOperations restOperations) {
130+
Assert.notNull(restOperations, "restOperations cannot be null");
131+
this.restOperations = restOperations;
132+
}
133+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2002-2019 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+
* https://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.security.oauth2.client.OAuth2AuthorizedClient;
19+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
20+
import org.springframework.util.Assert;
21+
22+
import java.util.Collections;
23+
import java.util.LinkedHashSet;
24+
import java.util.Set;
25+
26+
/**
27+
* An OAuth 2.0 Refresh Token Grant request that holds
28+
* the {@link OAuth2AuthorizedClient authorized client}.
29+
*
30+
* @author Joe Grandja
31+
* @since 5.2
32+
* @see AbstractOAuth2AuthorizationGrantRequest
33+
* @see OAuth2AuthorizedClient
34+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc6749#section-6">Section 6 Refreshing an Access Token</a>
35+
*/
36+
public class OAuth2RefreshTokenGrantRequest extends AbstractOAuth2AuthorizationGrantRequest {
37+
private final OAuth2AuthorizedClient authorizedClient;
38+
private final Set<String> scopes;
39+
40+
/**
41+
* Constructs an {@code OAuth2RefreshTokenGrantRequest} using the provided parameters.
42+
*
43+
* @param authorizedClient the authorized client
44+
*/
45+
public OAuth2RefreshTokenGrantRequest(OAuth2AuthorizedClient authorizedClient) {
46+
this(authorizedClient, Collections.emptySet());
47+
}
48+
49+
/**
50+
* Constructs an {@code OAuth2RefreshTokenGrantRequest} using the provided parameters.
51+
*
52+
* @param authorizedClient the authorized client
53+
* @param scopes the scopes
54+
*/
55+
public OAuth2RefreshTokenGrantRequest(OAuth2AuthorizedClient authorizedClient, Set<String> scopes) {
56+
super(AuthorizationGrantType.REFRESH_TOKEN);
57+
Assert.notNull(authorizedClient, "authorizedClient cannot be null");
58+
Assert.notNull(authorizedClient.getRefreshToken(), "authorizedClient.refreshToken cannot be null");
59+
this.authorizedClient = authorizedClient;
60+
this.scopes = Collections.unmodifiableSet(scopes != null ?
61+
new LinkedHashSet<>(scopes) : Collections.emptySet());
62+
63+
}
64+
65+
/**
66+
* Returns the {@link OAuth2AuthorizedClient authorized client}.
67+
*
68+
* @return the {@link OAuth2AuthorizedClient}
69+
*/
70+
public OAuth2AuthorizedClient getAuthorizedClient() {
71+
return this.authorizedClient;
72+
}
73+
74+
/**
75+
* Returns the scope(s).
76+
*
77+
* @return the scope(s)
78+
*/
79+
public Set<String> getScopes() {
80+
return this.scopes;
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2002-2019 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+
* https://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.RequestEntity;
22+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
23+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
24+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
25+
import org.springframework.util.CollectionUtils;
26+
import org.springframework.util.LinkedMultiValueMap;
27+
import org.springframework.util.MultiValueMap;
28+
import org.springframework.util.StringUtils;
29+
import org.springframework.web.util.UriComponentsBuilder;
30+
31+
import java.net.URI;
32+
33+
/**
34+
* A {@link Converter} that converts the provided {@link OAuth2RefreshTokenGrantRequest}
35+
* to a {@link RequestEntity} representation of an OAuth 2.0 Access Token Request
36+
* for the Refresh Token Grant.
37+
*
38+
* @author Joe Grandja
39+
* @since 5.2
40+
* @see Converter
41+
* @see OAuth2RefreshTokenGrantRequest
42+
* @see RequestEntity
43+
*/
44+
public class OAuth2RefreshTokenGrantRequestEntityConverter implements Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>> {
45+
46+
/**
47+
* Returns the {@link RequestEntity} used for the Access Token Request.
48+
*
49+
* @param refreshTokenGrantRequest the refresh token grant request
50+
* @return the {@link RequestEntity} used for the Access Token Request
51+
*/
52+
@Override
53+
public RequestEntity<?> convert(OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest) {
54+
ClientRegistration clientRegistration = refreshTokenGrantRequest.getAuthorizedClient().getClientRegistration();
55+
56+
HttpHeaders headers = OAuth2AuthorizationGrantRequestEntityUtils.getTokenRequestHeaders(clientRegistration);
57+
MultiValueMap<String, String> formParameters = buildFormParameters(refreshTokenGrantRequest);
58+
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getTokenUri())
59+
.build()
60+
.toUri();
61+
62+
return new RequestEntity<>(formParameters, headers, HttpMethod.POST, uri);
63+
}
64+
65+
/**
66+
* Returns a {@link MultiValueMap} of the form parameters used for the Access Token Request body.
67+
*
68+
* @param refreshTokenGrantRequest the refresh token grant request
69+
* @return a {@link MultiValueMap} of the form parameters used for the Access Token Request body
70+
*/
71+
private MultiValueMap<String, String> buildFormParameters(OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest) {
72+
ClientRegistration clientRegistration = refreshTokenGrantRequest.getAuthorizedClient().getClientRegistration();
73+
74+
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
75+
formParameters.add(OAuth2ParameterNames.GRANT_TYPE, refreshTokenGrantRequest.getGrantType().getValue());
76+
formParameters.add(OAuth2ParameterNames.REFRESH_TOKEN,
77+
refreshTokenGrantRequest.getAuthorizedClient().getRefreshToken().getTokenValue());
78+
if (!CollectionUtils.isEmpty(refreshTokenGrantRequest.getScopes())) {
79+
formParameters.add(OAuth2ParameterNames.SCOPE,
80+
StringUtils.collectionToDelimitedString(refreshTokenGrantRequest.getScopes(), " "));
81+
}
82+
if (ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
83+
formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
84+
formParameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
85+
}
86+
87+
return formParameters;
88+
}
89+
}

0 commit comments

Comments
 (0)