diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index bf3bc9f9f4..f9efe19ab6 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -815,52 +815,58 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal return browserAuthentication(clientCode.getClientSession(), message); } - private Response performAccountLinking(ClientSessionModel clientSession, BrokeredIdentityContext context, FederatedIdentityModel federatedIdentityModel, UserModel federatedUser) { + private Response performAccountLinking(ClientSessionModel clientSession, BrokeredIdentityContext context, FederatedIdentityModel newModel, UserModel federatedUser) { this.event.event(EventType.FEDERATED_IDENTITY_LINK); + + UserModel authenticatedUser = clientSession.getUserSession().getUser(); - if (federatedUser != null) { - if (authenticatedUser.getId().equals(federatedUser.getId())) { - // refresh the token - if (context.getIdpConfig().isStoreToken()) { - federatedIdentityModel = this.session.users().getFederatedIdentity(federatedUser, context.getIdpConfig().getAlias(), this.realmModel); - if (!ObjectUtil.isEqualOrBothNull(context.getToken(), federatedIdentityModel.getToken())) { - - this.session.users().updateFederatedIdentity(this.realmModel, federatedUser, federatedIdentityModel); - - if (isDebugEnabled()) { - logger.debugf("Identity [%s] update with response from identity provider [%s].", federatedUser, context.getIdpConfig().getAlias()); - } - } - } - return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build(); - } else { - return redirectToAccountErrorPage(clientSession, Messages.IDENTITY_PROVIDER_ALREADY_LINKED, context.getIdpConfig().getAlias()); - } - } - - - if (isDebugEnabled()) { - logger.debugf("Linking account [%s] from identity provider [%s] to user [%s].", federatedIdentityModel, context.getIdpConfig().getAlias(), authenticatedUser); - } - - if (!authenticatedUser.isEnabled()) { - return redirectToAccountErrorPage(clientSession, Messages.ACCOUNT_DISABLED); + if (federatedUser != null && !authenticatedUser.getId().equals(federatedUser.getId())) { + return redirectToAccountErrorPage(clientSession, Messages.IDENTITY_PROVIDER_ALREADY_LINKED, context.getIdpConfig().getAlias()); } if (!authenticatedUser.hasRole(this.realmModel.getClientByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID).getRole(MANAGE_ACCOUNT))) { return redirectToErrorPage(Messages.INSUFFICIENT_PERMISSION); } - this.session.users().addFederatedIdentity(this.realmModel, authenticatedUser, federatedIdentityModel); + if (!authenticatedUser.isEnabled()) { + return redirectToAccountErrorPage(clientSession, Messages.ACCOUNT_DISABLED); + } + + + + if (federatedUser != null) { + if (context.getIdpConfig().isStoreToken()) { + FederatedIdentityModel oldModel = this.session.users().getFederatedIdentity(federatedUser, context.getIdpConfig().getAlias(), this.realmModel); + if (!ObjectUtil.isEqualOrBothNull(context.getToken(), oldModel.getToken())) { + this.session.users().updateFederatedIdentity(this.realmModel, federatedUser, newModel); + if (isDebugEnabled()) { + logger.debugf("Identity [%s] update with response from identity provider [%s].", federatedUser, context.getIdpConfig().getAlias()); + } + } + } + } else { + this.session.users().addFederatedIdentity(this.realmModel, authenticatedUser, newModel); + } context.getIdp().attachUserSession(clientSession.getUserSession(), clientSession, context); + + if (isDebugEnabled()) { + logger.debugf("Linking account [%s] from identity provider [%s] to user [%s].", newModel, context.getIdpConfig().getAlias(), authenticatedUser); + } + this.event.user(authenticatedUser) .detail(Details.USERNAME, authenticatedUser.getUsername()) - .detail(Details.IDENTITY_PROVIDER, federatedIdentityModel.getIdentityProvider()) - .detail(Details.IDENTITY_PROVIDER_USERNAME, federatedIdentityModel.getUserName()) + .detail(Details.IDENTITY_PROVIDER, newModel.getIdentityProvider()) + .detail(Details.IDENTITY_PROVIDER_USERNAME, newModel.getUserName()) .success(); + + // we do this to make sure that the parent IDP is logged out when this user session is complete. + + clientSession.getUserSession().setNote(Details.IDENTITY_PROVIDER, context.getIdpConfig().getAlias()); + clientSession.getUserSession().setNote(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername()); + return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerTestTools.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerTestTools.java index 40c1f704fa..a43d842c84 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerTestTools.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerTestTools.java @@ -93,12 +93,12 @@ public class BrokerTestTools { public static void createKcOidcBroker(Keycloak adminClient, String childRealm, String idpRealm, SuiteContext suiteContext, String alias, boolean linkOnly) { IdentityProviderRepresentation idp = createIdentityProvider(alias, IDP_OIDC_PROVIDER_ID); idp.setLinkOnly(linkOnly); + idp.setStoreToken(true); Map config = idp.getConfig(); config.put("clientId", childRealm); config.put("clientSecret", childRealm); - config.put("prompt", "login"); config.put("authorizationUrl", getAuthRoot(suiteContext) + "/auth/realms/" + idpRealm + "/protocol/openid-connect/auth"); config.put("tokenUrl", getAuthRoot(suiteContext) + "/auth/realms/" + idpRealm + "/protocol/openid-connect/token"); config.put("logoutUrl", getAuthRoot(suiteContext) + "/auth/realms/" + idpRealm + "/protocol/openid-connect/logout"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/ClientInitiatedAccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/ClientInitiatedAccountLinkTest.java index e63912791f..487e8784fa 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/ClientInitiatedAccountLinkTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/ClientInitiatedAccountLinkTest.java @@ -29,8 +29,10 @@ import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.common.util.Base64Url; +import org.keycloak.models.Constants; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; +import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; @@ -48,7 +50,12 @@ import org.keycloak.testsuite.pages.AccountFederatedIdentityPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.UpdateAccountInformationPage; import org.keycloak.testsuite.util.AdapterServletDeployment; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.util.JsonSerialization; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import java.net.URL; import java.util.LinkedList; @@ -82,6 +89,10 @@ public class ClientInitiatedAccountLinkTest extends AbstractKeycloakTest { @Page protected AppServerContextRoot appServerContextRootPage; + @ArquillianResource + protected OAuthClient oauth; + + public boolean isRelative() { return testContext.isRelativeAdapterTest(); } @@ -122,6 +133,7 @@ public class ClientInitiatedAccountLinkTest extends AbstractKeycloakTest { uri = appServerContextRootPage.toString() + uri; } servlet.setAdminUrl(uri); + servlet.setDirectAccessGrantsEnabled(true); servlet.setBaseUrl(uri); servlet.setRedirectUris(new LinkedList<>()); servlet.getRedirectUris().add(uri + "/*"); @@ -172,6 +184,11 @@ public class ClientInitiatedAccountLinkTest extends AbstractKeycloakTest { List roles = new LinkedList<>(); roles.add(role); realm.users().get(childUserId).roles().realmLevel().add(roles); + ClientRepresentation brokerService = realm.clients().findByClientId(Constants.BROKER_SERVICE_CLIENT_ID).get(0); + role = realm.clients().get(brokerService.getId()).roles().get(Constants.READ_TOKEN_ROLE).toRepresentation(); + roles.clear(); + roles.add(role); + realm.users().get(childUserId).roles().clientLevel(brokerService.getId()).add(roles); } @@ -391,6 +408,25 @@ public class ClientInitiatedAccountLinkTest extends AbstractKeycloakTest { Assert.assertTrue(driver.getCurrentUrl().startsWith(linkBuilder.toTemplate())); Assert.assertTrue(driver.getPageSource().contains("Account Linked")); + OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(CHILD_IDP, "child", "password", null, "client-linking", "password"); + Assert.assertNotNull(response.getAccessToken()); + Assert.assertNull(response.getError()); + Client httpClient = ClientBuilder.newClient(); + String firstToken = getToken(response, httpClient); + Assert.assertNotNull(firstToken); + + + driver.navigate().to(linkUrl); + Assert.assertTrue(driver.getPageSource().contains("Account Linked")); + String nextToken = getToken(response, httpClient); + Assert.assertNotNull(nextToken); + Assert.assertNotEquals(firstToken, nextToken); + + + + + + links = realm.users().get(childUserId).getFederatedIdentity(); Assert.assertFalse(links.isEmpty()); @@ -403,6 +439,19 @@ public class ClientInitiatedAccountLinkTest extends AbstractKeycloakTest { } + private String getToken(OAuthClient.AccessTokenResponse response, Client httpClient) throws Exception { + String idpToken = httpClient.target(oauth.AUTH_SERVER_ROOT) + .path("realms") + .path("child/broker") + .path(PARENT_IDP) + .path("token") + .request() + .header("Authorization", "Bearer " + response.getAccessToken()) + .get(String.class); + AccessTokenResponse res = JsonSerialization.readValue(idpToken, AccessTokenResponse.class); + return res.getToken(); + } + public void logoutAll() { String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()).build(CHILD_IDP).toString(); driver.navigate().to(logoutUri); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java index 0977d2dbc6..b047595f5d 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java @@ -433,6 +433,7 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent // Assert I am logged immediately to account management due to previously linked "test-user" identity loginPage.clickSocial(identityProviderModel.getAlias()); + this.loginPage.login("test-user", "password"); doAfterProviderAuthentication(); assertTrue(accountFederatedIdentityPage.isCurrent()); assertTrue(driver.getPageSource().contains("id=\"remove-" + identityProviderModel.getAlias() + "\""));