diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/LinkAndExchangeServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/LinkAndExchangeServlet.java index c43ef97062..cb27e13589 100644 --- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/LinkAndExchangeServlet.java +++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/LinkAndExchangeServlet.java @@ -16,9 +16,16 @@ */ package org.keycloak.testsuite.adapter.servlet; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; import org.junit.Assert; import org.keycloak.KeycloakSecurityContext; import org.keycloak.OAuth2Constants; +import org.keycloak.adapters.HttpClientBuilder; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; @@ -31,14 +38,9 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.HttpHeaders; -import java.io.BufferedWriter; import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URL; import java.net.URLEncoder; import java.util.HashMap; import java.util.Map; @@ -69,22 +71,20 @@ public class LinkAndExchangeServlet extends HttpServlet { public AccessTokenResponse doTokenExchange(String realm, String token, String requestedIssuer, String clientId, String clientSecret) throws Exception { - try { + + try (CloseableHttpClient client = (CloseableHttpClient) new HttpClientBuilder().disableTrustManager().build()) { String exchangeUrl = KeycloakUriBuilder.fromUri(ServletTestUtils.getAuthServerUrlBase()) .path("/auth/realms/{realm}/protocol/openid-connect/token").build(realm).toString(); - URL url = new URL(exchangeUrl); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("POST"); - conn.setDoInput(true); - conn.setDoOutput(true); + HttpPost post = new HttpPost(exchangeUrl); HashMap parameters = new HashMap<>(); + if (clientSecret != null) { String authorization = BasicAuthHelper.createHeader(clientId, clientSecret); - conn.setRequestProperty(HttpHeaders.AUTHORIZATION, authorization); + post.setHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_FORM_URLENCODED.toString()); + post.setHeader(HttpHeaders.AUTHORIZATION, authorization); } else { parameters.put("client_id", clientId); - } parameters.put(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE); @@ -92,27 +92,15 @@ public class LinkAndExchangeServlet extends HttpServlet { parameters.put(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE); parameters.put(OAuth2Constants.REQUESTED_ISSUER, requestedIssuer); - OutputStream os = conn.getOutputStream(); - BufferedWriter writer = new BufferedWriter( - new OutputStreamWriter(os, "UTF-8")); - writer.write(getPostDataString(parameters)); - - writer.flush(); - writer.close(); - os.close(); - if (conn.getResponseCode() == 200) { - AccessTokenResponse tokenResponse = JsonSerialization.readValue(conn.getInputStream(), AccessTokenResponse.class); - conn.getInputStream().close(); - return tokenResponse; - } else if (conn.getResponseCode() == 400) { - AccessTokenResponse tokenResponse = JsonSerialization.readValue(conn.getErrorStream(), AccessTokenResponse.class); - conn.getErrorStream().close(); - return tokenResponse; + post.setEntity(new StringEntity(getPostDataString(parameters))); + HttpResponse response = client.execute(post); + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == 200 || statusCode == 400) { + return JsonSerialization.readValue(EntityUtils.toString(response.getEntity()), AccessTokenResponse.class); } else { throw new RuntimeException("Unknown error!"); } - } finally { } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/BrokerLinkAndTokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/BrokerLinkAndTokenExchangeTest.java index 3853d34863..e711dd41ba 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/BrokerLinkAndTokenExchangeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/BrokerLinkAndTokenExchangeTest.java @@ -22,9 +22,9 @@ import org.jboss.arquillian.container.test.api.TargetsContainer; import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.test.api.ArquillianResource; import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.resource.RealmResource; @@ -59,6 +59,7 @@ import org.keycloak.testsuite.ProfileAssume; import org.keycloak.testsuite.adapter.AbstractAdapterTest; import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest; import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; +import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected; import org.keycloak.testsuite.utils.arquillian.ContainerConstants; import org.keycloak.testsuite.broker.BrokerTestTools; import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl; @@ -84,6 +85,7 @@ import java.net.URL; import java.util.LinkedList; import java.util.List; +import static org.keycloak.testsuite.Assert.assertEquals; import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient; import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.AUTH_SERVER_CURRENT; @@ -187,7 +189,6 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest testRealms.add(realm); - realm = new RealmRepresentation(); realm.setRealm(PARENT_IDP); realm.setEnabled(true); @@ -200,17 +201,36 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest parentApp.setFullScopeAllowed(true); realm.setClients(new LinkedList<>()); realm.getClients().add(parentApp); - testRealms.add(realm); } - @BeforeClass - public static void enabled() { - ProfileAssume.assumeFeatureEnabled(Profile.Feature.TOKEN_EXCHANGE); + @Before + public void enableFeature() throws Exception { + try { + addIdpUser(); + addChildUser(); + createBroker(); + + checkFeature(Response.Status.NOT_IMPLEMENTED.getStatusCode()); + Response response = testingClient.testing().enableFeature(Profile.Feature.TOKEN_EXCHANGE.toString()); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + checkFeature(Response.Status.OK.getStatusCode()); + + ProfileAssume.assumeFeatureEnabled(Profile.Feature.TOKEN_EXCHANGE); + } catch (Exception e) { + disableFeature(); + throw e; + } + } + + @After + public void disableFeature() throws Exception { + Response response = testingClient.testing().disableFeature(Profile.Feature.TOKEN_EXCHANGE.toString()); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + checkFeature(Response.Status.NOT_IMPLEMENTED.getStatusCode()); } - @Before public void addIdpUser() { RealmResource realm = adminClient.realms().realm(PARENT_IDP); UserRepresentation user = new UserRepresentation(); @@ -226,8 +246,6 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest private String childUserId = null; - - @Before public void addChildUser() { RealmResource realm = adminClient.realms().realm(CHILD_IDP); UserRepresentation user = new UserRepresentation(); @@ -307,7 +325,7 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest idp.setStoreToken(true); realm.updateIdentityProvider(idp); } - @Before + public void createBroker() { createParentChild(); testingClient.server().run(BrokerLinkAndTokenExchangeTest::setupRealm); @@ -319,9 +337,9 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest @Test + @UncaughtServerErrorExpected public void testAccountLink() throws Exception { testingClient.server().run(BrokerLinkAndTokenExchangeTest::turnOnTokenStore); - RealmResource realm = adminClient.realms().realm(CHILD_IDP); List links = realm.users().get(childUserId).getFederatedIdentity(); Assert.assertTrue(links.isEmpty()); @@ -349,74 +367,76 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest Assert.assertFalse(links.isEmpty()); - // do exchange String accessToken = oauth.doGrantAccessTokenRequest(CHILD_IDP, "child", "password", null, ClientApp.DEPLOYMENT_NAME, "password").getAccessToken(); Client httpClient = ClientBuilder.newClient(); + try { + WebTarget exchangeUrl = childTokenExchangeWebTarget(httpClient); + System.out.println("Exchange url: " + exchangeUrl.getUri().toString()); - WebTarget exchangeUrl = childTokenExchangeWebTarget(httpClient); - System.out.println("Exchange url: " + exchangeUrl.getUri().toString()); + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE) + .param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP) - Response response = exchangeUrl.request() - .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) - .post(Entity.form( - new Form() - .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) - .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) - .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE) - .param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP) + )); + Assert.assertEquals(200, response.getStatus()); + AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class); + response.close(); + String externalToken = tokenResponse.getToken(); + Assert.assertNotNull(externalToken); + Assert.assertTrue(tokenResponse.getExpiresIn() > 0); + setTimeOffset((int) tokenResponse.getExpiresIn() + 1); - )); - Assert.assertEquals(200, response.getStatus()); - AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class); - response.close(); - String externalToken = tokenResponse.getToken(); - Assert.assertNotNull(externalToken); - Assert.assertTrue(tokenResponse.getExpiresIn() > 0); - setTimeOffset((int)tokenResponse.getExpiresIn() + 1); + // test that token refresh happens - // test that token refresh happens + // get access token again because we may have timed out + accessToken = oauth.doGrantAccessTokenRequest(CHILD_IDP, "child", "password", null, ClientApp.DEPLOYMENT_NAME, "password").getAccessToken(); + response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE) + .param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP) - // get access token again because we may have timed out - accessToken = oauth.doGrantAccessTokenRequest(CHILD_IDP, "child", "password", null, ClientApp.DEPLOYMENT_NAME, "password").getAccessToken(); - response = exchangeUrl.request() - .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) - .post(Entity.form( - new Form() - .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) - .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) - .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE) - .param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP) + )); + Assert.assertEquals(200, response.getStatus()); + tokenResponse = response.readEntity(AccessTokenResponse.class); + response.close(); + Assert.assertNotEquals(externalToken, tokenResponse.getToken()); - )); - Assert.assertEquals(200, response.getStatus()); - tokenResponse = response.readEntity(AccessTokenResponse.class); - response.close(); - Assert.assertNotEquals(externalToken, tokenResponse.getToken()); + // test direct exchange + response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-exchanger", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.REQUESTED_SUBJECT, "child") + .param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP) - // test direct exchange - response = exchangeUrl.request() - .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("direct-exchanger", "secret")) - .post(Entity.form( - new Form() - .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) - .param(OAuth2Constants.REQUESTED_SUBJECT, "child") - .param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP) - - )); - Assert.assertEquals(200, response.getStatus()); - tokenResponse = response.readEntity(AccessTokenResponse.class); - response.close(); - Assert.assertNotEquals(externalToken, tokenResponse.getToken()); + )); + Assert.assertEquals(200, response.getStatus()); + tokenResponse = response.readEntity(AccessTokenResponse.class); + response.close(); + Assert.assertNotEquals(externalToken, tokenResponse.getToken()); - logoutAll(); + logoutAll(); - realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP); - links = realm.users().get(childUserId).getFederatedIdentity(); - Assert.assertTrue(links.isEmpty()); + realm.users().get(childUserId).removeFederatedIdentity(PARENT_IDP); + links = realm.users().get(childUserId).getFederatedIdentity(); + Assert.assertTrue(links.isEmpty()); + } finally { + httpClient.close(); + } } protected WebTarget childTokenExchangeWebTarget(Client httpClient) { @@ -443,6 +463,7 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest } @Test + @UncaughtServerErrorExpected public void testAccountLinkNoTokenStore() throws Exception { testingClient.server().run(BrokerLinkAndTokenExchangeTest::turnOffTokenStore); @@ -472,7 +493,6 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest Assert.assertFalse(links.isEmpty()); - logoutAll(); @@ -488,6 +508,7 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest * @throws Exception */ @Test + @UncaughtServerErrorExpected public void testExportImport() throws Exception { testExternalExchange(); testingClient.testing().exportImport().setProvider(SingleFileExportProviderFactory.PROVIDER_ID); @@ -507,219 +528,190 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest testExternalExchange(); } - - @Test + @UncaughtServerErrorExpected public void testExternalExchange() throws Exception { - RealmResource childRealm = adminClient.realms().realm(CHILD_IDP); String accessToken = oauth.doGrantAccessTokenRequest(PARENT_IDP, PARENT2_USERNAME, "password", null, PARENT_CLIENT, "password").getAccessToken(); Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); Client httpClient = ClientBuilder.newClient(); - WebTarget exchangeUrl = childTokenExchangeWebTarget(httpClient); - System.out.println("Exchange url: " + exchangeUrl.getUri().toString()); + try { + WebTarget exchangeUrl = childTokenExchangeWebTarget(httpClient); + System.out.println("Exchange url: " + exchangeUrl.getUri().toString()); + + checkFeature(200); - { IdentityProviderRepresentation rep = adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).toRepresentation(); - rep.getConfig().put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, String.valueOf(false)); + rep.getConfig().put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, String.valueOf(true)); + rep.getConfig().put(OIDCIdentityProviderConfig.USE_JWKS_URL, String.valueOf(true)); + rep.getConfig().put(OIDCIdentityProviderConfig.JWKS_URL, parentJwksUrl()); + String parentIssuer = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT) + .path("/realms") + .path(PARENT_IDP) + .build().toString(); + rep.getConfig().put("issuer", parentIssuer); adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).update(rep); - // test user info validation. - Response response = exchangeUrl.request() - .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) - .post(Entity.form( - new Form() - .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) - .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) - .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE) - .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP) - )); - Assert.assertEquals(200, response.getStatus()); - AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class); - String exchangedAccessToken = tokenResponse.getToken(); - Assert.assertNotNull(exchangedAccessToken); - response.close(); + String exchangedUserId = null; + String exchangedUsername = null; - Assert.assertEquals(1, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); + { + // test signature validation + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE) + .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP) - // test logout - response = childLogoutWebTarget(httpClient) - .queryParam("id_token_hint", exchangedAccessToken) - .request() - .get(); - response.close(); + )); + Assert.assertEquals(200, response.getStatus()); + AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class); + String exchangedAccessToken = tokenResponse.getToken(); + JWSInput jws = new JWSInput(tokenResponse.getToken()); + AccessToken token = jws.readJsonContent(AccessToken.class); + response.close(); - Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); + exchangedUserId = token.getSubject(); + exchangedUsername = token.getPreferredUsername(); - } - IdentityProviderRepresentation rep = adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).toRepresentation(); - rep.getConfig().put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, String.valueOf(true)); - rep.getConfig().put(OIDCIdentityProviderConfig.USE_JWKS_URL, String.valueOf(true)); - rep.getConfig().put(OIDCIdentityProviderConfig.JWKS_URL, parentJwksUrl()); - String parentIssuer = UriBuilder.fromUri(OAuthClient.AUTH_SERVER_ROOT) - .path("/realms") - .path(PARENT_IDP) - .build().toString(); - rep.getConfig().put("issuer", parentIssuer); - adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).update(rep); - - String exchangedUserId = null; - String exchangedUsername = null; - - { - // test signature validation - Response response = exchangeUrl.request() - .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) - .post(Entity.form( - new Form() - .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) - .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) - .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE) - .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP) - - )); - Assert.assertEquals(200, response.getStatus()); - AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class); - String exchangedAccessToken = tokenResponse.getToken(); - JWSInput jws = new JWSInput(tokenResponse.getToken()); - AccessToken token = jws.readJsonContent(AccessToken.class); - response.close(); - - exchangedUserId = token.getSubject(); - exchangedUsername = token.getPreferredUsername(); - - System.out.println("exchangedUserId: " + exchangedUserId); - System.out.println("exchangedUsername: " + exchangedUsername); + System.out.println("exchangedUserId: " + exchangedUserId); + System.out.println("exchangedUsername: " + exchangedUsername); - // test that we can exchange back to external token - response = exchangeUrl.request() - .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) - .post(Entity.form( - new Form() - .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) - .param(OAuth2Constants.SUBJECT_TOKEN, tokenResponse.getToken()) - .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE) - .param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP) + // test that we can exchange back to external token + response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, tokenResponse.getToken()) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.ACCESS_TOKEN_TYPE) + .param(OAuth2Constants.REQUESTED_ISSUER, PARENT_IDP) - )); - Assert.assertEquals(200, response.getStatus()); - tokenResponse = response.readEntity(AccessTokenResponse.class); - Assert.assertEquals(accessToken, tokenResponse.getToken()); - response.close(); + )); + Assert.assertEquals(200, response.getStatus()); + tokenResponse = response.readEntity(AccessTokenResponse.class); + Assert.assertEquals(accessToken, tokenResponse.getToken()); + response.close(); - Assert.assertEquals(1, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); + Assert.assertEquals(1, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); - // test logout - response = childLogoutWebTarget(httpClient) - .queryParam("id_token_hint", exchangedAccessToken) - .request() - .get(); - response.close(); + // test logout + response = childLogoutWebTarget(httpClient) + .queryParam("id_token_hint", exchangedAccessToken) + .request() + .get(); + response.close(); - Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); + Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); - List links = childRealm.users().get(exchangedUserId).getFederatedIdentity(); - Assert.assertEquals(1, links.size()); - } - { - // check that we can request an exchange again and that the previously linked user is obtained - Response response = exchangeUrl.request() - .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) - .post(Entity.form( - new Form() - .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) - .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) - .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE) - .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP) + List links = childRealm.users().get(exchangedUserId).getFederatedIdentity(); + Assert.assertEquals(1, links.size()); + } + { + // check that we can request an exchange again and that the previously linked user is obtained + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE) + .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP) - )); - Assert.assertEquals(200, response.getStatus()); - AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class); - String exchangedAccessToken = tokenResponse.getToken(); - JWSInput jws = new JWSInput(tokenResponse.getToken()); - AccessToken token = jws.readJsonContent(AccessToken.class); - response.close(); + )); + Assert.assertEquals(200, response.getStatus()); + AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class); + String exchangedAccessToken = tokenResponse.getToken(); + JWSInput jws = new JWSInput(tokenResponse.getToken()); + AccessToken token = jws.readJsonContent(AccessToken.class); + response.close(); - String exchanged2UserId = token.getSubject(); - String exchanged2Username = token.getPreferredUsername(); + String exchanged2UserId = token.getSubject(); + String exchanged2Username = token.getPreferredUsername(); - // assert that we get the same linked account as was previously imported + // assert that we get the same linked account as was previously imported - Assert.assertEquals(exchangedUserId, exchanged2UserId); - Assert.assertEquals(exchangedUsername, exchanged2Username); + Assert.assertEquals(exchangedUserId, exchanged2UserId); + Assert.assertEquals(exchangedUsername, exchanged2Username); - // test logout - response = childLogoutWebTarget(httpClient) - .queryParam("id_token_hint", exchangedAccessToken) - .request() - .get(); - response.close(); + // test logout + response = childLogoutWebTarget(httpClient) + .queryParam("id_token_hint", exchangedAccessToken) + .request() + .get(); + response.close(); - Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); + Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); - List links = childRealm.users().get(exchangedUserId).getFederatedIdentity(); - Assert.assertEquals(1, links.size()); - } - { - // check that we can exchange without specifying an SUBJECT_ISSUER - Response response = exchangeUrl.request() - .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) - .post(Entity.form( - new Form() - .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) - .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) - .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE) + List links = childRealm.users().get(exchangedUserId).getFederatedIdentity(); + Assert.assertEquals(1, links.size()); + } + { + // check that we can exchange without specifying an SUBJECT_ISSUER + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE) - )); - Assert.assertEquals(200, response.getStatus()); - AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class); - String exchangedAccessToken = tokenResponse.getToken(); - JWSInput jws = new JWSInput(tokenResponse.getToken()); - AccessToken token = jws.readJsonContent(AccessToken.class); - response.close(); + )); + Assert.assertEquals(200, response.getStatus()); + AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class); + String exchangedAccessToken = tokenResponse.getToken(); + JWSInput jws = new JWSInput(tokenResponse.getToken()); + AccessToken token = jws.readJsonContent(AccessToken.class); + response.close(); - String exchanged2UserId = token.getSubject(); - String exchanged2Username = token.getPreferredUsername(); + String exchanged2UserId = token.getSubject(); + String exchanged2Username = token.getPreferredUsername(); - // assert that we get the same linked account as was previously imported + // assert that we get the same linked account as was previously imported - Assert.assertEquals(exchangedUserId, exchanged2UserId); - Assert.assertEquals(exchangedUsername, exchanged2Username); + Assert.assertEquals(exchangedUserId, exchanged2UserId); + Assert.assertEquals(exchangedUsername, exchanged2Username); - // test logout - response = childLogoutWebTarget(httpClient) - .queryParam("id_token_hint", exchangedAccessToken) - .request() - .get(); - response.close(); + // test logout + response = childLogoutWebTarget(httpClient) + .queryParam("id_token_hint", exchangedAccessToken) + .request() + .get(); + response.close(); - Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); + Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); - List links = childRealm.users().get(exchangedUserId).getFederatedIdentity(); - Assert.assertEquals(1, links.size()); - } - // cleanup remove the user - childRealm.users().get(exchangedUserId).remove(); + List links = childRealm.users().get(exchangedUserId).getFederatedIdentity(); + Assert.assertEquals(1, links.size()); + } + // cleanup remove the user + childRealm.users().get(exchangedUserId).remove(); - { - // test unauthorized client gets 403 - Response response = exchangeUrl.request() - .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(UNAUTHORIZED_CHILD_CLIENT, "password")) - .post(Entity.form( - new Form() - .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) - .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) - .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE) - .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP) + { + // test unauthorized client gets 403 + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(UNAUTHORIZED_CHILD_CLIENT, "password")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE) + .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP) - )); - Assert.assertEquals(403, response.getStatus()); + )); + Assert.assertEquals(403, response.getStatus()); + } + } finally { + httpClient.close(); } } @@ -736,6 +728,53 @@ public class BrokerLinkAndTokenExchangeTest extends AbstractServletsAdapterTest WaitUtils.waitForPageToLoad(); } - + private void checkFeature(int statusCode) throws Exception { + String accessToken = oauth.doGrantAccessTokenRequest(PARENT_IDP, PARENT2_USERNAME, "password", null, PARENT_CLIENT, "password").getAccessToken(); + if (statusCode != Response.Status.NOT_IMPLEMENTED.getStatusCode()) { + Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); + } + + Client httpClient = ClientBuilder.newClient(); + try { + WebTarget exchangeUrl = childTokenExchangeWebTarget(httpClient); + { + IdentityProviderRepresentation rep = adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).toRepresentation(); + rep.getConfig().put(OIDCIdentityProviderConfig.VALIDATE_SIGNATURE, String.valueOf(false)); + adminClient.realm(CHILD_IDP).identityProviders().get(PARENT_IDP).update(rep); + // test user info validation. + Response response = exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader(ClientApp.DEPLOYMENT_NAME, "password")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, accessToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, OAuth2Constants.JWT_TOKEN_TYPE) + .param(OAuth2Constants.SUBJECT_ISSUER, PARENT_IDP) + + )); + Assert.assertEquals(statusCode, response.getStatus()); + + if (statusCode != Response.Status.NOT_IMPLEMENTED.getStatusCode()) { + AccessTokenResponse tokenResponse = response.readEntity(AccessTokenResponse.class); + String exchangedAccessToken = tokenResponse.getToken(); + Assert.assertNotNull(exchangedAccessToken); + response.close(); + + Assert.assertEquals(1, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); + + // test logout + response = childLogoutWebTarget(httpClient) + .queryParam("id_token_hint", exchangedAccessToken) + .request() + .get(); + response.close(); + + Assert.assertEquals(0, adminClient.realm(CHILD_IDP).getClientSessionStats().size()); + } + } + } finally { + httpClient.close(); + } + } }