diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java index 51e9a459bd..b02dd0d9b2 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java @@ -17,6 +17,7 @@ package org.keycloak.admin.client.resource; +import org.jboss.resteasy.annotations.cache.NoCache; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.GroupRepresentation; @@ -35,6 +36,7 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.List; +import java.util.Map; /** * @author rodrigo.sasaki@icarros.com.br @@ -132,4 +134,13 @@ public interface UserResource { @Path("role-mappings") public RoleMappingResource roles(); + + @GET + @Path("consents") + public List> getConsents(); + + @DELETE + @Path("consents/{client}") + public void revokeConsent(@PathParam("client") String clientId); + } diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java index c585c62c13..7fae66d96b 100755 --- a/services/src/main/java/org/keycloak/services/messages/Messages.java +++ b/services/src/main/java/org/keycloak/services/messages/Messages.java @@ -156,6 +156,8 @@ public class Messages { public static final String STALE_CODE = "staleCodeMessage"; + public static final String STALE_CODE_ACCOUNT = "staleCodeAccountMessage"; + public static final String IDENTITY_PROVIDER_NOT_UNIQUE = "identityProviderNotUniqueMessage"; public static final String REALM_SUPPORTS_NO_CREDENTIALS = "realmSupportsNoCredentialsMessage"; 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 c0cdb59a5b..5218983643 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -634,7 +634,22 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal if (!clientCode.isValid(AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) { logger.debugf("Authorization code is not valid. Client session ID: %s, Client session's action: %s", clientSession.getId(), clientSession.getAction()); - Response staleCodeError = redirectToErrorPage(Messages.STALE_CODE); + + Response staleCodeError; + + // Linking identityProvider from account mgmt + if (clientSession.getUserSession() != null && client.getClientId().equals(ACCOUNT_MANAGEMENT_CLIENT_ID)) { + + this.event.event(EventType.FEDERATED_IDENTITY_LINK); + UserModel user = clientSession.getUserSession().getUser(); + this.event.user(user); + this.event.detail(Details.USERNAME, user.getUsername()); + + staleCodeError = redirectToAccountErrorPage(clientSession, Messages.STALE_CODE_ACCOUNT); + } else { + staleCodeError = redirectToErrorPage(Messages.STALE_CODE); + } + return ParsedCodeContext.response(staleCodeError); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java index 4d72ea8489..446ad5a3af 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeyCloakServerBrokerBasicTest.java @@ -146,88 +146,6 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP keycloak.realm("realm-with-broker").identityProviders().get("kc-oidc-idp").update(idp); } - - @Test - public void testConsentDeniedWithExpiredClientSession() throws Exception { - // Disable update profile - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); - - Keycloak keycloak1 = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID); - Keycloak keycloak2 = Keycloak.getInstance("http://localhost:8082/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID); - - // Require broker to show consent screen - RealmResource brokeredRealm = keycloak2.realm("realm-with-oidc-identity-provider"); - List clients = brokeredRealm.clients().findByClientId("broker-app"); - Assert.assertEquals(1, clients.size()); - ClientRepresentation brokerApp = clients.get(0); - brokerApp.setConsentRequired(true); - brokeredRealm.clients().get(brokerApp.getId()).update(brokerApp); - - // Change timeouts on realm-with-broker to lower values - RealmResource realmWithBroker = keycloak1.realm("realm-with-broker"); - RealmRepresentation realmBackup = realmWithBroker.toRepresentation(); - RealmRepresentation realm = realmWithBroker.toRepresentation(); - realm.setAccessCodeLifespanLogin(30);; - realm.setAccessCodeLifespan(30); - realm.setAccessCodeLifespanUserAction(30); - realmWithBroker.update(realm); - - // Login to broker - loginIDP("test-user"); - - // Set time offset - Time.setOffset(60); - try { - // User rejected consent - grantPage.assertCurrent(); - grantPage.cancel(); - - // Assert error page with backToApplication link displayed - errorPage.assertCurrent(); - errorPage.clickBackToApplication(); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); - - - // Login to broker again - loginIDP("test-user"); - - // Set time offset and manually remove expiredSessions TODO: Will require custom endpoint when migrate to integration-arquillian - Time.setOffset(120); - - brokerServerRule.stopSession(this.session, true); - this.session = brokerServerRule.startSession(); - - session.sessions().removeExpired(getRealm()); - - brokerServerRule.stopSession(this.session, true); - this.session = brokerServerRule.startSession(); - - // User rejected consent - grantPage.assertCurrent(); - grantPage.cancel(); - - // Assert error page without backToApplication link (clientSession expired and was removed on the server) - errorPage.assertCurrent(); - try { - errorPage.clickBackToApplication(); - fail("Not expected to have link backToApplication available"); - } catch (NoSuchElementException nsee) { - // Expected; - } - - } finally { - Time.setOffset(0); - } - - // Revert consent - brokerApp.setConsentRequired(false); - brokeredRealm.clients().get(brokerApp.getId()).update(brokerApp); - - // Revert timeouts - realmWithBroker.update(realmBackup); - } - @Test public void testSuccessfulAuthenticationWithoutUpdateProfile() { super.testSuccessfulAuthenticationWithoutUpdateProfile(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithConsentTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithConsentTest.java new file mode 100644 index 0000000000..c2606e3c0d --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCKeycloakServerBrokerWithConsentTest.java @@ -0,0 +1,227 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.testsuite.broker; + +import java.util.List; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.admin.client.Keycloak; +import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.common.util.Time; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.KeycloakServer; +import org.keycloak.testsuite.pages.AccountFederatedIdentityPage; +import org.keycloak.testsuite.rule.AbstractKeycloakRule; +import org.keycloak.testsuite.rule.WebResource; +import org.openqa.selenium.NoSuchElementException; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author Marek Posolda + */ +public class OIDCKeycloakServerBrokerWithConsentTest extends AbstractIdentityProviderTest { + + private static final int PORT = 8082; + + private static Keycloak keycloak1; + private static Keycloak keycloak2; + + @ClassRule + public static AbstractKeycloakRule oidcServerRule = new AbstractKeycloakRule() { + + @Override + protected void configureServer(KeycloakServer server) { + server.getConfig().setPort(PORT); + } + + @Override + protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) { + server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-kc-oidc.json")); + + // Disable update profile + RealmModel realm = getRealm(session); + setUpdateProfileFirstLogin(realm, IdentityProviderRepresentation.UPFLM_OFF); + } + + @Override + protected String[] getTestRealms() { + return new String[] { "realm-with-oidc-identity-provider" }; + } + }; + + + @BeforeClass + public static void before() { + keycloak1 = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID); + keycloak2 = Keycloak.getInstance("http://localhost:8082/auth", "master", "admin", "admin", org.keycloak.models.Constants.ADMIN_CLI_CLIENT_ID); + + // Require broker to show consent screen + RealmResource brokeredRealm = keycloak2.realm("realm-with-oidc-identity-provider"); + List clients = brokeredRealm.clients().findByClientId("broker-app"); + Assert.assertEquals(1, clients.size()); + ClientRepresentation brokerApp = clients.get(0); + brokerApp.setConsentRequired(true); + brokeredRealm.clients().get(brokerApp.getId()).update(brokerApp); + + + // Change timeouts on realm-with-broker to lower values + RealmResource realmWithBroker = keycloak1.realm("realm-with-broker"); + RealmRepresentation realmRep = realmWithBroker.toRepresentation(); + realmRep.setAccessCodeLifespanLogin(30);; + realmRep.setAccessCodeLifespan(30); + realmRep.setAccessCodeLifespanUserAction(30); + realmWithBroker.update(realmRep); + } + + + @Override + protected String getProviderId() { + return "kc-oidc-idp"; + } + + + // KEYCLOAK-2769 + @Test + public void testConsentDeniedWithExpiredClientSession() throws Exception { + // Login to broker + loginIDP("test-user"); + + // Set time offset + Time.setOffset(60); + try { + // User rejected consent + grantPage.assertCurrent(); + grantPage.cancel(); + + // Assert error page with backToApplication link displayed + errorPage.assertCurrent(); + errorPage.clickBackToApplication(); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); + + } finally { + Time.setOffset(0); + } + } + + + // KEYCLOAK-2769 + @Test + public void testConsentDeniedWithExpiredAndClearedClientSession() throws Exception { + // Login to broker again + loginIDP("test-user"); + + // Set time offset + Time.setOffset(60); + try { + // Manually remove expiredSessions TODO: Will require custom endpoint when migrate to integration-arquillian + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + session.sessions().removeExpired(getRealm()); + + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + // User rejected consent + grantPage.assertCurrent(); + grantPage.cancel(); + + // Assert error page without backToApplication link (clientSession expired and was removed on the server) + errorPage.assertCurrent(); + try { + errorPage.clickBackToApplication(); + fail("Not expected to have link backToApplication available"); + } catch (NoSuchElementException nsee) { + // Expected; + } + + } finally { + Time.setOffset(0); + } + } + + + // KEYCLOAK-2801 + @Test + public void testAccountManagementLinkingAndExpiredClientSession() throws Exception { + // Login as pedroigor to account management + accountFederatedIdentityPage.realm("realm-with-broker"); + accountFederatedIdentityPage.open(); + assertTrue(driver.getTitle().equals("Log in to realm-with-broker")); + loginPage.login("pedroigor", "password"); + assertTrue(accountFederatedIdentityPage.isCurrent()); + + // Link my "pedroigor" identity with "test-user" from brokered Keycloak + accountFederatedIdentityPage.clickAddProvider(getProviderId()); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/")); + this.loginPage.login("test-user", "password"); + + // Set time offset + Time.setOffset(60); + try { + // User rejected consent + grantPage.assertCurrent(); + grantPage.cancel(); + + // Assert account error page with "staleCodeAccount" error displayed + accountFederatedIdentityPage.assertCurrent(); + Assert.assertEquals("The page expired. Please try one more time", accountFederatedIdentityPage.getError()); + + + // Try to link one more time + accountFederatedIdentityPage.clickAddProvider(getProviderId()); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/")); + this.loginPage.login("test-user", "password"); + + Time.setOffset(120); + + // User granted consent + grantPage.assertCurrent(); + grantPage.accept(); + + // Assert account error page with "staleCodeAccount" error displayed + accountFederatedIdentityPage.assertCurrent(); + Assert.assertEquals("The page expired. Please try one more time", accountFederatedIdentityPage.getError()); + + } finally { + Time.setOffset(0); + + // Revoke consent + RealmResource brokeredRealm = keycloak2.realm("realm-with-oidc-identity-provider"); + List users = brokeredRealm.users().search("test-user", 0, 1); + brokeredRealm.users().get(users.get(0).getId()).revokeConsent("broker-app"); + } + } +} diff --git a/themes/src/main/resources/theme/base/account/messages/messages_en.properties b/themes/src/main/resources/theme/base/account/messages/messages_en.properties index 5803146529..27a4565ba9 100755 --- a/themes/src/main/resources/theme/base/account/messages/messages_en.properties +++ b/themes/src/main/resources/theme/base/account/messages/messages_en.properties @@ -135,6 +135,7 @@ federatedIdentityRemovingLastProviderMessage=You can''t remove last federated id identityProviderRedirectErrorMessage=Failed to redirect to identity provider. identityProviderRemovedMessage=Identity provider removed successfully. identityProviderAlreadyLinkedMessage=Federated identity returned by {0} is already linked to another user. +staleCodeAccountMessage=The page expired. Please try one more time accountDisabledMessage=Account is disabled, contact admin.