|
16 | 16 |
|
17 | 17 | package org.springframework.security.oauth2.client.endpoint;
|
18 | 18 |
|
| 19 | +import java.nio.charset.StandardCharsets; |
19 | 20 | import java.time.Instant;
|
20 | 21 | import java.util.Collections;
|
21 | 22 | import java.util.HashMap;
|
22 | 23 | import java.util.Map;
|
| 24 | +import java.util.function.Function; |
23 | 25 |
|
| 26 | +import javax.crypto.spec.SecretKeySpec; |
| 27 | + |
| 28 | +import com.nimbusds.jose.jwk.JWK; |
24 | 29 | import okhttp3.mockwebserver.MockResponse;
|
25 | 30 | import okhttp3.mockwebserver.MockWebServer;
|
26 | 31 | import okhttp3.mockwebserver.RecordedRequest;
|
|
36 | 41 | import org.springframework.http.ReactiveHttpInputMessage;
|
37 | 42 | import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
38 | 43 | import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
| 44 | +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; |
39 | 45 | import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
40 | 46 | import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
41 | 47 | import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
|
|
44 | 50 | import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
45 | 51 | import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
46 | 52 | import org.springframework.security.oauth2.core.endpoint.TestOAuth2AccessTokenResponses;
|
| 53 | +import org.springframework.security.oauth2.jose.TestJwks; |
| 54 | +import org.springframework.security.oauth2.jose.TestKeys; |
| 55 | +import org.springframework.util.LinkedMultiValueMap; |
| 56 | +import org.springframework.util.MultiValueMap; |
47 | 57 | import org.springframework.web.reactive.function.BodyExtractor;
|
48 | 58 | import org.springframework.web.reactive.function.client.WebClient;
|
49 | 59 |
|
@@ -112,6 +122,75 @@ public void getTokenResponseWhenSuccessResponseThenReturnAccessTokenResponse() t
|
112 | 122 | assertThat(accessTokenResponse.getAdditionalParameters()).containsEntry("custom_parameter_2", "custom-value-2");
|
113 | 123 | }
|
114 | 124 |
|
| 125 | + @Test |
| 126 | + public void getTokenResponseWhenAuthenticationClientSecretJwtThenFormParametersAreSent() throws Exception { |
| 127 | + // @formatter:off |
| 128 | + String accessTokenSuccessResponse = "{\n" |
| 129 | + + " \"access_token\": \"access-token-1234\",\n" |
| 130 | + + " \"token_type\": \"bearer\",\n" |
| 131 | + + " \"expires_in\": \"3600\"\n" |
| 132 | + + "}\n"; |
| 133 | + // @formatter:on |
| 134 | + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); |
| 135 | + |
| 136 | + // @formatter:off |
| 137 | + ClientRegistration clientRegistration = this.clientRegistration |
| 138 | + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT) |
| 139 | + .clientSecret(TestKeys.DEFAULT_ENCODED_SECRET_KEY) |
| 140 | + .build(); |
| 141 | + // @formatter:on |
| 142 | + |
| 143 | + // Configure Jwt client authentication converter |
| 144 | + SecretKeySpec secretKey = new SecretKeySpec( |
| 145 | + clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8), "HmacSHA256"); |
| 146 | + JWK jwk = TestJwks.jwk(secretKey).build(); |
| 147 | + Function<ClientRegistration, JWK> jwkResolver = (registration) -> jwk; |
| 148 | + configureJwtClientAuthenticationConverter(jwkResolver); |
| 149 | + |
| 150 | + this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest(clientRegistration)).block(); |
| 151 | + RecordedRequest actualRequest = this.server.takeRequest(); |
| 152 | + assertThat(actualRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull(); |
| 153 | + assertThat(actualRequest.getBody().readUtf8()).contains("grant_type=authorization_code", |
| 154 | + "client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer", |
| 155 | + "client_assertion="); |
| 156 | + } |
| 157 | + |
| 158 | + @Test |
| 159 | + public void getTokenResponseWhenAuthenticationPrivateKeyJwtThenFormParametersAreSent() throws Exception { |
| 160 | + // @formatter:off |
| 161 | + String accessTokenSuccessResponse = "{\n" |
| 162 | + + " \"access_token\": \"access-token-1234\",\n" |
| 163 | + + " \"token_type\": \"bearer\",\n" |
| 164 | + + " \"expires_in\": \"3600\"\n" |
| 165 | + + "}\n"; |
| 166 | + // @formatter:on |
| 167 | + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); |
| 168 | + |
| 169 | + // @formatter:off |
| 170 | + ClientRegistration clientRegistration = this.clientRegistration |
| 171 | + .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) |
| 172 | + .build(); |
| 173 | + // @formatter:on |
| 174 | + |
| 175 | + // Configure Jwt client authentication converter |
| 176 | + JWK jwk = TestJwks.DEFAULT_RSA_JWK; |
| 177 | + Function<ClientRegistration, JWK> jwkResolver = (registration) -> jwk; |
| 178 | + configureJwtClientAuthenticationConverter(jwkResolver); |
| 179 | + |
| 180 | + this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest(clientRegistration)).block(); |
| 181 | + RecordedRequest actualRequest = this.server.takeRequest(); |
| 182 | + assertThat(actualRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull(); |
| 183 | + assertThat(actualRequest.getBody().readUtf8()).contains("grant_type=authorization_code", |
| 184 | + "client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer", |
| 185 | + "client_assertion="); |
| 186 | + } |
| 187 | + |
| 188 | + private void configureJwtClientAuthenticationConverter(Function<ClientRegistration, JWK> jwkResolver) { |
| 189 | + NimbusJwtClientAuthenticationParametersConverter<OAuth2AuthorizationCodeGrantRequest> jwtClientAuthenticationConverter = new NimbusJwtClientAuthenticationParametersConverter<>( |
| 190 | + jwkResolver); |
| 191 | + this.tokenResponseClient.addParametersConverter(jwtClientAuthenticationConverter); |
| 192 | + } |
| 193 | + |
115 | 194 | // @Test
|
116 | 195 | // public void
|
117 | 196 | // getTokenResponseWhenRedirectUriMalformedThenThrowIllegalArgumentException() throws
|
@@ -261,7 +340,10 @@ public void getTokenResponseWhenSuccessResponseDoesNotIncludeScopeThenReturnAcce
|
261 | 340 | }
|
262 | 341 |
|
263 | 342 | private OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest() {
|
264 |
| - ClientRegistration registration = this.clientRegistration.build(); |
| 343 | + return authorizationCodeGrantRequest(this.clientRegistration.build()); |
| 344 | + } |
| 345 | + |
| 346 | + private OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest(ClientRegistration registration) { |
265 | 347 | OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
266 | 348 | .clientId(registration.getClientId()).state("state")
|
267 | 349 | .authorizationUri(registration.getProviderDetails().getAuthorizationUri())
|
@@ -414,6 +496,67 @@ public void convertWhenHeadersConverterSetThenCalled() throws Exception {
|
414 | 496 | .isEqualTo("Basic Y2xpZW50LWlkOmNsaWVudC1zZWNyZXQ=");
|
415 | 497 | }
|
416 | 498 |
|
| 499 | + @Test |
| 500 | + public void setParametersConverterWhenNullThenThrowIllegalArgumentException() { |
| 501 | + assertThatIllegalArgumentException().isThrownBy(() -> this.tokenResponseClient.setParametersConverter(null)) |
| 502 | + .withMessage("parametersConverter cannot be null"); |
| 503 | + } |
| 504 | + |
| 505 | + @Test |
| 506 | + public void addParametersConverterWhenNullThenThrowIllegalArgumentException() { |
| 507 | + assertThatIllegalArgumentException().isThrownBy(() -> this.tokenResponseClient.addParametersConverter(null)) |
| 508 | + .withMessage("parametersConverter cannot be null"); |
| 509 | + } |
| 510 | + |
| 511 | + @Test |
| 512 | + public void convertWhenParametersConverterAddedThenCalled() throws Exception { |
| 513 | + OAuth2AuthorizationCodeGrantRequest request = authorizationCodeGrantRequest(); |
| 514 | + Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> addedParametersConverter = mock( |
| 515 | + Converter.class); |
| 516 | + MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(); |
| 517 | + parameters.add("custom-parameter-name", "custom-parameter-value"); |
| 518 | + given(addedParametersConverter.convert(request)).willReturn(parameters); |
| 519 | + this.tokenResponseClient.addParametersConverter(addedParametersConverter); |
| 520 | + // @formatter:off |
| 521 | + String accessTokenSuccessResponse = "{\n" |
| 522 | + + " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" |
| 523 | + + " \"token_type\":\"bearer\",\n" |
| 524 | + + " \"expires_in\":3600,\n" |
| 525 | + + " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\"\n" |
| 526 | + + "}"; |
| 527 | + // @formatter:on |
| 528 | + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); |
| 529 | + this.tokenResponseClient.getTokenResponse(request).block(); |
| 530 | + verify(addedParametersConverter).convert(request); |
| 531 | + RecordedRequest actualRequest = this.server.takeRequest(); |
| 532 | + assertThat(actualRequest.getBody().readUtf8()).contains("grant_type=authorization_code", |
| 533 | + "custom-parameter-name=custom-parameter-value"); |
| 534 | + } |
| 535 | + |
| 536 | + @Test |
| 537 | + public void convertWhenParametersConverterSetThenCalled() throws Exception { |
| 538 | + OAuth2AuthorizationCodeGrantRequest request = authorizationCodeGrantRequest(); |
| 539 | + Converter<OAuth2AuthorizationCodeGrantRequest, MultiValueMap<String, String>> parametersConverter = mock( |
| 540 | + Converter.class); |
| 541 | + MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(); |
| 542 | + parameters.add("custom-parameter-name", "custom-parameter-value"); |
| 543 | + given(parametersConverter.convert(request)).willReturn(parameters); |
| 544 | + this.tokenResponseClient.setParametersConverter(parametersConverter); |
| 545 | + // @formatter:off |
| 546 | + String accessTokenSuccessResponse = "{\n" |
| 547 | + + " \"access_token\":\"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3\",\n" |
| 548 | + + " \"token_type\":\"bearer\",\n" |
| 549 | + + " \"expires_in\":3600,\n" |
| 550 | + + " \"refresh_token\":\"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk\"\n" |
| 551 | + + "}"; |
| 552 | + // @formatter:on |
| 553 | + this.server.enqueue(jsonResponse(accessTokenSuccessResponse)); |
| 554 | + this.tokenResponseClient.getTokenResponse(request).block(); |
| 555 | + verify(parametersConverter).convert(request); |
| 556 | + RecordedRequest actualRequest = this.server.takeRequest(); |
| 557 | + assertThat(actualRequest.getBody().readUtf8()).contains("custom-parameter-name=custom-parameter-value"); |
| 558 | + } |
| 559 | + |
417 | 560 | // gh-10260
|
418 | 561 | @Test
|
419 | 562 | public void getTokenResponseWhenSuccessCustomResponseThenReturnAccessTokenResponse() {
|
|
0 commit comments