Skip to content

Commit 5f26dae

Browse files
committed
Error On Unsupported Client Authentication Methods
Closes gh-13144
1 parent b472a06 commit 5f26dae

14 files changed

+275
-24
lines changed

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/AbstractWebClientReactiveOAuth2AccessTokenResponseClient.java

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -42,6 +42,7 @@
4242
import org.springframework.web.reactive.function.BodyInserters;
4343
import org.springframework.web.reactive.function.client.ClientResponse;
4444
import org.springframework.web.reactive.function.client.WebClient;
45+
import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec;
4546

4647
/**
4748
* Abstract base class for all of the {@code WebClientReactive*TokenResponseClient}s that
@@ -70,6 +71,8 @@ public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T
7071

7172
private WebClient webClient = WebClient.builder().build();
7273

74+
private Converter<T, RequestHeadersSpec<?>> requestEntityConverter = this::validatingPopulateRequest;
75+
7376
private Converter<T, HttpHeaders> headersConverter = this::populateTokenRequestHeaders;
7477

7578
private Converter<T, MultiValueMap<String, String>> parametersConverter = this::populateTokenRequestParameters;
@@ -84,15 +87,7 @@ public abstract class AbstractWebClientReactiveOAuth2AccessTokenResponseClient<T
8487
public Mono<OAuth2AccessTokenResponse> getTokenResponse(T grantRequest) {
8588
Assert.notNull(grantRequest, "grantRequest cannot be null");
8689
// @formatter:off
87-
return Mono.defer(() -> this.webClient.post()
88-
.uri(clientRegistration(grantRequest).getProviderDetails().getTokenUri())
89-
.headers((headers) -> {
90-
HttpHeaders headersToAdd = getHeadersConverter().convert(grantRequest);
91-
if (headersToAdd != null) {
92-
headers.addAll(headersToAdd);
93-
}
94-
})
95-
.body(createTokenRequestBody(grantRequest))
90+
return Mono.defer(() -> this.requestEntityConverter.convert(grantRequest)
9691
.exchange()
9792
.flatMap((response) -> readTokenResponse(grantRequest, response))
9893
);
@@ -106,6 +101,34 @@ public Mono<OAuth2AccessTokenResponse> getTokenResponse(T grantRequest) {
106101
*/
107102
abstract ClientRegistration clientRegistration(T grantRequest);
108103

104+
private RequestHeadersSpec<?> validatingPopulateRequest(T grantRequest) {
105+
validateClientAuthenticationMethod(grantRequest);
106+
return populateRequest(grantRequest);
107+
}
108+
109+
private void validateClientAuthenticationMethod(T grantRequest) {
110+
ClientRegistration clientRegistration = grantRequest.getClientRegistration();
111+
ClientAuthenticationMethod clientAuthenticationMethod = clientRegistration.getClientAuthenticationMethod();
112+
boolean supportedClientAuthenticationMethod = clientAuthenticationMethod.equals(ClientAuthenticationMethod.NONE)
113+
|| clientAuthenticationMethod.equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
114+
|| clientAuthenticationMethod.equals(ClientAuthenticationMethod.CLIENT_SECRET_POST);
115+
if (!supportedClientAuthenticationMethod) {
116+
throw new IllegalArgumentException(String.format(
117+
"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].",
118+
clientRegistration.getRegistrationId(), clientAuthenticationMethod, clientAuthenticationMethod));
119+
}
120+
}
121+
122+
private RequestHeadersSpec<?> populateRequest(T grantRequest) {
123+
return this.webClient.post().uri(clientRegistration(grantRequest).getProviderDetails().getTokenUri())
124+
.headers((headers) -> {
125+
HttpHeaders headersToAdd = getHeadersConverter().convert(grantRequest);
126+
if (headersToAdd != null) {
127+
headers.addAll(headersToAdd);
128+
}
129+
}).body(createTokenRequestBody(grantRequest));
130+
}
131+
109132
/**
110133
* Populates the headers for the token request.
111134
* @param grantRequest the grant request
@@ -280,6 +303,7 @@ final Converter<T, HttpHeaders> getHeadersConverter() {
280303
public final void setHeadersConverter(Converter<T, HttpHeaders> headersConverter) {
281304
Assert.notNull(headersConverter, "headersConverter cannot be null");
282305
this.headersConverter = headersConverter;
306+
this.requestEntityConverter = this::populateRequest;
283307
}
284308

285309
/**
@@ -307,6 +331,7 @@ public final void addHeadersConverter(Converter<T, HttpHeaders> headersConverter
307331
}
308332
return headers;
309333
};
334+
this.requestEntityConverter = this::populateRequest;
310335
}
311336

312337
/**
@@ -331,6 +356,7 @@ final Converter<T, MultiValueMap<String, String>> getParametersConverter() {
331356
public final void setParametersConverter(Converter<T, MultiValueMap<String, String>> parametersConverter) {
332357
Assert.notNull(parametersConverter, "parametersConverter cannot be null");
333358
this.parametersConverter = parametersConverter;
359+
this.requestEntityConverter = this::populateRequest;
334360
}
335361

336362
/**
@@ -357,6 +383,7 @@ public final void addParametersConverter(Converter<T, MultiValueMap<String, Stri
357383
}
358384
return parameters;
359385
};
386+
this.requestEntityConverter = this::populateRequest;
360387
}
361388

362389
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2002-2023 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.RequestEntity;
21+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
22+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
23+
import org.springframework.util.Assert;
24+
25+
class ClientAuthenticationMethodValidatingRequestEntityConverter<T extends AbstractOAuth2AuthorizationGrantRequest>
26+
implements Converter<T, RequestEntity<?>> {
27+
28+
private final Converter<T, RequestEntity<?>> delegate;
29+
30+
ClientAuthenticationMethodValidatingRequestEntityConverter(Converter<T, RequestEntity<?>> delegate) {
31+
this.delegate = delegate;
32+
}
33+
34+
@Override
35+
public RequestEntity<?> convert(T grantRequest) {
36+
ClientRegistration clientRegistration = grantRequest.getClientRegistration();
37+
ClientAuthenticationMethod clientAuthenticationMethod = clientRegistration.getClientAuthenticationMethod();
38+
String registrationId = clientRegistration.getRegistrationId();
39+
boolean supportedClientAuthenticationMethod = clientAuthenticationMethod.equals(ClientAuthenticationMethod.NONE)
40+
|| clientAuthenticationMethod.equals(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
41+
|| clientAuthenticationMethod.equals(ClientAuthenticationMethod.CLIENT_SECRET_POST);
42+
Assert.isTrue(supportedClientAuthenticationMethod, () -> String.format(
43+
"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 `setRequestEntityConverter` to supply an instance that supports [%s].",
44+
registrationId, clientAuthenticationMethod, clientAuthenticationMethod));
45+
return this.delegate.convert(grantRequest);
46+
}
47+
48+
}

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ public final class DefaultAuthorizationCodeTokenResponseClient
5858

5959
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
6060

61-
private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
61+
private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter = new ClientAuthenticationMethodValidatingRequestEntityConverter<>(
62+
new OAuth2AuthorizationCodeGrantRequestEntityConverter());
6263

6364
private RestOperations restOperations;
6465

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClient.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -58,7 +58,8 @@ public final class DefaultClientCredentialsTokenResponseClient
5858

5959
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
6060

61-
private Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> requestEntityConverter = new OAuth2ClientCredentialsGrantRequestEntityConverter();
61+
private Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>> requestEntityConverter = new ClientAuthenticationMethodValidatingRequestEntityConverter<>(
62+
new OAuth2ClientCredentialsGrantRequestEntityConverter());
6263

6364
private RestOperations restOperations;
6465

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClient.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -56,7 +56,8 @@ public final class DefaultJwtBearerTokenResponseClient
5656

5757
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
5858

59-
private Converter<JwtBearerGrantRequest, RequestEntity<?>> requestEntityConverter = new JwtBearerGrantRequestEntityConverter();
59+
private Converter<JwtBearerGrantRequest, RequestEntity<?>> requestEntityConverter = new ClientAuthenticationMethodValidatingRequestEntityConverter<>(
60+
new JwtBearerGrantRequestEntityConverter());
6061

6162
private RestOperations restOperations;
6263

oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/endpoint/DefaultRefreshTokenTokenResponseClient.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -55,7 +55,8 @@ public final class DefaultRefreshTokenTokenResponseClient
5555

5656
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
5757

58-
private Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>> requestEntityConverter = new OAuth2RefreshTokenGrantRequestEntityConverter();
58+
private Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>> requestEntityConverter = new ClientAuthenticationMethodValidatingRequestEntityConverter<>(
59+
new OAuth2RefreshTokenGrantRequestEntityConverter());
5960

6061
private RestOperations restOperations;
6162

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultAuthorizationCodeTokenResponseClientTests.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,28 @@ public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthorizationE
370370
+ "the OAuth 2.0 Access Token Response");
371371
}
372372

373+
// gh-13144
374+
@Test
375+
public void getTokenResponseWhenCustomClientAuthenticationMethodThenIllegalArgument() {
376+
ClientRegistration clientRegistration = this.clientRegistration
377+
.clientAuthenticationMethod(new ClientAuthenticationMethod("basic")).build();
378+
OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest = authorizationCodeGrantRequest(
379+
clientRegistration);
380+
assertThatExceptionOfType(IllegalArgumentException.class)
381+
.isThrownBy(() -> this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest));
382+
}
383+
384+
// gh-13144
385+
@Test
386+
public void getTokenResponseWhenUnsupportedClientAuthenticationMethodThenIllegalArgument() {
387+
ClientRegistration clientRegistration = this.clientRegistration
388+
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT).build();
389+
OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest = authorizationCodeGrantRequest(
390+
clientRegistration);
391+
assertThatExceptionOfType(IllegalArgumentException.class)
392+
.isThrownBy(() -> this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest));
393+
}
394+
373395
private OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest(ClientRegistration clientRegistration) {
374396
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
375397
.clientId(clientRegistration.getClientId()).state("state-1234")

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultClientCredentialsTokenResponseClientTests.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -379,6 +379,28 @@ public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthorizationE
379379
"[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response");
380380
}
381381

382+
// gh-13144
383+
@Test
384+
public void getTokenResponseWhenCustomClientAuthenticationMethodThenIllegalArgument() {
385+
ClientRegistration clientRegistration = this.clientRegistration
386+
.clientAuthenticationMethod(new ClientAuthenticationMethod("basic")).build();
387+
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = new OAuth2ClientCredentialsGrantRequest(
388+
clientRegistration);
389+
assertThatExceptionOfType(IllegalArgumentException.class)
390+
.isThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest));
391+
}
392+
393+
// gh-13144
394+
@Test
395+
public void getTokenResponseWhenUnsupportedClientAuthenticationMethodThenIllegalArgument() {
396+
ClientRegistration clientRegistration = this.clientRegistration
397+
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT).build();
398+
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest = new OAuth2ClientCredentialsGrantRequest(
399+
clientRegistration);
400+
assertThatExceptionOfType(IllegalArgumentException.class)
401+
.isThrownBy(() -> this.tokenResponseClient.getTokenResponse(clientCredentialsGrantRequest));
402+
}
403+
382404
private MockResponse jsonResponse(String json) {
383405
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).setBody(json);
384406
}

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultJwtBearerTokenResponseClientTests.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -243,6 +243,26 @@ public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthorizationE
243243
+ "retrieve the OAuth 2.0 Access Token Response");
244244
}
245245

246+
// gh-13144
247+
@Test
248+
public void getTokenResponseWhenCustomClientAuthenticationMethodThenIllegalArgument() {
249+
ClientRegistration clientRegistration = this.clientRegistration
250+
.clientAuthenticationMethod(new ClientAuthenticationMethod("basic")).build();
251+
JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(clientRegistration, this.jwtAssertion);
252+
assertThatExceptionOfType(IllegalArgumentException.class)
253+
.isThrownBy(() -> this.tokenResponseClient.getTokenResponse(jwtBearerGrantRequest));
254+
}
255+
256+
// gh-13144
257+
@Test
258+
public void getTokenResponseWhenUnsupportedClientAuthenticationMethodThenIllegalArgument() {
259+
ClientRegistration clientRegistration = this.clientRegistration
260+
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT).build();
261+
JwtBearerGrantRequest jwtBearerGrantRequest = new JwtBearerGrantRequest(clientRegistration, this.jwtAssertion);
262+
assertThatExceptionOfType(IllegalArgumentException.class)
263+
.isThrownBy(() -> this.tokenResponseClient.getTokenResponse(jwtBearerGrantRequest));
264+
}
265+
246266
private MockResponse jsonResponse(String json) {
247267
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).setBody(json);
248268
}

oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/endpoint/DefaultRefreshTokenTokenResponseClientTests.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -313,6 +313,28 @@ public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthorizationE
313313
+ "retrieve the OAuth 2.0 Access Token Response");
314314
}
315315

316+
// gh-13144
317+
@Test
318+
public void getTokenResponseWhenCustomClientAuthenticationMethodThenIllegalArgument() {
319+
ClientRegistration clientRegistration = this.clientRegistration
320+
.clientAuthenticationMethod(new ClientAuthenticationMethod("basic")).build();
321+
OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest = new OAuth2RefreshTokenGrantRequest(clientRegistration,
322+
this.accessToken, this.refreshToken);
323+
assertThatExceptionOfType(IllegalArgumentException.class)
324+
.isThrownBy(() -> this.tokenResponseClient.getTokenResponse(refreshTokenGrantRequest));
325+
}
326+
327+
// gh-13144
328+
@Test
329+
public void getTokenResponseWhenUnsupportedClientAuthenticationMethodThenIllegalArgument() {
330+
ClientRegistration clientRegistration = this.clientRegistration
331+
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT).build();
332+
OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest = new OAuth2RefreshTokenGrantRequest(clientRegistration,
333+
this.accessToken, this.refreshToken);
334+
assertThatExceptionOfType(IllegalArgumentException.class)
335+
.isThrownBy(() -> this.tokenResponseClient.getTokenResponse(refreshTokenGrantRequest));
336+
}
337+
316338
private MockResponse jsonResponse(String json) {
317339
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).setBody(json);
318340
}

0 commit comments

Comments
 (0)