From 3acf3c5d5dbc5e917457683e493ef01e7bc6c9f0 Mon Sep 17 00:00:00 2001
From: Markus Strehle <11627201+strehle@users.noreply.github.com>
Date: Thu, 19 Oct 2023 06:41:00 +0200
Subject: [PATCH] test: Add Client Authentication Integration Tests (#2508)
Run different grant types with private_key_jwt authentication instead of client_secret
Works after merge of https://github.com/cloudfoundry/uaa/pull/2507
---
.../resources/integration.test.properties | 3 +-
.../webapp/WEB-INF/spring/oauth-clients.xml | 23 ++
.../feature/PrivateKeyJwtClientAuthIT.java | 362 ++++++++++++++++++
.../uaa/integration/feature/TestClient.java | 20 +
.../util/IntegrationTestUtils.java | 48 ++-
5 files changed, 453 insertions(+), 3 deletions(-)
create mode 100644 uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/PrivateKeyJwtClientAuthIT.java
diff --git a/server/src/test/resources/integration.test.properties b/server/src/test/resources/integration.test.properties
index 1fd54dd5cfd..560a134146d 100644
--- a/server/src/test/resources/integration.test.properties
+++ b/server/src/test/resources/integration.test.properties
@@ -1,4 +1,5 @@
integration.test.app_url=http://localhost:8080/app/
integration.test.base_url=http://localhost:8080/uaa
integration.test.timeout_multiplier=1
-smtp.port=2525
\ No newline at end of file
+smtp.port=2525
+integration.test.signing-key=-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCowKUlfOfJxXZtDWkVs3xb4BZJiGlLYxUAaGRY2WbG/YjHT/6frOOK+N2jFyrtElHiRXJyhV4PTsOJYSVhKdAt15A+AoBwGLCKVHfRTLINMpyoNBDmuQKDY42XBXRoyyDvgppd5exXrncBKzcVgS25LVoP8Nvn4XJcXweQejzHLX01SeqwNZCeHUeGSXKfG7a29bR/DagMTWnA2X5YsRU+2VykK1/hVK/4ZrC0GIjrGZiwYEwL3Db0RIcWo/DQ1IJGGXIl/qsME0f/vrqbMr+8TMDivMZMERSoPFOD/wmlGGH0PeqWNKyaoK2lCiWg4BpQoKpIlv+Eo+yl77uv1xibAgMBAAECggEADtC4jwJ/MDuZ6pGtvKSBEgKp7wzyJZa00ZzYo0sVSi1L58FSDiW15Zqn84YSR2iY1l//eY0HVYCDC6aDC07W9cQoaArjLzQ6GslQqm6GOtqX+CJ3q2Uc+RKkuL7XWgEfZDexb4+PwNQfb/OIOgCZCY1kP0sHm3BNEIDQheXD1gtq8KTOBy/TtN7rV940LoudgQ8vzz+ShhmG7Dt5yws/QzaBpryLncGsGYZSDnvvEBYYdlbYQEgfLmzdKSDKW3DNV+duaBDeArxZViD7EqPpQxIOawBvl5bs6Radz7OEGZ7khTr2fYU4JGn4WJl61yJg4Xm6pp9cJmi+BIgwLrvXNQKBgQDTPjZ6jCPXAfY6WRVD+hTKgrdhMzNALuiZeyWNSKiiDKgtx1A8OhUAgGzY/PeqmDabiVWKtso+EazfMwa+BrScf+HEZFUvNJ5tGxe6nEEFCx0n5ELUM/L4SWCtD6eFCNYcAY1XvzPD1F3KzewEvw8FbT34fef+YayuIF3PPODtJwKBgQDMgcEnXARRzsIC7i1ggqSU8N0OUSu+rA/hMd9Uh9HsY18p8JtNezAQ2vV4RL3R/CUPGXeDvCBYWhXlkNmjdCAsk24DZHs2Q18xTeHZ15PUtmd2tH/tAxANDi6RTTjpQI3w2poXHl2ZVuT8M+XkTv0WzI0c8TNog2RASzHd5z5JbQKBgQCDlvim5E+bKzywYjfuDYYQFNeZNCTT8aSxn1XoKf/qWooVYlin++KDWnzzurmpSoKR5z4jV/SqL6aJr6aej1zJNJx2E65A5r1d6AejFp0mQCMca4P53paXdlZD2EGZjMSb05extojPj6YRpK9G0aHQ1plJB12SSFQicEUfyKOw9wKBgD09ScLoih6ZRH2uJwZ0eKZlLj0AT5IsYiD0V0Uv2svnwfKEK21bSzxw5Prb0t/TmqFX5fMb3a+3YkE5TALnXk8a4uG/MCpCqHnSMaSTKqCS8o6YZIpr1V2jdoxqTHWEsDyEqYnsvOiTHcTsIZZplN5D6KnXDKbqWZXrLoadnYhNAoGACESLgCy552WcM7vNt4Fw7lR/O3gEnwD41gIx5EGa/UoA08Q+i7sBt9PkL4oQrJ/MYCcVNnmg9KrJdlqF4AlEHYeZSkMuQDYHcaO9xtYP3QdhD+nLXbNrCxaSSaSX8tS4BjdcSH1yMyLFg5OqiJYgwYFiptyKFm5QqFhFTY+20aE=\n-----END PRIVATE KEY-----
\ No newline at end of file
diff --git a/uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml b/uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml
index b0f3b6efe8b..4dbce00184c 100644
--- a/uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml
+++ b/uaa/src/main/webapp/WEB-INF/spring/oauth-clients.xml
@@ -230,6 +230,29 @@
+
+
+
+
+
+
diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/PrivateKeyJwtClientAuthIT.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/PrivateKeyJwtClientAuthIT.java
new file mode 100644
index 00000000000..3b6088ce9bc
--- /dev/null
+++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/integration/feature/PrivateKeyJwtClientAuthIT.java
@@ -0,0 +1,362 @@
+package org.cloudfoundry.identity.uaa.integration.feature;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.cloudfoundry.identity.uaa.ServerRunning;
+import org.cloudfoundry.identity.uaa.constants.OriginKeys;
+import org.cloudfoundry.identity.uaa.integration.util.IntegrationTestUtils;
+import org.cloudfoundry.identity.uaa.oauth.jwt.JwtClientAuthentication;
+import org.cloudfoundry.identity.uaa.oauth.jwt.JwtHelper;
+import org.cloudfoundry.identity.uaa.provider.IdentityProvider;
+import org.cloudfoundry.identity.uaa.provider.OIDCIdentityProviderDefinition;
+import org.cloudfoundry.identity.uaa.util.JsonUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.openqa.selenium.WebDriver;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.jwt.Jwt;
+import org.springframework.security.oauth2.client.test.TestAccounts;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestOperations;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
+import static org.cloudfoundry.identity.uaa.provider.ExternalIdentityProviderDefinition.USER_NAME_ATTRIBUTE_NAME;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.springframework.http.HttpStatus.OK;
+import static org.springframework.http.HttpStatus.UNAUTHORIZED;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes = DefaultIntegrationTestConfig.class)
+public class PrivateKeyJwtClientAuthIT {
+
+ //jwt.token.signing-key from uaa.yml
+ @Value("${integration.test.signing-key}")
+ private String jwtTokenSigningKey;
+
+ @Autowired
+ @Rule
+ public IntegrationTestRule integrationTestRule;
+
+ @Autowired
+ WebDriver webDriver;
+
+ @Value("${integration.test.base_url}")
+ String baseUrl;
+
+ @Autowired
+ TestAccounts testAccounts;
+
+ @Autowired
+ TestClient testClient;
+
+ @Autowired
+ RestOperations restOperations;
+
+ @Test
+ public void testPasswordGrantWithClientUsingPrivateKeyJwtAndExpectValidToken() {
+ // Given, client with jwks trusted keys
+ String usedClientId = "client_with_jwks_trust";
+ Map expectedClaims = Map.of(
+ "client_auth_method", "private_key_jwt",
+ "client_id", usedClientId,
+ "origin", "uaa",
+ "zid", "uaa",
+ "user_name", testAccounts.getUserName(),
+ "grant_type", "password"
+ );
+ // When
+ String accessToken = getPasswordGrantToken(usedClientId, "access_token", OK);
+ // Then
+ assertNotNull(accessToken);
+ assertThat(getTokenClaims(accessToken)).containsAllEntriesOf(expectedClaims);
+ }
+
+ @Test
+ public void testClientCredentialGrantWithClientUsingPrivateKeyJwtAndExpectValidToken() {
+ // Given, client with jwks trusted keys
+ String usedClientId = "client_with_jwks_trust";
+ Map expectedClaims = Map.of(
+ "client_auth_method", "private_key_jwt",
+ "client_id", usedClientId,
+ "zid", "uaa",
+ "grant_type", "client_credentials"
+ );
+ // When
+ String accessToken = getClientCredentialsGrantToken(usedClientId, "access_token", OK);
+ // Then
+ assertNotNull(accessToken);
+ assertThat(getTokenClaims(accessToken)).containsAllEntriesOf(expectedClaims);
+ assertThat(getTokenClaims(accessToken)).doesNotContainKeys("user_name", "origin");
+ }
+
+ @Test
+ public void testClientCredentialGrantWithClientUsingPrivateKeyJwtUriAndExpectValidToken() {
+ // Given, client with jwks_uri keys
+ String usedClientId = "client_with_allowpublic_and_jwks_uri_trust";
+ Map expectedClaims = Map.of(
+ "client_auth_method", "private_key_jwt",
+ "client_id", usedClientId,
+ "zid", "uaa",
+ "grant_type", "client_credentials"
+ );
+ // When
+ String accessToken = getClientCredentialsGrantToken(usedClientId, "access_token", OK);
+ // Then
+ assertNotNull(accessToken);
+ assertThat(getTokenClaims(accessToken)).containsAllEntriesOf(expectedClaims);
+ assertThat(getTokenClaims(accessToken)).doesNotContainKeys("user_name", "origin");
+ }
+
+ @Test
+ public void testRefreshAfterPasswordWithClientUsingPrivateKeyJwtUriAndExpectValidToken() {
+ // Given, client with jwks_uri keys
+ String usedClientId = "client_with_allowpublic_and_jwks_uri_trust";
+ Map expectedClaims = Map.of(
+ "client_auth_method", "private_key_jwt",
+ "client_id", usedClientId,
+ "origin", "uaa",
+ "zid", "uaa",
+ "user_name", testAccounts.getUserName(),
+ "grant_type", "password"
+ );
+ // When
+ String refreshToken = getPasswordGrantToken(usedClientId, "refresh_token", OK);
+ // Then
+ assertNotNull(refreshToken);
+ // When
+ String accessToken = getRefreshGrantToken(usedClientId, refreshToken, "access_token", OK);
+ // Then
+ assertNotNull(accessToken);
+ assertThat(getTokenClaims(accessToken)).containsAllEntriesOf(expectedClaims);
+ }
+
+ @Test
+ public void testJwtBearerAfterPasswordWithClientUsingPrivateKeyJwtUriAndExpectValidToken() {
+ // Given, client with jwks_uri keys
+ String usedClientId = "client_with_allowpublic_and_jwks_uri_trust";
+ Map expectedClaims = Map.of(
+ "client_auth_method", "private_key_jwt",
+ "client_id", usedClientId,
+ "origin", "uaa",
+ "zid", "uaa",
+ "user_name", testAccounts.getUserName(),
+ "grant_type", "password"
+ );
+ // When
+ String passwordToken = getPasswordGrantToken(usedClientId, "access_token", OK);
+ // Then
+ assertNotNull(passwordToken);
+ assertThat(getTokenClaims(passwordToken)).containsAllEntriesOf(expectedClaims);
+ // When
+ String accessToken = getJwtBearerGrantToken(usedClientId, passwordToken, "access_token", OK);
+ // Then
+ assertNotNull(accessToken);
+ assertThat(getTokenClaims(accessToken)).containsAllEntriesOf(Map.of(
+ "client_auth_method", "private_key_jwt",
+ "client_id", usedClientId,
+ "origin", "uaa",
+ "zid", "uaa",
+ "user_name", testAccounts.getUserName(),
+ "grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer"
+ ));
+ }
+
+ @Test
+ public void testPasswordGrantWithClientUsingOidcProxyAndExpectValidToken() throws Exception {
+ // Given
+ String usedClientId = "client_with_allowpublic_and_jwks_uri_trust";
+ String expectedOriginKey = "oidc-proxy";
+ Map expectedClaims = Map.of(
+ "client_auth_method", "private_key_jwt",
+ "client_id", usedClientId,
+ "origin", expectedOriginKey,
+ "zid", "uaa",
+ "user_name", testAccounts.getUserName(),
+ "grant_type", "password"
+ );
+ String clientCredentialsToken = IntegrationTestUtils.getClientCredentialsToken(baseUrl, "admin", "adminsecret");
+ try {
+ // create OIDC IdP using private_key_jwt with jwks trust
+ IdentityProvider oidcProxy = createOidcProviderTemplate("client_with_jwks_trust", expectedOriginKey);
+ IntegrationTestUtils.createOrUpdateProvider(clientCredentialsToken, baseUrl, oidcProxy);
+ // When Password Grant with OIDC proxy in between
+ String accessToken = getPasswordProxyGrantToken(usedClientId, "access_token", expectedOriginKey, OK);
+ // Then
+ assertNotNull(accessToken);
+ assertThat(getTokenClaims(accessToken)).containsAllEntriesOf(expectedClaims);
+ } finally {
+ IntegrationTestUtils.deleteProvider(clientCredentialsToken, baseUrl, "uaa", expectedOriginKey);
+ }
+ }
+
+ @Test
+ public void testAutorizationCodeGrantWithClientUsingOidcProxyAndExpectValidToken() throws Exception {
+ // Given
+ String expectedOriginKey = "oidc-proxy-private-key-jwt";
+ String usedClientId = "client_with_allowpublic_and_jwks_uri_trust";
+ String clientCredentialsToken = IntegrationTestUtils.getClientCredentialsToken(baseUrl, "admin", "adminsecret");
+ try {
+ // create OIDC IdP using private_key_jwt with jwks trust
+ IdentityProvider oidcProxy = createOidcProviderTemplate("client_with_jwks_trust", expectedOriginKey);
+ IntegrationTestUtils.createOrUpdateProvider(clientCredentialsToken, baseUrl, oidcProxy);
+ ServerRunning serverRunning = ServerRunning.isRunning();
+ serverRunning.setHostName("localhost");
+ // login
+ String accessToken = IntegrationTestUtils.getAuthorizationCodeToken(
+ serverRunning,
+ usedClientId,
+ testClient.createClientJwt(usedClientId, jwtTokenSigningKey),
+ testAccounts.getUserName(),
+ testAccounts.getPassword(),
+ null,
+ baseUrl + "/login/callback/" + expectedOriginKey,
+ expectedOriginKey,
+ false);
+ assertThat(getTokenClaims(accessToken)).containsAllEntriesOf(Map.of(
+ "client_auth_method", "private_key_jwt",
+ "client_id", usedClientId,
+ "origin", "uaa",
+ "zid", "uaa",
+ "user_name", testAccounts.getUserName(),
+ "grant_type", "authorization_code"
+ ));
+ } finally {
+ IntegrationTestUtils.deleteProvider(clientCredentialsToken, baseUrl, "uaa", expectedOriginKey);
+ }
+ }
+
+ @Test
+ public void testPasswordGrantWithClientUsingPrivateKeyJwtAndExpectClientError() {
+ // When
+ String response = getPasswordGrantToken("admin", "access_token", UNAUTHORIZED);
+ // Then
+ assertNotNull(response);
+ assertThat(response).contains("401");
+ }
+
+ @Test
+ public void testClientCredentialGrantWithClientUsingPrivateKeyJwtAndExpectClientError() {
+ // When
+ String response = getClientCredentialsGrantToken("any-other-not-existing-client", "access_token", UNAUTHORIZED);
+ // Then
+ assertNotNull(response);
+ assertThat(response).contains("401");
+ }
+
+ private String getClientCredentialsGrantToken(String clientId, String returnToken, HttpStatus expected) {
+ return getToken(clientId, returnToken, expected, "client_credentials", null, null, null, null);
+ }
+
+ private String getPasswordGrantToken(String clientId, String returnToken, HttpStatus expected) {
+ return getToken(clientId, returnToken, expected, "password", testAccounts.getUserName(), testAccounts.getPassword(), null, null);
+ }
+
+ private String getPasswordProxyGrantToken(String clientId, String returnToken, String origin, HttpStatus expected) {
+ return getToken(clientId, returnToken, expected, "password", testAccounts.getUserName(), testAccounts.getPassword(), "login_hint", "{\"origin\":\""+origin+"\"}");
+ }
+
+ private String getRefreshGrantToken(String clientId, String refreshTokenValue, String returnToken, HttpStatus expected) {
+ return getToken(clientId, returnToken, expected, "refresh_token", null, null, "refresh_token", refreshTokenValue);
+ }
+
+ private String getJwtBearerGrantToken(String clientId, String bearerToken, String returnToken, HttpStatus expected) {
+ return getToken(clientId, returnToken, expected, "urn:ietf:params:oauth:grant-type:jwt-bearer", null, null, "assertion", bearerToken);
+ }
+
+ private String getToken(String clientId, String returnToken, HttpStatus expected, String grantType, String userName, String password, String extraKey, String extraValue) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+ headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
+
+ LinkedMultiValueMap postBody = new LinkedMultiValueMap<>();
+ postBody.add("grant_type", grantType);
+ if (userName != null) {
+ postBody.add("username", userName);
+ }
+ if (password != null) {
+ postBody.add("password", password);
+ }
+ if (extraKey != null && extraValue != null) {
+ postBody.add(extraKey, extraValue);
+ }
+ postBody.add("token_format", "jwt");
+ postBody.add(JwtClientAuthentication.CLIENT_ASSERTION, testClient.createClientJwt(clientId, jwtTokenSigningKey));
+ postBody.add(JwtClientAuthentication.CLIENT_ASSERTION_TYPE, JwtClientAuthentication.GRANT_TYPE);
+
+ ResponseEntity