|
@@ -1,5 +1,5 @@
|
|
|
/*
|
|
|
- * Copyright 2002-2020 the original author or authors.
|
|
|
+ * Copyright 2002-2021 the original author or authors.
|
|
|
*
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
* you may not use this file except in compliance with the License.
|
|
@@ -16,8 +16,13 @@
|
|
|
|
|
|
package org.springframework.security.oauth2.client.endpoint;
|
|
|
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
import java.time.Instant;
|
|
|
+import java.util.function.Function;
|
|
|
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
+
|
|
|
+import com.nimbusds.jose.jwk.JWK;
|
|
|
import okhttp3.mockwebserver.MockResponse;
|
|
|
import okhttp3.mockwebserver.MockWebServer;
|
|
|
import okhttp3.mockwebserver.RecordedRequest;
|
|
@@ -29,7 +34,7 @@ import org.springframework.http.HttpHeaders;
|
|
|
import org.springframework.http.HttpMethod;
|
|
|
import org.springframework.http.MediaType;
|
|
|
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
|
|
-import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
|
|
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
|
|
|
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
|
|
import org.springframework.security.oauth2.core.OAuth2AccessToken;
|
|
|
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
|
|
@@ -37,6 +42,8 @@ import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenRespon
|
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange;
|
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
|
|
|
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
|
|
|
+import org.springframework.security.oauth2.jose.TestJwks;
|
|
|
+import org.springframework.security.oauth2.jose.TestKeys;
|
|
|
|
|
|
import static org.assertj.core.api.Assertions.assertThat;
|
|
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
|
@@ -49,32 +56,25 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
|
|
*/
|
|
|
public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
|
|
|
- private DefaultAuthorizationCodeTokenResponseClient tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
|
|
|
+ private DefaultAuthorizationCodeTokenResponseClient tokenResponseClient;
|
|
|
|
|
|
- private ClientRegistration clientRegistration;
|
|
|
+ private ClientRegistration.Builder clientRegistration;
|
|
|
|
|
|
private MockWebServer server;
|
|
|
|
|
|
@Before
|
|
|
public void setup() throws Exception {
|
|
|
+ this.tokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
|
|
|
this.server = new MockWebServer();
|
|
|
this.server.start();
|
|
|
String tokenUri = this.server.url("/oauth2/token").toString();
|
|
|
// @formatter:off
|
|
|
- this.clientRegistration = ClientRegistration
|
|
|
- .withRegistrationId("registration-1")
|
|
|
+ this.clientRegistration = TestClientRegistrations.clientRegistration()
|
|
|
.clientId("client-1")
|
|
|
.clientSecret("secret")
|
|
|
- .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
|
|
- .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
|
|
.redirectUri("https://client.com/callback/client-1")
|
|
|
- .scope("read", "write")
|
|
|
- .authorizationUri("https://provider.com/oauth2/authorize")
|
|
|
.tokenUri(tokenUri)
|
|
|
- .userInfoUri("https://provider.com/user")
|
|
|
- .userNameAttributeName("id")
|
|
|
- .clientName("client-1")
|
|
|
- .build();
|
|
|
+ .scope("read", "write");
|
|
|
// @formatter:on
|
|
|
}
|
|
|
|
|
@@ -114,7 +114,7 @@ public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
|
|
Instant expiresAtBefore = Instant.now().plusSeconds(3600);
|
|
|
OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient
|
|
|
- .getTokenResponse(this.authorizationCodeGrantRequest());
|
|
|
+ .getTokenResponse(authorizationCodeGrantRequest(this.clientRegistration.build()));
|
|
|
Instant expiresAtAfter = Instant.now().plusSeconds(3600);
|
|
|
RecordedRequest recordedRequest = this.server.takeRequest();
|
|
|
assertThat(recordedRequest.getMethod()).isEqualTo(HttpMethod.POST.toString());
|
|
@@ -136,7 +136,7 @@ public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- public void getTokenResponseWhenClientAuthenticationBasicThenAuthorizationHeaderIsSent() throws Exception {
|
|
|
+ public void getTokenResponseWhenAuthenticationClientSecretBasicThenAuthorizationHeaderIsSent() throws Exception {
|
|
|
// @formatter:off
|
|
|
String accessTokenSuccessResponse = "{\n"
|
|
|
+ " \"access_token\": \"access-token-1234\",\n"
|
|
@@ -145,13 +145,13 @@ public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
+ "}\n";
|
|
|
// @formatter:on
|
|
|
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
|
|
- this.tokenResponseClient.getTokenResponse(this.authorizationCodeGrantRequest());
|
|
|
+ this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest(this.clientRegistration.build()));
|
|
|
RecordedRequest recordedRequest = this.server.takeRequest();
|
|
|
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).startsWith("Basic ");
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
- public void getTokenResponseWhenClientAuthenticationPostThenFormParametersAreSent() throws Exception {
|
|
|
+ public void getTokenResponseWhenAuthenticationClientSecretPostThenFormParametersAreSent() throws Exception {
|
|
|
// @formatter:off
|
|
|
String accessTokenSuccessResponse = "{\n"
|
|
|
+ " \"access_token\": \"access-token-1234\",\n"
|
|
@@ -160,9 +160,9 @@ public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
+ "}\n";
|
|
|
// @formatter:on
|
|
|
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
|
|
- ClientRegistration clientRegistration = this.from(this.clientRegistration)
|
|
|
+ ClientRegistration clientRegistration = this.clientRegistration
|
|
|
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST).build();
|
|
|
- this.tokenResponseClient.getTokenResponse(this.authorizationCodeGrantRequest(clientRegistration));
|
|
|
+ this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest(clientRegistration));
|
|
|
RecordedRequest recordedRequest = this.server.takeRequest();
|
|
|
assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull();
|
|
|
String formParameters = recordedRequest.getBody().readUtf8();
|
|
@@ -170,6 +170,79 @@ public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
assertThat(formParameters).contains("client_secret=secret");
|
|
|
}
|
|
|
|
|
|
+ @Test
|
|
|
+ public void getTokenResponseWhenAuthenticationClientSecretJwtThenFormParametersAreSent() throws Exception {
|
|
|
+ // @formatter:off
|
|
|
+ String accessTokenSuccessResponse = "{\n"
|
|
|
+ + " \"access_token\": \"access-token-1234\",\n"
|
|
|
+ + " \"token_type\": \"bearer\",\n"
|
|
|
+ + " \"expires_in\": \"3600\"\n"
|
|
|
+ + "}\n";
|
|
|
+ // @formatter:on
|
|
|
+ this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
|
|
+
|
|
|
+ // @formatter:off
|
|
|
+ ClientRegistration clientRegistration = this.clientRegistration
|
|
|
+ .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
|
|
|
+ .clientSecret(TestKeys.DEFAULT_ENCODED_SECRET_KEY)
|
|
|
+ .build();
|
|
|
+ // @formatter:on
|
|
|
+
|
|
|
+ // Configure Jwt client authentication converter
|
|
|
+ SecretKeySpec secretKey = new SecretKeySpec(
|
|
|
+ clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8), "HmacSHA256");
|
|
|
+ JWK jwk = TestJwks.jwk(secretKey).build();
|
|
|
+ Function<ClientRegistration, JWK> jwkResolver = (registration) -> jwk;
|
|
|
+ configureJwtClientAuthenticationConverter(jwkResolver);
|
|
|
+
|
|
|
+ this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest(clientRegistration));
|
|
|
+ RecordedRequest recordedRequest = this.server.takeRequest();
|
|
|
+ assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull();
|
|
|
+ String formParameters = recordedRequest.getBody().readUtf8();
|
|
|
+ assertThat(formParameters)
|
|
|
+ .contains("client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer");
|
|
|
+ assertThat(formParameters).contains("client_assertion=");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void getTokenResponseWhenAuthenticationPrivateKeyJwtThenFormParametersAreSent() throws Exception {
|
|
|
+ // @formatter:off
|
|
|
+ String accessTokenSuccessResponse = "{\n"
|
|
|
+ + " \"access_token\": \"access-token-1234\",\n"
|
|
|
+ + " \"token_type\": \"bearer\",\n"
|
|
|
+ + " \"expires_in\": \"3600\"\n"
|
|
|
+ + "}\n";
|
|
|
+ // @formatter:on
|
|
|
+ this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
|
|
+
|
|
|
+ // @formatter:off
|
|
|
+ ClientRegistration clientRegistration = this.clientRegistration
|
|
|
+ .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
|
|
|
+ .build();
|
|
|
+ // @formatter:on
|
|
|
+
|
|
|
+ // Configure Jwt client authentication converter
|
|
|
+ JWK jwk = TestJwks.DEFAULT_RSA_JWK;
|
|
|
+ Function<ClientRegistration, JWK> jwkResolver = (registration) -> jwk;
|
|
|
+ configureJwtClientAuthenticationConverter(jwkResolver);
|
|
|
+
|
|
|
+ this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest(clientRegistration));
|
|
|
+ RecordedRequest recordedRequest = this.server.takeRequest();
|
|
|
+ assertThat(recordedRequest.getHeader(HttpHeaders.AUTHORIZATION)).isNull();
|
|
|
+ String formParameters = recordedRequest.getBody().readUtf8();
|
|
|
+ assertThat(formParameters)
|
|
|
+ .contains("client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer");
|
|
|
+ assertThat(formParameters).contains("client_assertion=");
|
|
|
+ }
|
|
|
+
|
|
|
+ private void configureJwtClientAuthenticationConverter(Function<ClientRegistration, JWK> jwkResolver) {
|
|
|
+ NimbusJwtClientAuthenticationParametersConverter<OAuth2AuthorizationCodeGrantRequest> jwtClientAuthenticationConverter = new NimbusJwtClientAuthenticationParametersConverter<>(
|
|
|
+ jwkResolver);
|
|
|
+ OAuth2AuthorizationCodeGrantRequestEntityConverter requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
|
|
|
+ requestEntityConverter.addParametersConverter(jwtClientAuthenticationConverter);
|
|
|
+ this.tokenResponseClient.setRequestEntityConverter(requestEntityConverter);
|
|
|
+ }
|
|
|
+
|
|
|
@Test
|
|
|
public void getTokenResponseWhenSuccessResponseAndNotBearerTokenTypeThenThrowOAuth2AuthorizationException() {
|
|
|
// @formatter:off
|
|
@@ -181,7 +254,8 @@ public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
// @formatter:on
|
|
|
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
|
|
assertThatExceptionOfType(OAuth2AuthorizationException.class)
|
|
|
- .isThrownBy(() -> this.tokenResponseClient.getTokenResponse(this.authorizationCodeGrantRequest()))
|
|
|
+ .isThrownBy(() -> this.tokenResponseClient
|
|
|
+ .getTokenResponse(authorizationCodeGrantRequest(this.clientRegistration.build())))
|
|
|
.withMessageContaining(
|
|
|
"[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response")
|
|
|
.withMessageContaining("tokenType cannot be null");
|
|
@@ -196,7 +270,8 @@ public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
// @formatter:on
|
|
|
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
|
|
assertThatExceptionOfType(OAuth2AuthorizationException.class)
|
|
|
- .isThrownBy(() -> this.tokenResponseClient.getTokenResponse(this.authorizationCodeGrantRequest()))
|
|
|
+ .isThrownBy(() -> this.tokenResponseClient
|
|
|
+ .getTokenResponse(authorizationCodeGrantRequest(this.clientRegistration.build())))
|
|
|
.withMessageContaining(
|
|
|
"[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response")
|
|
|
.withMessageContaining("tokenType cannot be null");
|
|
@@ -215,7 +290,7 @@ public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
// @formatter:on
|
|
|
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
|
|
OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient
|
|
|
- .getTokenResponse(this.authorizationCodeGrantRequest());
|
|
|
+ .getTokenResponse(authorizationCodeGrantRequest(this.clientRegistration.build()));
|
|
|
assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read");
|
|
|
}
|
|
|
|
|
@@ -231,16 +306,16 @@ public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
// @formatter:on
|
|
|
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
|
|
OAuth2AccessTokenResponse accessTokenResponse = this.tokenResponseClient
|
|
|
- .getTokenResponse(this.authorizationCodeGrantRequest());
|
|
|
+ .getTokenResponse(authorizationCodeGrantRequest(this.clientRegistration.build()));
|
|
|
assertThat(accessTokenResponse.getAccessToken().getScopes()).containsExactly("read", "write");
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
public void getTokenResponseWhenTokenUriInvalidThenThrowOAuth2AuthorizationException() {
|
|
|
String invalidTokenUri = "https://invalid-provider.com/oauth2/token";
|
|
|
- ClientRegistration clientRegistration = this.from(this.clientRegistration).tokenUri(invalidTokenUri).build();
|
|
|
+ ClientRegistration clientRegistration = this.clientRegistration.tokenUri(invalidTokenUri).build();
|
|
|
assertThatExceptionOfType(OAuth2AuthorizationException.class).isThrownBy(
|
|
|
- () -> this.tokenResponseClient.getTokenResponse(this.authorizationCodeGrantRequest(clientRegistration)))
|
|
|
+ () -> this.tokenResponseClient.getTokenResponse(authorizationCodeGrantRequest(clientRegistration)))
|
|
|
.withMessageContaining(
|
|
|
"[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response");
|
|
|
}
|
|
@@ -260,7 +335,8 @@ public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
// @formatter:on
|
|
|
this.server.enqueue(jsonResponse(accessTokenSuccessResponse));
|
|
|
assertThatExceptionOfType(OAuth2AuthorizationException.class)
|
|
|
- .isThrownBy(() -> this.tokenResponseClient.getTokenResponse(this.authorizationCodeGrantRequest()))
|
|
|
+ .isThrownBy(() -> this.tokenResponseClient
|
|
|
+ .getTokenResponse(authorizationCodeGrantRequest(this.clientRegistration.build())))
|
|
|
.withMessageContaining(
|
|
|
"[invalid_token_response] An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response");
|
|
|
}
|
|
@@ -270,7 +346,8 @@ public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
String accessTokenErrorResponse = "{\n" + " \"error\": \"unauthorized_client\"\n" + "}\n";
|
|
|
this.server.enqueue(jsonResponse(accessTokenErrorResponse).setResponseCode(400));
|
|
|
assertThatExceptionOfType(OAuth2AuthorizationException.class)
|
|
|
- .isThrownBy(() -> this.tokenResponseClient.getTokenResponse(this.authorizationCodeGrantRequest()))
|
|
|
+ .isThrownBy(() -> this.tokenResponseClient
|
|
|
+ .getTokenResponse(authorizationCodeGrantRequest(this.clientRegistration.build())))
|
|
|
.withMessageContaining("[unauthorized_client]");
|
|
|
}
|
|
|
|
|
@@ -278,15 +355,12 @@ public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
public void getTokenResponseWhenServerErrorResponseThenThrowOAuth2AuthorizationException() {
|
|
|
this.server.enqueue(new MockResponse().setResponseCode(500));
|
|
|
assertThatExceptionOfType(OAuth2AuthorizationException.class)
|
|
|
- .isThrownBy(() -> this.tokenResponseClient.getTokenResponse(this.authorizationCodeGrantRequest()))
|
|
|
+ .isThrownBy(() -> this.tokenResponseClient
|
|
|
+ .getTokenResponse(authorizationCodeGrantRequest(this.clientRegistration.build())))
|
|
|
.withMessageContaining("[invalid_token_response] An error occurred while attempting to retrieve "
|
|
|
+ "the OAuth 2.0 Access Token Response");
|
|
|
}
|
|
|
|
|
|
- private OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest() {
|
|
|
- return this.authorizationCodeGrantRequest(this.clientRegistration);
|
|
|
- }
|
|
|
-
|
|
|
private OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest(ClientRegistration clientRegistration) {
|
|
|
OAuth2AuthorizationRequest authorizationRequest = OAuth2AuthorizationRequest.authorizationCode()
|
|
|
.clientId(clientRegistration.getClientId()).state("state-1234")
|
|
@@ -303,22 +377,4 @@ public class DefaultAuthorizationCodeTokenResponseClientTests {
|
|
|
return new MockResponse().setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).setBody(json);
|
|
|
}
|
|
|
|
|
|
- private ClientRegistration.Builder from(ClientRegistration registration) {
|
|
|
- // @formatter:off
|
|
|
- return ClientRegistration.withRegistrationId(registration.getRegistrationId())
|
|
|
- .clientId(registration.getClientId())
|
|
|
- .clientSecret(registration.getClientSecret())
|
|
|
- .clientAuthenticationMethod(registration.getClientAuthenticationMethod())
|
|
|
- .authorizationGrantType(registration.getAuthorizationGrantType())
|
|
|
- .redirectUri(registration.getRedirectUri())
|
|
|
- .scope(registration.getScopes())
|
|
|
- .authorizationUri(registration.getProviderDetails().getAuthorizationUri())
|
|
|
- .tokenUri(registration.getProviderDetails().getTokenUri())
|
|
|
- .userInfoUri(registration.getProviderDetails().getUserInfoEndpoint().getUri())
|
|
|
- .userNameAttributeName(
|
|
|
- registration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName())
|
|
|
- .clientName(registration.getClientName());
|
|
|
- // @formatter:on
|
|
|
- }
|
|
|
-
|
|
|
}
|