Skip to content

Commit 2cead9b

Browse files
committed
Add RestClient implementations
Issue gh-15298
1 parent 98975a9 commit 2cead9b

11 files changed

+3257
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
/*
2+
* Copyright 2002-2024 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+
17+
package org.springframework.security.oauth2.client.endpoint;
18+
19+
import org.springframework.core.convert.converter.Converter;
20+
import org.springframework.http.HttpHeaders;
21+
import org.springframework.http.converter.FormHttpMessageConverter;
22+
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
23+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
24+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
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.endpoint.OAuth2ParameterNames;
29+
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
30+
import org.springframework.util.Assert;
31+
import org.springframework.util.LinkedMultiValueMap;
32+
import org.springframework.util.MultiValueMap;
33+
import org.springframework.web.client.RestClient;
34+
import org.springframework.web.client.RestClient.RequestHeadersSpec;
35+
import org.springframework.web.client.RestClientException;
36+
37+
/**
38+
* Abstract base class for {@link RestClient}-based implementations of
39+
* {@link OAuth2AccessTokenResponseClient} that communicate to the Authorization Server's
40+
* Token Endpoint.
41+
* <p>
42+
* Submits a form request body specific to the type of grant request and accepts a JSON
43+
* response body containing an OAuth 2.0 Access Token Response or OAuth 2.0 Error
44+
* Response.
45+
*
46+
* @param <T> type of grant request
47+
* @author Steve Riesenberg
48+
* @since 6.4
49+
* @see <a href="https://tools.ietf.org/html/rfc6749#section-3.2">RFC-6749 Token
50+
* Endpoint</a>
51+
* @see RestClientAuthorizationCodeTokenResponseClient
52+
* @see RestClientClientCredentialsTokenResponseClient
53+
* @see RestClientRefreshTokenTokenResponseClient
54+
* @see RestClientJwtBearerTokenResponseClient
55+
* @see RestClientTokenExchangeTokenResponseClient
56+
* @see DefaultOAuth2TokenRequestHeadersConverter
57+
*/
58+
public abstract class AbstractRestClientOAuth2AccessTokenResponseClient<T extends AbstractOAuth2AuthorizationGrantRequest>
59+
implements OAuth2AccessTokenResponseClient<T> {
60+
61+
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
62+
63+
// @formatter:off
64+
private RestClient restClient = RestClient.builder()
65+
.messageConverters((messageConverters) -> {
66+
messageConverters.clear();
67+
messageConverters.add(new FormHttpMessageConverter());
68+
messageConverters.add(new OAuth2AccessTokenResponseHttpMessageConverter());
69+
})
70+
.defaultStatusHandler(new OAuth2ErrorResponseErrorHandler())
71+
.build();
72+
// @formatter:on
73+
74+
private Converter<T, RequestHeadersSpec<?>> requestEntityConverter = this::validatingPopulateRequest;
75+
76+
private Converter<T, HttpHeaders> headersConverter = new DefaultOAuth2TokenRequestHeadersConverter<>();
77+
78+
private Converter<T, MultiValueMap<String, String>> parametersConverter = this::createParameters;
79+
80+
AbstractRestClientOAuth2AccessTokenResponseClient() {
81+
}
82+
83+
@Override
84+
public OAuth2AccessTokenResponse getTokenResponse(T grantRequest) {
85+
Assert.notNull(grantRequest, "grantRequest cannot be null");
86+
try {
87+
// @formatter:off
88+
OAuth2AccessTokenResponse accessTokenResponse = this.requestEntityConverter.convert(grantRequest)
89+
.retrieve()
90+
.body(OAuth2AccessTokenResponse.class);
91+
// @formatter:on
92+
if (accessTokenResponse == null) {
93+
OAuth2Error error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
94+
"Empty OAuth 2.0 Access Token Response", null);
95+
throw new OAuth2AuthorizationException(error);
96+
}
97+
return accessTokenResponse;
98+
}
99+
catch (RestClientException ex) {
100+
OAuth2Error error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
101+
"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: "
102+
+ ex.getMessage(),
103+
null);
104+
throw new OAuth2AuthorizationException(error, ex);
105+
}
106+
}
107+
108+
private RequestHeadersSpec<?> validatingPopulateRequest(T grantRequest) {
109+
validateClientAuthenticationMethod(grantRequest);
110+
return populateRequest(grantRequest);
111+
}
112+
113+
private void validateClientAuthenticationMethod(T grantRequest) {
114+
ClientRegistration clientRegistration = grantRequest.getClientRegistration();
115+
ClientAuthenticationMethod clientAuthenticationMethod = clientRegistration.getClientAuthenticationMethod();
116+
boolean supportedClientAuthenticationMethod = clientAuthenticationMethod.equals(ClientAuthenticationMethod.NONE)
117+
|| clientAuthenticationMethod.equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
118+
|| clientAuthenticationMethod.equals(ClientAuthenticationMethod.CLIENT_SECRET_POST);
119+
if (!supportedClientAuthenticationMethod) {
120+
throw new IllegalArgumentException(String.format(
121+
"This class supports `client_secret_basic`, `client_secret_post`, and `none` by default. Client [%s] is using [%s] instead. Please use a supported client authentication method, or use `set/addParametersConverter` or `set/addHeadersConverter` to supply an instance that supports [%s].",
122+
clientRegistration.getRegistrationId(), clientAuthenticationMethod, clientAuthenticationMethod));
123+
}
124+
}
125+
126+
private RequestHeadersSpec<?> populateRequest(T grantRequest) {
127+
return this.restClient.post()
128+
.uri(grantRequest.getClientRegistration().getProviderDetails().getTokenUri())
129+
.headers((headers) -> {
130+
HttpHeaders headersToAdd = this.headersConverter.convert(grantRequest);
131+
if (headersToAdd != null) {
132+
headers.addAll(headersToAdd);
133+
}
134+
})
135+
.body(this.parametersConverter.convert(grantRequest));
136+
}
137+
138+
/**
139+
* Returns a {@link MultiValueMap} of the parameters used in the OAuth 2.0 Access
140+
* Token Request body.
141+
* @param grantRequest the authorization grant request
142+
* @return a {@link MultiValueMap} of the parameters used in the OAuth 2.0 Access
143+
* Token Request body
144+
*/
145+
MultiValueMap<String, String> createParameters(T grantRequest) {
146+
ClientRegistration clientRegistration = grantRequest.getClientRegistration();
147+
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
148+
parameters.set(OAuth2ParameterNames.GRANT_TYPE, grantRequest.getGrantType().getValue());
149+
if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC
150+
.equals(clientRegistration.getClientAuthenticationMethod())) {
151+
parameters.set(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
152+
}
153+
if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod())) {
154+
parameters.set(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
155+
}
156+
return parameters;
157+
}
158+
159+
/**
160+
* Sets the {@link RestClient} used when requesting the OAuth 2.0 Access Token
161+
* Response.
162+
* @param restClient the {@link RestClient} used when requesting the Access Token
163+
* Response
164+
*/
165+
public final void setRestClient(RestClient restClient) {
166+
Assert.notNull(restClient, "restClient cannot be null");
167+
this.restClient = restClient;
168+
}
169+
170+
/**
171+
* Sets the {@link Converter} used for converting the
172+
* {@link AbstractOAuth2AuthorizationGrantRequest} instance to a {@link HttpHeaders}
173+
* used in the OAuth 2.0 Access Token Request headers.
174+
* @param headersConverter the {@link Converter} used for converting the
175+
* {@link AbstractOAuth2AuthorizationGrantRequest} to {@link HttpHeaders}
176+
*/
177+
public final void setHeadersConverter(Converter<T, HttpHeaders> headersConverter) {
178+
Assert.notNull(headersConverter, "headersConverter cannot be null");
179+
this.headersConverter = headersConverter;
180+
this.requestEntityConverter = this::populateRequest;
181+
}
182+
183+
/**
184+
* Add (compose) the provided {@code headersConverter} to the current
185+
* {@link Converter} used for converting the
186+
* {@link AbstractOAuth2AuthorizationGrantRequest} instance to a {@link HttpHeaders}
187+
* used in the OAuth 2.0 Access Token Request headers.
188+
* @param headersConverter the {@link Converter} to add (compose) to the current
189+
* {@link Converter} used for converting the
190+
* {@link AbstractOAuth2AuthorizationGrantRequest} to a {@link HttpHeaders}
191+
*/
192+
public final void addHeadersConverter(Converter<T, HttpHeaders> headersConverter) {
193+
Assert.notNull(headersConverter, "headersConverter cannot be null");
194+
Converter<T, HttpHeaders> currentHeadersConverter = this.headersConverter;
195+
this.headersConverter = (authorizationGrantRequest) -> {
196+
// Append headers using a Composite Converter
197+
HttpHeaders headers = currentHeadersConverter.convert(authorizationGrantRequest);
198+
if (headers == null) {
199+
headers = new HttpHeaders();
200+
}
201+
HttpHeaders headersToAdd = headersConverter.convert(authorizationGrantRequest);
202+
if (headersToAdd != null) {
203+
headers.addAll(headersToAdd);
204+
}
205+
return headers;
206+
};
207+
this.requestEntityConverter = this::populateRequest;
208+
}
209+
210+
/**
211+
* Sets the {@link Converter} used for converting the
212+
* {@link AbstractOAuth2AuthorizationGrantRequest} instance to a {@link MultiValueMap}
213+
* used in the OAuth 2.0 Access Token Request body.
214+
* @param parametersConverter the {@link Converter} used for converting the
215+
* {@link AbstractOAuth2AuthorizationGrantRequest} to {@link MultiValueMap}
216+
*/
217+
public final void setParametersConverter(Converter<T, MultiValueMap<String, String>> parametersConverter) {
218+
Assert.notNull(parametersConverter, "parametersConverter cannot be null");
219+
this.parametersConverter = parametersConverter;
220+
this.requestEntityConverter = this::populateRequest;
221+
}
222+
223+
/**
224+
* Add (compose) the provided {@code parametersConverter} to the current
225+
* {@link Converter} used for converting the
226+
* {@link AbstractOAuth2AuthorizationGrantRequest} instance to a {@link MultiValueMap}
227+
* used in the OAuth 2.0 Access Token Request body.
228+
* @param parametersConverter the {@link Converter} to add (compose) to the current
229+
* {@link Converter} used for converting the
230+
* {@link AbstractOAuth2AuthorizationGrantRequest} to a {@link MultiValueMap}
231+
*/
232+
public final void addParametersConverter(Converter<T, MultiValueMap<String, String>> parametersConverter) {
233+
Assert.notNull(parametersConverter, "parametersConverter cannot be null");
234+
Converter<T, MultiValueMap<String, String>> currentParametersConverter = this.parametersConverter;
235+
this.parametersConverter = (authorizationGrantRequest) -> {
236+
MultiValueMap<String, String> parameters = currentParametersConverter.convert(authorizationGrantRequest);
237+
if (parameters == null) {
238+
parameters = new LinkedMultiValueMap<>();
239+
}
240+
MultiValueMap<String, String> parametersToAdd = parametersConverter.convert(authorizationGrantRequest);
241+
if (parametersToAdd != null) {
242+
parameters.addAll(parametersToAdd);
243+
}
244+
return parameters;
245+
};
246+
this.requestEntityConverter = this::populateRequest;
247+
}
248+
249+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2002-2024 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+
17+
package org.springframework.security.oauth2.client.endpoint;
18+
19+
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
20+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
21+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
22+
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
23+
import org.springframework.util.MultiValueMap;
24+
25+
/**
26+
* An implementation of {@link OAuth2AccessTokenResponseClient} that &quot;exchanges&quot;
27+
* an authorization code for an access token at the Authorization Server's Token Endpoint.
28+
*
29+
* @author Steve Riesenberg
30+
* @since 6.4
31+
* @see OAuth2AccessTokenResponseClient
32+
* @see OAuth2AuthorizationCodeGrantRequest
33+
* @see OAuth2AccessTokenResponse
34+
* @see <a target="_blank" href=
35+
* "https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request
36+
* (Authorization Code Grant)</a>
37+
* @see <a target="_blank" href=
38+
* "https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response
39+
* (Authorization Code Grant)</a>
40+
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7636#section-4.2">Section
41+
* 4.2 Client Creates the Code Challenge</a>
42+
*/
43+
public final class RestClientAuthorizationCodeTokenResponseClient
44+
extends AbstractRestClientOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
45+
46+
@Override
47+
MultiValueMap<String, String> createParameters(OAuth2AuthorizationCodeGrantRequest grantRequest) {
48+
OAuth2AuthorizationExchange authorizationExchange = grantRequest.getAuthorizationExchange();
49+
MultiValueMap<String, String> parameters = super.createParameters(grantRequest);
50+
parameters.set(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode());
51+
String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
52+
if (redirectUri != null) {
53+
parameters.set(OAuth2ParameterNames.REDIRECT_URI, redirectUri);
54+
}
55+
String codeVerifier = authorizationExchange.getAuthorizationRequest()
56+
.getAttribute(PkceParameterNames.CODE_VERIFIER);
57+
if (codeVerifier != null) {
58+
parameters.set(PkceParameterNames.CODE_VERIFIER, codeVerifier);
59+
}
60+
return parameters;
61+
}
62+
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2002-2024 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+
17+
package org.springframework.security.oauth2.client.endpoint;
18+
19+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
20+
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
21+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
22+
import org.springframework.util.CollectionUtils;
23+
import org.springframework.util.MultiValueMap;
24+
import org.springframework.util.StringUtils;
25+
26+
/**
27+
* An implementation of {@link OAuth2AccessTokenResponseClient} that &quot;exchanges&quot;
28+
* client credentials for an access token at the Authorization Server's Token Endpoint.
29+
*
30+
* @author Steve Riesenberg
31+
* @since 6.4
32+
* @see OAuth2AccessTokenResponseClient
33+
* @see OAuth2ClientCredentialsGrantRequest
34+
* @see OAuth2AccessTokenResponse
35+
* @see <a target="_blank" href=
36+
* "https://tools.ietf.org/html/rfc6749#section-4.1.3">Section 4.1.3 Access Token Request
37+
* (Authorization Code Grant)</a>
38+
* @see <a target="_blank" href=
39+
* "https://tools.ietf.org/html/rfc6749#section-4.1.4">Section 4.1.4 Access Token Response
40+
* (Authorization Code Grant)</a>
41+
*/
42+
public final class RestClientClientCredentialsTokenResponseClient
43+
extends AbstractRestClientOAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> {
44+
45+
@Override
46+
MultiValueMap<String, String> createParameters(OAuth2ClientCredentialsGrantRequest grantRequest) {
47+
ClientRegistration clientRegistration = grantRequest.getClientRegistration();
48+
MultiValueMap<String, String> parameters = super.createParameters(grantRequest);
49+
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
50+
parameters.set(OAuth2ParameterNames.SCOPE,
51+
StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " "));
52+
}
53+
return parameters;
54+
}
55+
56+
}

0 commit comments

Comments
 (0)