From 1437f7da3555fdfa5f9ee545b96b1b3198abc776 Mon Sep 17 00:00:00 2001 From: Michael Gerber Date: Tue, 10 Nov 2015 09:04:10 +0100 Subject: [PATCH 1/4] KEYCLOAK-2024 - username guessing --- .../broker/IdpUsernamePasswordForm.java | 2 +- .../AbstractUsernameFormAuthenticator.java | 51 ++++++++++++------- .../browser/UsernamePasswordForm.java | 2 +- .../keycloak/testsuite/forms/LoginTest.java | 6 ++- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java index b293d1bb439..5e732d8be79 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpUsernamePasswordForm.java @@ -37,7 +37,7 @@ public class IdpUsernamePasswordForm extends UsernamePasswordForm { // Restore formData for the case of error setupForm(context, formData, existingUser); - return validatePassword(context, formData); + return validatePassword(context, existingUser, formData); } protected LoginFormsProvider setupForm(AuthenticationFlowContext context, MultivaluedMap formData, UserModel existingUser) { diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java index 294d220696f..0506f2156bb 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java @@ -71,12 +71,16 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth context.failureChallenge(AuthenticationFlowError.INVALID_USER, challengeResponse); return true; } + return false; + } + + public boolean enabledUser(AuthenticationFlowContext context, UserModel user) { if (!user.isEnabled()) { context.getEvent().user(user); context.getEvent().error(Errors.USER_DISABLED); Response challengeResponse = disabledUser(context); context.failureChallenge(AuthenticationFlowError.USER_DISABLED, challengeResponse); - return true; + return false; } if (context.getRealm().isBruteForceProtected()) { if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user.getUsername())) { @@ -84,13 +88,13 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth context.getEvent().error(Errors.USER_TEMPORARILY_DISABLED); Response challengeResponse = temporarilyDisabledUser(context); context.failureChallenge(AuthenticationFlowError.USER_TEMPORARILY_DISABLED, challengeResponse); - return true; + return false; } } - return false; + return true; } - public boolean validateUser(AuthenticationFlowContext context, MultivaluedMap inputData) { + public boolean validateUserAndPassword(AuthenticationFlowContext context, MultivaluedMap inputData) { String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME); if (username == null) { context.getEvent().error(Errors.USER_NOT_FOUND); @@ -117,7 +121,18 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth return false; } - if (invalidUser(context, user)) return false; + if (invalidUser(context, user)){ + return false; + } + + if (!validatePassword(context, user, inputData)){ + return false; + } + + if(!enabledUser(context, user)){ + return false; + } + String rememberMe = inputData.getFirst("rememberMe"); boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on"); if (remember) { @@ -130,29 +145,27 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth return true; } - public boolean validatePassword(AuthenticationFlowContext context, MultivaluedMap inputData) { + public boolean validatePassword(AuthenticationFlowContext context, UserModel user, MultivaluedMap inputData) { List credentials = new LinkedList<>(); String password = inputData.getFirst(CredentialRepresentation.PASSWORD); if (password == null || password.isEmpty()) { - if (context.getUser() != null) { - context.getEvent().user(context.getUser()); - } - context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); - Response challengeResponse = invalidCredentials(context); - context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse); - context.clearUser(); + invalidPassword(context, user); return false; } credentials.add(UserCredentialModel.password(password)); - boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials); + boolean valid = context.getSession().users().validCredentials(context.getRealm(), user, credentials); if (!valid) { - context.getEvent().user(context.getUser()); - context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); - Response challengeResponse = invalidCredentials(context); - context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse); - context.clearUser(); + invalidPassword(context, user); return false; } return true; } + + private void invalidPassword(AuthenticationFlowContext context, UserModel user) { + context.getEvent().user(user); + context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); + Response challengeResponse = invalidCredentials(context); + context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse); + context.clearUser(); + } } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java index 70d9fd99024..74636384bc7 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java @@ -38,7 +38,7 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl } protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap formData) { - return validateUser(context, formData) && validatePassword(context, formData); + return validateUserAndPassword(context, formData); } @Override diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java index 950c182a3f6..30d94fae783 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTest.java @@ -215,9 +215,10 @@ public class LoginTest { Assert.assertEquals("login-test", loginPage.getUsername()); Assert.assertEquals("", loginPage.getPassword()); - Assert.assertEquals("Account is disabled, contact admin.", loginPage.getError()); + // KEYCLOAK-2024 + Assert.assertEquals("Invalid username or password.", loginPage.getError()); - events.expectLogin().user(userId).session((String) null).error("user_disabled") + events.expectLogin().user(userId).session((String) null).error("invalid_user_credentials") .detail(Details.USERNAME, "login-test") .removeDetail(Details.CONSENT) .assertEvent(); @@ -250,6 +251,7 @@ public class LoginTest { Assert.assertEquals("login-test", loginPage.getUsername()); Assert.assertEquals("", loginPage.getPassword()); + // KEYCLOAK-2024 Assert.assertEquals("Account is disabled, contact admin.", loginPage.getError()); events.expectLogin().user(userId).session((String) null).error("user_disabled") From 6c3a3517b8e918be012486d16b337823f8f080fe Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Fri, 13 Nov 2015 09:03:58 +0100 Subject: [PATCH 2/4] KEYCLOAK-2037 Update documentation --- .../en/en-US/modules/server-installation.xml | 34 +++---------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml index 549bc84bc8d..78d9a4b3d64 100755 --- a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml +++ b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml @@ -818,44 +818,20 @@ $ keytool -import -alias yourdomain -keystore keycloak.jks -file your-certificat
- Adding Keycloak server in Domain Mode + Keycloak server in Domain Mode In domain mode, you start the server with the "domain" command instead of the "standalone" command. In this case, the Keycloak subsystem is defined in domain/configuration/domain.xml instead of standalone/configuration.standalone.xml. Inside domain.xml, you will see more than one - profile. A Keycloak subsystem can be defined in zero or more of those profiles. + profile. The Keycloak subsystem is defined for all initial profiles. - To enable Keycloak for a server profile edit domain/configuration/domain.xml. To the extensions - element add the Keycloak extension: - - ... - - -]]> - Then you need to add the server to the required server profiles. By default WildFly starts two servers - in the main-server-group which uses the full profile. To add Keycloak for this profile add the Keycloak - subsystem to the profile element with name full: - - ... - - - true - auth - - -]]> + THe server is also added to server profiles. By default two servers are started + in the main-server-group which uses the full profile. - To configure the server copy standalone/configuration/keycloak-server.json to - domain/servers/<SERVER NAME>/configuration. The configuration should be identical + You need to make sure domain/servers/<SERVER NAME>/configuration is identical for all servers in a group. - - Follow the Clustering section of the documentation to configure Keycloak - for clustering. In domain mode it doesn't make much sense to not configure Keycloak in cluster mode. - To deploy custom providers and themes you should deploys these as modules and make sure the modules are available to all servers in the group. See Providers and From f2e4e67ba00573f0cf25dcd2467cdd90f4a38a2a Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 10 Nov 2015 22:55:22 +0100 Subject: [PATCH 3/4] KEYCLOAK-1750 First broker login - events --- .../main/java/org/keycloak/events/Errors.java | 3 +-- .../java/org/keycloak/events/EventType.java | 4 +-- .../IdpCreateUserIfUniqueAuthenticator.java | 12 ++++++++- .../IdpEmailVerificationAuthenticator.java | 25 +++++++++++++------ .../services/resources/AccountService.java | 4 ++- .../resources/IdentityBrokerService.java | 25 ++++++++++++++----- .../resources/LoginActionsService.java | 4 +++ 7 files changed, 57 insertions(+), 20 deletions(-) diff --git a/events/api/src/main/java/org/keycloak/events/Errors.java b/events/api/src/main/java/org/keycloak/events/Errors.java index 34c5979c7cc..b0cbc6a3e1d 100755 --- a/events/api/src/main/java/org/keycloak/events/Errors.java +++ b/events/api/src/main/java/org/keycloak/events/Errors.java @@ -44,8 +44,7 @@ public interface Errors { String NOT_ALLOWED = "not_allowed"; - String FEDERATED_IDENTITY_EMAIL_EXISTS = "federated_identity_email_exists"; - String FEDERATED_IDENTITY_USERNAME_EXISTS = "federated_identity_username_exists"; + String FEDERATED_IDENTITY_EXISTS = "federated_identity_account_exists"; String SSL_REQUIRED = "ssl_required"; String USER_SESSION_NOT_FOUND = "user_session_not_found"; diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java index 5cffe7845ca..b75728bd15a 100755 --- a/events/api/src/main/java/org/keycloak/events/EventType.java +++ b/events/api/src/main/java/org/keycloak/events/EventType.java @@ -48,6 +48,8 @@ public enum EventType { SEND_VERIFY_EMAIL_ERROR(true), SEND_RESET_PASSWORD(true), SEND_RESET_PASSWORD_ERROR(true), + SEND_IDENTITY_PROVIDER_LINK(true), + SEND_IDENTITY_PROVIDER_LINK_ERROR(true), RESET_PASSWORD(true), RESET_PASSWORD_ERROR(true), @@ -66,8 +68,6 @@ public enum EventType { IDENTITY_PROVIDER_RESPONSE_ERROR(false), IDENTITY_PROVIDER_RETRIEVE_TOKEN(false), IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR(false), - IDENTITY_PROVIDER_ACCCOUNT_LINKING(false), - IDENTITY_PROVIDER_ACCCOUNT_LINKING_ERROR(false), IMPERSONATE(true), CUSTOM_REQUIRED_ACTION(true), CUSTOM_REQUIRED_ACTION_ERROR(true), diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java index ffb23004a39..b4ee957e43f 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpCreateUserIfUniqueAuthenticator.java @@ -10,11 +10,12 @@ import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.authenticators.broker.util.ExistingUserInfo; import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -import org.keycloak.models.utils.FormMessage; import org.keycloak.services.messages.Messages; /** @@ -78,6 +79,15 @@ public class IdpCreateUserIfUniqueAuthenticator extends AbstractIdpAuthenticator .setError(Messages.FEDERATED_IDENTITY_EXISTS, duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue()) .createErrorPage(); context.challenge(challengeResponse); + + if (context.getExecution().isRequired()) { + context.getEvent() + .user(duplication.getExistingUserId()) + .detail("existing_" + duplication.getDuplicateAttributeName(), duplication.getDuplicateAttributeValue()) + .removeDetail(Details.AUTH_METHOD) + .removeDetail(Details.AUTH_TYPE) + .error(Errors.FEDERATED_IDENTITY_EXISTS); + } } } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java index d6bf10fad36..ae28d3edc91 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java @@ -14,6 +14,10 @@ import org.keycloak.authentication.authenticators.broker.util.SerializedBrokered import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; +import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.Constants; @@ -52,6 +56,15 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator String link = UriBuilder.fromUri(context.getActionUrl()) .queryParam(Constants.KEY, clientSession.getNote(Constants.VERIFY_EMAIL_KEY)) .build().toString(); + + EventBuilder event = context.getEvent().clone().event(EventType.SEND_IDENTITY_PROVIDER_LINK) + .user(existingUser) + .detail(Details.USERNAME, existingUser.getUsername()) + .detail(Details.EMAIL, existingUser.getEmail()) + .detail(Details.CODE_ID, clientSession.getId()) + .removeDetail(Details.AUTH_METHOD) + .removeDetail(Details.AUTH_TYPE); + long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction()); try { @@ -60,15 +73,11 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator .setUser(existingUser) .setAttribute(EmailProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext) .sendConfirmIdentityBrokerLink(link, expiration); -// event.clone().event(EventType.SEND_RESET_PASSWORD) -// .user(user) -// .detail(Details.USERNAME, username) -// .detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, context.getClientSession().getId()).success(); + + event.success(); } catch (EmailException e) { -// event.clone().event(EventType.SEND_RESET_PASSWORD) -// .detail(Details.USERNAME, username) -// .user(user) -// .error(Errors.EMAIL_SEND_FAILED); + event.error(Errors.EMAIL_SEND_FAILED); + logger.error("Failed to send email to confirm identity broker linking", e); Response challenge = context.form() .setError(Messages.EMAIL_SENT_ERROR) diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 71f849b6d9f..2bebec3bfba 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -724,7 +724,9 @@ public class AccountService extends AbstractSecuredLocalService { logger.debugv("Social provider {0} removed successfully from user {1}", providerId, user.getUsername()); event.event(EventType.REMOVE_FEDERATED_IDENTITY).client(auth.getClient()).user(auth.getUser()) - .detail(Details.USERNAME, link.getUserId() + "@" + link.getIdentityProvider()) + .detail(Details.USERNAME, auth.getUser().getUsername()) + .detail(Details.IDENTITY_PROVIDER, link.getIdentityProvider()) + .detail(Details.IDENTITY_PROVIDER_USERNAME, link.getUserName()) .success(); setReferrerOnPage(); 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 fda37efc009..c8784bda2f4 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -368,6 +368,13 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal context.getUsername(), context.getToken()); session.users().addFederatedIdentity(realmModel, federatedUser, federatedIdentityModel); + EventBuilder event = this.event.clone().user(federatedUser) + .detail(Details.CODE_ID, clientSession.getId()) + .detail(Details.USERNAME, federatedUser.getUsername()) + .detail(Details.IDENTITY_PROVIDER, providerId) + .detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername()) + .removeDetail("auth_method"); + String isRegisteredNewUser = clientSession.getNote(AbstractIdpAuthenticator.BROKER_REGISTERED_NEW_USER); if (Boolean.parseBoolean(isRegisteredNewUser)) { @@ -388,15 +395,17 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal federatedUser.setEmailVerified(true); } - this.event.clone().user(federatedUser).event(EventType.REGISTER) - .detail(Details.IDENTITY_PROVIDER, providerId) - .detail(Details.IDENTITY_PROVIDER_USERNAME, context.getUsername()) - .removeDetail("auth_method") + event.event(EventType.REGISTER) + .detail(Details.REGISTER_METHOD, "broker") + .detail(Details.EMAIL, federatedUser.getEmail()) .success(); } else { LOGGER.debugf("Linked existing keycloak user '%s' with identity provider '%s' . Identity provider username is '%s' .", federatedUser.getUsername(), providerId, context.getUsername()); + event.event(EventType.FEDERATED_IDENTITY_LINK) + .success(); + updateFederatedIdentity(context, federatedUser); } @@ -453,7 +462,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal } private Response performAccountLinking(ClientSessionModel clientSession, BrokeredIdentityContext context, FederatedIdentityModel federatedIdentityModel, UserModel federatedUser) { - this.event.event(EventType.IDENTITY_PROVIDER_ACCCOUNT_LINKING); + this.event.event(EventType.FEDERATED_IDENTITY_LINK); if (federatedUser != null) { return redirectToErrorPage(Messages.IDENTITY_PROVIDER_ALREADY_LINKED, context.getIdpConfig().getAlias()); @@ -478,7 +487,11 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal this.session.users().addFederatedIdentity(this.realmModel, authenticatedUser, federatedIdentityModel); context.getIdp().attachUserSession(clientSession.getUserSession(), clientSession, context); - this.event.success(); + this.event.user(authenticatedUser) + .detail(Details.USERNAME, authenticatedUser.getUsername()) + .detail(Details.IDENTITY_PROVIDER, federatedIdentityModel.getIdentityProvider()) + .detail(Details.IDENTITY_PROVIDER_USERNAME, federatedIdentityModel.getUserName()) + .success(); return Response.status(302).location(UriBuilder.fromUri(clientSession.getRedirectUri()).build()).build(); } diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index 8f845c051ff..7f15d2d025b 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -509,6 +509,10 @@ public class LoginActionsService { BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, clientSession); AuthenticationFlowModel firstBrokerLoginFlow = realm.getAuthenticationFlowById(brokerContext.getIdpConfig().getFirstBrokerLoginFlowId()); + event.detail(Details.IDENTITY_PROVIDER, brokerContext.getIdpConfig().getAlias()) + .detail(Details.IDENTITY_PROVIDER_USERNAME, brokerContext.getUsername()); + + AuthenticationProcessor processor = new AuthenticationProcessor() { @Override From ce4a8655797869b6df6d55564aad9b2d05523e56 Mon Sep 17 00:00:00 2001 From: mposolda Date: Wed, 11 Nov 2015 11:12:03 +0100 Subject: [PATCH 4/4] KEYCLOAK-1750 First broker login - tests --- .../org/keycloak/common/util/ObjectUtil.java | 4 + .../base/login/login-idp-link-confirm.ftl | 4 +- .../theme/base/login/login-idp-link-email.ftl | 4 +- .../freemarker/FreeMarkerEmailProvider.java | 13 +- .../FreeMarkerLoginFormsProvider.java | 5 +- .../utils/DefaultAuthenticationFlows.java | 6 +- .../broker/AbstractFirstBrokerLoginTest.java | 397 +++++++++++ .../broker/AbstractIdentityProviderTest.java | 665 ++---------------- .../AbstractKeycloakIdentityProviderTest.java | 519 ++++++++++++++ .../broker/OIDCFirstBrokerLoginTest.java | 145 ++++ .../OIDCKeyCloakServerBrokerBasicTest.java | 2 +- .../broker/SAMLFirstBrokerLoginTest.java | 40 ++ .../SAMLKeyCloakServerBrokerBasicTest.java | 2 +- ...KeyCloakServerBrokerWithSignatureTest.java | 2 +- .../testsuite/pages/IdpConfirmLinkPage.java | 41 ++ .../testsuite/pages/IdpLinkEmailPage.java | 27 + .../keycloak/testsuite/pages/LoginPage.java | 4 + .../pages/LoginUpdateProfilePage.java | 2 + 18 files changed, 1272 insertions(+), 610 deletions(-) create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCFirstBrokerLoginTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpConfirmLinkPage.java create mode 100644 testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java diff --git a/common/src/main/java/org/keycloak/common/util/ObjectUtil.java b/common/src/main/java/org/keycloak/common/util/ObjectUtil.java index 1ade852c881..bec8acf6481 100644 --- a/common/src/main/java/org/keycloak/common/util/ObjectUtil.java +++ b/common/src/main/java/org/keycloak/common/util/ObjectUtil.java @@ -24,4 +24,8 @@ public class ObjectUtil { return str1.equals(str2); } + + public static String capitalize(String str) { + return str.substring(0, 1).toUpperCase() + str.substring(1); + } } diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-confirm.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-confirm.ftl index 02923d97b5a..f77eb38bfa6 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-confirm.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-confirm.ftl @@ -12,8 +12,8 @@
- - + +
diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-email.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-email.ftl index ab3e83e0918..0ba068603c5 100644 --- a/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-email.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login-idp-link-email.ftl @@ -5,10 +5,10 @@ <#elseif section = "header"> ${msg("emailLinkIdpTitle", idpAlias)} <#elseif section = "form"> -

+

${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.name)}

-

+

${msg("emailLinkIdp2")} ${msg("doClickHere")} ${msg("emailLinkIdp3")}

diff --git a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java index 3a04fbfb2e6..25b0166fd7f 100755 --- a/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java +++ b/forms/email-freemarker/src/main/java/org/keycloak/email/freemarker/FreeMarkerEmailProvider.java @@ -21,6 +21,7 @@ import javax.mail.internet.MimeMultipart; import org.jboss.logging.Logger; import org.keycloak.broker.provider.BrokeredIdentityContext; +import org.keycloak.common.util.ObjectUtil; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; import org.keycloak.email.freemarker.beans.EventBean; @@ -89,7 +90,7 @@ public class FreeMarkerEmailProvider implements EmailProvider { attributes.put("link", link); attributes.put("linkExpiration", expirationInMinutes); - String realmName = realm.getName().substring(0, 1).toUpperCase() + realm.getName().substring(1); + String realmName = ObjectUtil.capitalize(realm.getName()); attributes.put("realmName", realmName); send("passwordResetSubject", "password-reset.ftl", attributes); @@ -102,12 +103,12 @@ public class FreeMarkerEmailProvider implements EmailProvider { attributes.put("link", link); attributes.put("linkExpiration", expirationInMinutes); - String realmName = realm.getName().substring(0, 1).toUpperCase() + realm.getName().substring(1); + String realmName = ObjectUtil.capitalize(realm.getName()); attributes.put("realmName", realmName); BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT); String idpAlias = brokerContext.getIdpConfig().getAlias(); - idpAlias = idpAlias.substring(0, 1).toUpperCase() + idpAlias.substring(1); + idpAlias = ObjectUtil.capitalize(idpAlias); attributes.put("identityProviderContext", brokerContext); attributes.put("identityProviderAlias", idpAlias); @@ -123,7 +124,7 @@ public class FreeMarkerEmailProvider implements EmailProvider { attributes.put("link", link); attributes.put("linkExpiration", expirationInMinutes); - String realmName = realm.getName().substring(0, 1).toUpperCase() + realm.getName().substring(1); + String realmName = ObjectUtil.capitalize(realm.getName()); attributes.put("realmName", realmName); send("executeActionsSubject", "executeActions.ftl", attributes); @@ -137,7 +138,7 @@ public class FreeMarkerEmailProvider implements EmailProvider { attributes.put("link", link); attributes.put("linkExpiration", expirationInMinutes); - String realmName = realm.getName().substring(0, 1).toUpperCase() + realm.getName().substring(1); + String realmName = ObjectUtil.capitalize(realm.getName()); attributes.put("realmName", realmName); send("emailVerificationSubject", "email-verification.ftl", attributes); @@ -253,7 +254,7 @@ public class FreeMarkerEmailProvider implements EmailProvider { private String toCamelCase(EventType event){ StringBuilder sb = new StringBuilder("event"); for(String s : event.name().toString().toLowerCase().split("_")){ - sb.append(s.substring(0,1).toUpperCase()).append(s.substring(1)); + sb.append(ObjectUtil.capitalize(s)); } return sb.toString(); } diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java index ef8cc74cecd..d1b4df9bd65 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -22,6 +22,7 @@ import org.keycloak.OAuth2Constants; import org.keycloak.authentication.requiredactions.util.UpdateProfileContext; import org.keycloak.authentication.requiredactions.util.UserUpdateProfileContext; import org.keycloak.broker.provider.BrokeredIdentityContext; +import org.keycloak.common.util.ObjectUtil; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; import org.keycloak.freemarker.BrowserSecurityHeaderSetup; @@ -288,7 +289,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { case LOGIN_IDP_LINK_EMAIL: BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT); String idpAlias = brokerContext.getIdpConfig().getAlias(); - idpAlias = idpAlias.substring(0, 1).toUpperCase() + idpAlias.substring(1); + idpAlias = ObjectUtil.capitalize(idpAlias); attributes.put("brokerContext", brokerContext); attributes.put("idpAlias", idpAlias); @@ -470,7 +471,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { public Response createIdpLinkEmailPage() { BrokeredIdentityContext brokerContext = (BrokeredIdentityContext) this.attributes.get(IDENTITY_PROVIDER_BROKER_CONTEXT); String idpAlias = brokerContext.getIdpConfig().getAlias(); - idpAlias = idpAlias.substring(0, 1).toUpperCase() + idpAlias.substring(1); + idpAlias = ObjectUtil.capitalize(idpAlias);; setMessage(MessageType.WARNING, Messages.LINK_IDP, idpAlias); return createResponse(LoginFormsPages.LOGIN_IDP_LINK_EMAIL); diff --git a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java index a874f5eecb0..98e960cc408 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java +++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java @@ -25,8 +25,10 @@ public class DefaultAuthenticationFlows { public static final String CLIENT_AUTHENTICATION_FLOW = "clients"; public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login"; + public static final String FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW = "Handle Existing Account"; public static final String IDP_REVIEW_PROFILE_CONFIG_ALIAS = "review profile config"; + public static final String IDP_CREATE_UNIQUE_USER_CONFIG_ALIAS = "create unique user config"; public static void addFlows(RealmModel realm) { if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm); @@ -347,7 +349,7 @@ public class DefaultAuthenticationFlows { AuthenticatorConfigModel createUserIfUniqueConfig = new AuthenticatorConfigModel(); - createUserIfUniqueConfig.setAlias("create unique user config"); + createUserIfUniqueConfig.setAlias(IDP_CREATE_UNIQUE_USER_CONFIG_ALIAS); config = new HashMap<>(); config.put("require.password.update.after.registration", "false"); createUserIfUniqueConfig.setConfig(config); @@ -366,7 +368,7 @@ public class DefaultAuthenticationFlows { AuthenticationFlowModel linkExistingAccountFlow = new AuthenticationFlowModel(); linkExistingAccountFlow.setTopLevel(false); linkExistingAccountFlow.setBuiltIn(true); - linkExistingAccountFlow.setAlias("Handle Existing Account"); + linkExistingAccountFlow.setAlias(FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW); linkExistingAccountFlow.setDescription("Handle what to do if there is existing account with same email/username like authenticated identity provider"); linkExistingAccountFlow.setProviderId("basic-flow"); linkExistingAccountFlow = realm.addAuthenticationFlow(linkExistingAccountFlow); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java new file mode 100644 index 00000000000..7d780cd72f1 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java @@ -0,0 +1,397 @@ +package org.keycloak.testsuite.broker; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.mail.internet.MimeMessage; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticator; +import org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticatorFactory; +import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory; +import org.keycloak.authentication.authenticators.broker.IdpReviewProfileAuthenticatorFactory; +import org.keycloak.common.util.ObjectUtil; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.AuthenticatorConfigModel; +import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.pages.IdpConfirmLinkPage; +import org.keycloak.testsuite.pages.IdpLinkEmailPage; +import org.keycloak.testsuite.pages.LoginPasswordResetPage; +import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; +import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.WebResource; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebElement; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Marek Posolda + */ +public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProviderTest { + + protected static final String APP_REALM_ID = "realm-with-broker"; + + @WebResource + protected LoginUpdateProfileEditUsernameAllowedPage updateProfileWithUsernamePage; + + @WebResource + protected IdpConfirmLinkPage idpConfirmLinkPage; + + @WebResource + protected IdpLinkEmailPage idpLinkEmailPage; + + @WebResource + protected LoginPasswordUpdatePage passwordUpdatePage; + + + + /** + * Tests that if updateProfile is off and CreateUserIfUnique authenticator mandatory, error page will be shown if user with same email already exists + */ + @Test + public void testErrorPageWhenDuplicationNotAllowed_updateProfileOff() { + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW, IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.REQUIRED); + setUpdateProfileFirstLogin(realmWithBroker, IdentityProviderRepresentation.UPFLM_OFF); + } + + }, APP_REALM_ID); + + loginIDP("pedroigor"); + + WebElement element = this.driver.findElement(By.className("instruction")); + + assertNotNull(element); + + assertEquals("User with email psilva@redhat.com already exists. Please login to account management to link the account.", element.getText()); + + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW, IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.ALTERNATIVE); + } + + }, APP_REALM_ID); + } + + + /** + * Tests that if updateProfile is on and CreateUserIfUnique authenticator mandatory, error page will be shown if user with same email already exists + */ + @Test + public void testErrorPageWhenDuplicationNotAllowed_updateProfileOn() { + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW, IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.REQUIRED); + setUpdateProfileFirstLogin(realmWithBroker, IdentityProviderRepresentation.UPFLM_ON); + } + + }, APP_REALM_ID); + + loginIDP("test-user"); + + this.updateProfileWithUsernamePage.assertCurrent(); + this.updateProfileWithUsernamePage.update("Test", "User", "test-user@redhat.com", "pedroigor"); + + WebElement element = this.driver.findElement(By.className("instruction")); + + assertNotNull(element); + + assertEquals("User with username pedroigor already exists. Please login to account management to link the account.", element.getText()); + + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW, IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.ALTERNATIVE); + } + + }, APP_REALM_ID); + } + + + /** + * Test user registers with IdentityProvider and needs to update password when it's required by IdpCreateUserIfUniqueAuthenticator + */ + @Test + public void testRegistrationWithPasswordUpdateRequired() { + // Require updatePassword after user registered with broker + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + AuthenticatorConfigModel authenticatorConfig = realmWithBroker.getAuthenticatorConfigByAlias(DefaultAuthenticationFlows.IDP_CREATE_UNIQUE_USER_CONFIG_ALIAS); + authenticatorConfig.getConfig().put(IdpCreateUserIfUniqueAuthenticatorFactory.REQUIRE_PASSWORD_UPDATE_AFTER_REGISTRATION, "true"); + realmWithBroker.updateAuthenticatorConfig(authenticatorConfig); + + setUpdateProfileFirstLogin(realmWithBroker, IdentityProviderRepresentation.UPFLM_MISSING); + } + + }, APP_REALM_ID); + + loginIDP("pedroigor"); + this.updateProfileWithUsernamePage.assertCurrent(); + this.updateProfileWithUsernamePage.update("Test", "User", "some-user@redhat.com", "some-user"); + + // Need to update password now + this.passwordUpdatePage.assertCurrent(); + this.passwordUpdatePage.changePassword("password1", "password1"); + + + // assert authenticated + assertFederatedUser("some-user", "some-user@redhat.com", "pedroigor"); + + + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + AuthenticatorConfigModel authenticatorConfig = realmWithBroker.getAuthenticatorConfigByAlias(DefaultAuthenticationFlows.IDP_CREATE_UNIQUE_USER_CONFIG_ALIAS); + authenticatorConfig.getConfig().put(IdpCreateUserIfUniqueAuthenticatorFactory.REQUIRE_PASSWORD_UPDATE_AFTER_REGISTRATION, "false"); + realmWithBroker.updateAuthenticatorConfig(authenticatorConfig); + } + + }, APP_REALM_ID); + } + + + /** + * Tests that duplication is detected, the confirmation page is displayed, user clicks on "Review profile" and goes back to updateProfile page and resolves duplication + * by create new user + */ + @Test + public void testFixDuplicationsByReviewProfile() { + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); + + loginIDP("pedroigor"); + + // There is user with same email. Update profile to use different email + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + this.idpConfirmLinkPage.clickReviewProfile(); + + this.updateProfileWithUsernamePage.assertCurrent(); + this.updateProfileWithUsernamePage.update("Test", "User", "testing-user@redhat.com", "pedroigor"); + + // There is user with same username. Update profile to use different username + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with username pedroigor already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + this.idpConfirmLinkPage.clickReviewProfile(); + + this.updateProfileWithUsernamePage.assertCurrent(); + this.updateProfileWithUsernamePage.update("Test", "User", "testing-user@redhat.com", "testing-user"); + + assertFederatedUser("testing-user", "testing-user@redhat.com", "pedroigor"); + } + + /** + * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by email + */ + @Test + public void testLinkAccountByEmailVerification() throws Exception { + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); + + loginIDP("pedroigor"); + + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + this.idpConfirmLinkPage.clickLinkAccount(); + + // Confirm linking account by email + this.idpLinkEmailPage.assertCurrent(); + Assert.assertEquals("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.", this.idpLinkEmailPage.getMessage()); + + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + MimeMessage message = greenMail.getReceivedMessages()[0]; + String linkFromMail = getVerificationEmailLink(message); + + driver.navigate().to(linkFromMail.trim()); + + // authenticated and redirected to app. User is linked with identity provider + assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor"); + } + + + /** + * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by reauthentication (confirm password on login screen) + */ + @Test + public void testLinkAccountByReauthenticationWithPassword() throws Exception { + // Remove smtp config. The reauthentication by username+password screen will be automatically used then + final Map smtpConfig = new HashMap<>(); + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + setUpdateProfileFirstLogin(realmWithBroker, IdentityProviderRepresentation.UPFLM_OFF); + smtpConfig.putAll(realmWithBroker.getSmtpConfig()); + realmWithBroker.setSmtpConfig(Collections.emptyMap()); + } + + }, APP_REALM_ID); + + + loginIDP("pedroigor"); + + + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + this.idpConfirmLinkPage.clickLinkAccount(); + + // Login screen shown. Username is prefilled and disabled. Registration link and social buttons are not shown + Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle()); + Assert.assertEquals("pedroigor", this.loginPage.getUsername()); + Assert.assertFalse(this.loginPage.isUsernameInputEnabled()); + + Assert.assertEquals("Authenticate as pedroigor to link your account with " + getProviderId(), this.loginPage.getSuccessMessage()); + + try { + this.loginPage.findSocialButton(getProviderId()); + Assert.fail("Not expected to see social button with " + getProviderId()); + } catch (NoSuchElementException expected) { + } + + try { + this.loginPage.clickRegister(); + Assert.fail("Not expected to see register link"); + } catch (NoSuchElementException expected) { + } + + // Use bad password first + this.loginPage.login("password1"); + Assert.assertEquals("Invalid username or password.", this.loginPage.getError()); + + // Use correct password now + this.loginPage.login("password"); + + // authenticated and redirected to app. User is linked with identity provider + assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor"); + + + // Restore smtp config + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + realmWithBroker.setSmtpConfig(smtpConfig); + } + + }, APP_REALM_ID); + } + + + /** + * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by reauthentication (confirm password on login screen) + * and additionally he goes through "forget password" + */ + @Test + public void testLinkAccountByReauthentication_forgetPassword() throws Exception { + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW, + IdpEmailVerificationAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.DISABLED); + + setUpdateProfileFirstLogin(realmWithBroker, IdentityProviderRepresentation.UPFLM_OFF); + } + + }, APP_REALM_ID); + + loginIDP("pedroigor"); + + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + this.idpConfirmLinkPage.clickLinkAccount(); + + // Click "Forget password" on login page. Email sent directly because username is known + Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle()); + this.loginPage.resetPassword(); + + Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle()); + Assert.assertEquals("You should receive an email shortly with further instructions.", this.loginPage.getSuccessMessage()); + + // Click on link from email + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + MimeMessage message = greenMail.getReceivedMessages()[0]; + String linkFromMail = getVerificationEmailLink(message); + + driver.navigate().to(linkFromMail.trim()); + + // Need to update password now + this.passwordUpdatePage.assertCurrent(); + this.passwordUpdatePage.changePassword("password", "password"); + + // authenticated and redirected to app. User is linked with identity provider + assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor"); + + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW, + IdpEmailVerificationAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.ALTERNATIVE); + + } + + }, APP_REALM_ID); + } + + + protected void assertFederatedUser(String expectedUsername, String expectedEmail, String expectedFederatedUsername) { + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); + UserModel federatedUser = getFederatedUser(); + + assertNotNull(federatedUser); + assertEquals(expectedUsername, federatedUser.getUsername()); + assertEquals(expectedEmail, federatedUser.getEmail()); + + RealmModel realmWithBroker = getRealm(); + Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realmWithBroker); + assertEquals(1, federatedIdentities.size()); + + FederatedIdentityModel federatedIdentityModel = federatedIdentities.iterator().next(); + + assertEquals(getProviderId(), federatedIdentityModel.getIdentityProvider()); + assertEquals(expectedFederatedUsername, federatedIdentityModel.getUserName()); + } + + + protected void setExecutionRequirement(RealmModel realmWithBroker, String flowAlias, String authenticatorProvider, AuthenticationExecutionModel.Requirement requirement) { + AuthenticationFlowModel flowModel = realmWithBroker.getFlowByAlias(flowAlias); + List authExecutions = realmWithBroker.getAuthenticationExecutions(flowModel.getId()); + for (AuthenticationExecutionModel execution : authExecutions) { + if (execution.getAuthenticator().equals(authenticatorProvider)) { + execution.setRequirement(requirement); + realmWithBroker.updateAuthenticatorExecution(execution); + return; + } + } + + throw new IllegalStateException("Execution not found for flow " + flowAlias + " and authenticator " + authenticatorProvider); + } + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index 8af43b2b63f..5248c38f5e4 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -42,6 +42,7 @@ import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.services.Urls; import org.keycloak.testsuite.MailUtil; import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.broker.util.UserSessionStatusServlet; import org.keycloak.testsuite.broker.util.UserSessionStatusServlet.UserSessionStatus; import org.keycloak.testsuite.pages.AccountFederatedIdentityPage; import org.keycloak.testsuite.pages.AccountPasswordPage; @@ -87,7 +88,7 @@ import static org.junit.Assert.fail; */ public abstract class AbstractIdentityProviderTest { - private static final URI BASE_URI = UriBuilder.fromUri("http://localhost:8081/auth").build(); + protected static final URI BASE_URI = UriBuilder.fromUri("http://localhost:8081/auth").build(); @ClassRule public static BrokerKeyCloakRule brokerServerRule = new BrokerKeyCloakRule(); @@ -99,10 +100,10 @@ public abstract class AbstractIdentityProviderTest { protected WebDriver driver; @WebResource - private LoginPage loginPage; + protected LoginPage loginPage; @WebResource - private LoginUpdateProfilePage updateProfilePage; + protected LoginUpdateProfilePage updateProfilePage; @WebResource @@ -123,7 +124,7 @@ public abstract class AbstractIdentityProviderTest { @WebResource protected AccountFederatedIdentityPage accountFederatedIdentityPage; - private KeycloakSession session; + protected KeycloakSession session; @Before public void onBefore() { @@ -140,552 +141,7 @@ public abstract class AbstractIdentityProviderTest { brokerServerRule.stopSession(this.session, true); } - @Test - public void testSuccessfulAuthentication() { - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_ON); - - UserModel user = assertSuccessfulAuthentication(identityProviderModel, "test-user", "new@email.com", true); - Assert.assertEquals("617-666-7777", user.getFirstAttribute("mobile")); - } - - @Test - public void testSuccessfulAuthenticationUpdateProfileOnMissing_nothingMissing() { - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_MISSING); - - assertSuccessfulAuthentication(identityProviderModel, "test-user", "test-user@localhost", false); - } - - @Test - public void testSuccessfulAuthenticationUpdateProfileOnMissing_missingEmail() { - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_MISSING); - - assertSuccessfulAuthentication(identityProviderModel, "test-user-noemail", "new@email.com", true); - } - - @Test - public void testSuccessfulAuthenticationWithoutUpdateProfile() { - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); - - assertSuccessfulAuthentication(identityProviderModel, "test-user", "test-user@localhost", false); - } - - /** - * Test that verify email action is performed if email is provided and email trust is not enabled for the provider - * - * @throws MessagingException - * @throws IOException - */ - @Test - public void testSuccessfulAuthenticationWithoutUpdateProfile_emailProvided_emailVerifyEnabled() throws IOException, MessagingException { - getRealm().setVerifyEmail(true); - brokerServerRule.stopSession(this.session, true); - this.session = brokerServerRule.startSession(); - - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - try { - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); - identityProviderModel.setTrustEmail(false); - - UserModel federatedUser = assertSuccessfulAuthenticationWithEmailVerification(identityProviderModel, "test-user", "test-user@localhost", false); - - // email is verified now - assertFalse(federatedUser.getRequiredActions().contains(RequiredAction.VERIFY_EMAIL.name())); - - } finally { - getRealm().setVerifyEmail(false); - } - } - - private UserModel assertSuccessfulAuthenticationWithEmailVerification(IdentityProviderModel identityProviderModel, String username, String expectedEmail, - boolean isProfileUpdateExpected) - throws IOException, MessagingException { - authenticateWithIdentityProvider(identityProviderModel, username, isProfileUpdateExpected); - - // verify email is sent - Assert.assertTrue(verifyEmailPage.isCurrent()); - - // read email to take verification link from - Assert.assertEquals(1, greenMail.getReceivedMessages().length); - MimeMessage message = greenMail.getReceivedMessages()[0]; - String verificationUrl = getVerificationEmailLink(message); - - driver.navigate().to(verificationUrl.trim()); - - // authenticated and redirected to app - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); - - UserModel federatedUser = getFederatedUser(); - - assertNotNull(federatedUser); - - doAssertFederatedUser(federatedUser, identityProviderModel, expectedEmail, isProfileUpdateExpected); - - brokerServerRule.stopSession(session, true); - session = brokerServerRule.startSession(); - - RealmModel realm = getRealm(); - - Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realm); - - assertEquals(1, federatedIdentities.size()); - - FederatedIdentityModel federatedIdentityModel = federatedIdentities.iterator().next(); - - assertEquals(getProviderId(), federatedIdentityModel.getIdentityProvider()); - assertEquals(federatedUser.getUsername(), federatedIdentityModel.getIdentityProvider() + "." + federatedIdentityModel.getUserName()); - - driver.navigate().to("http://localhost:8081/test-app/logout"); - driver.navigate().to("http://localhost:8081/test-app"); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); - return federatedUser; - } - - /** - * Test for KEYCLOAK-1053 - verify email action is not performed if email is not provided, login is normal, but action stays in set to be performed later - */ - @Test - public void testSuccessfulAuthenticationWithoutUpdateProfile_emailNotProvided_emailVerifyEnabled() { - getRealm().setVerifyEmail(true); - brokerServerRule.stopSession(this.session, true); - this.session = brokerServerRule.startSession(); - - try { - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); - - UserModel federatedUser = assertSuccessfulAuthentication(identityProviderModel, "test-user-noemail", null, false); - - assertTrue(federatedUser.getRequiredActions().contains(RequiredAction.VERIFY_EMAIL.name())); - - } finally { - getRealm().setVerifyEmail(false); - } - } - - /** - * Test for KEYCLOAK-1372 - verify email action is not performed if email is provided but email trust is enabled for the provider - */ - @Test - public void testSuccessfulAuthenticationWithoutUpdateProfile_emailProvided_emailVerifyEnabled_emailTrustEnabled() { - getRealm().setVerifyEmail(true); - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); - brokerServerRule.stopSession(this.session, true); - this.session = brokerServerRule.startSession(); - - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - try { - identityProviderModel.setTrustEmail(true); - - UserModel federatedUser = assertSuccessfulAuthentication(identityProviderModel, "test-user", "test-user@localhost", false); - - assertFalse(federatedUser.getRequiredActions().contains(RequiredAction.VERIFY_EMAIL.name())); - - } finally { - identityProviderModel.setTrustEmail(false); - getRealm().setVerifyEmail(false); - } - } - - /** - * Test for KEYCLOAK-1372 - verify email action is performed if email is provided and email trust is enabled for the provider, but email is changed on First login update profile page - * - * @throws MessagingException - * @throws IOException - */ - @Test - public void testSuccessfulAuthentication_emailTrustEnabled_emailVerifyEnabled_emailUpdatedOnFirstLogin() throws IOException, MessagingException { - getRealm().setVerifyEmail(true); - brokerServerRule.stopSession(this.session, true); - this.session = brokerServerRule.startSession(); - - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - try { - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_ON); - identityProviderModel.setTrustEmail(true); - - UserModel user = assertSuccessfulAuthenticationWithEmailVerification(identityProviderModel, "test-user", "new@email.com", true); - Assert.assertEquals("617-666-7777", user.getFirstAttribute("mobile")); - } finally { - identityProviderModel.setTrustEmail(false); - getRealm().setVerifyEmail(false); - } - } - - @Test - public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() { - - getRealm().setRegistrationEmailAsUsername(true); - brokerServerRule.stopSession(this.session, true); - this.session = brokerServerRule.startSession(); - - try { - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); - - authenticateWithIdentityProvider(identityProviderModel, "test-user", false); - - // authenticated and redirected to app - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); - - brokerServerRule.stopSession(session, true); - session = brokerServerRule.startSession(); - - // check correct user is created with email as username and bound to correct federated identity - RealmModel realm = getRealm(); - - UserModel federatedUser = session.users().getUserByUsername("test-user@localhost", realm); - - assertNotNull(federatedUser); - - assertEquals("test-user@localhost", federatedUser.getUsername()); - - doAssertFederatedUser(federatedUser, identityProviderModel, "test-user@localhost", false); - - Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realm); - - assertEquals(1, federatedIdentities.size()); - - FederatedIdentityModel federatedIdentityModel = federatedIdentities.iterator().next(); - - assertEquals(getProviderId(), federatedIdentityModel.getIdentityProvider()); - - driver.navigate().to("http://localhost:8081/test-app/logout"); - driver.navigate().to("http://localhost:8081/test-app"); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); - - } finally { - getRealm().setRegistrationEmailAsUsername(false); - } - } - - @Test - public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername_emailNotProvided() { - - getRealm().setRegistrationEmailAsUsername(true); - brokerServerRule.stopSession(this.session, true); - this.session = brokerServerRule.startSession(); - - try { - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); - - authenticateWithIdentityProvider(identityProviderModel, "test-user-noemail", false); - - brokerServerRule.stopSession(session, true); - session = brokerServerRule.startSession(); - - // check correct user is created with username from provider as email is not available - RealmModel realm = getRealm(); - UserModel federatedUser = getFederatedUser(); - assertNotNull(federatedUser); - - doAssertFederatedUserNoEmail(federatedUser); - - Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realm); - - assertEquals(1, federatedIdentities.size()); - - FederatedIdentityModel federatedIdentityModel = federatedIdentities.iterator().next(); - - assertEquals(getProviderId(), federatedIdentityModel.getIdentityProvider()); - revokeGrant(); - - driver.navigate().to("http://localhost:8081/test-app/logout"); - driver.navigate().to("http://localhost:8081/test-app"); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); - - } finally { - getRealm().setRegistrationEmailAsUsername(false); - } - } - - protected void doAssertFederatedUserNoEmail(UserModel federatedUser) { - assertEquals("kc-oidc-idp.test-user-noemail", federatedUser.getUsername()); - assertEquals(null, federatedUser.getEmail()); - assertEquals("Test", federatedUser.getFirstName()); - assertEquals("User", federatedUser.getLastName()); - } - - @Test - public void testDisabled() { - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - - identityProviderModel.setEnabled(false); - - this.driver.navigate().to("http://localhost:8081/test-app/"); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); - - try { - this.driver.findElement(By.className(getProviderId())); - fail("Provider [" + getProviderId() + "] not disabled."); - } catch (NoSuchElementException nsee) { - - } - } - - @Test - public void testProviderOnLoginPage() { - // Provider button is available on login page - this.driver.navigate().to("http://localhost:8081/test-app/"); - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); - loginPage.findSocialButton(getProviderId()); - } - - // TODO: Reenable and adjust to KEYCLOAK-1750 changed behaviour - // @Test - public void testUserAlreadyExistsWhenUpdatingProfile() { - this.driver.navigate().to("http://localhost:8081/test-app/"); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); - - // choose the identity provider - this.loginPage.clickSocial(getProviderId()); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/")); - - // log in to identity provider - this.loginPage.login("test-user", "password"); - - doAfterProviderAuthentication(); - - this.updateProfilePage.assertCurrent(); - this.updateProfilePage.update("Test", "User", "psilva@redhat.com"); - - WebElement element = this.driver.findElement(By.className("kc-feedback-text")); - - assertNotNull(element); - - assertEquals("Email already exists.", element.getText()); - - this.updateProfilePage.assertCurrent(); - this.updateProfilePage.update("Test", "User", "test-user@redhat.com"); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); - - UserModel federatedUser = getFederatedUser(); - - assertNotNull(federatedUser); - } - - // TODO: Reenable and adjust to KEYCLOAK-1750 changed behaviour - // @Test - public void testUserAlreadyExistsWhenNotUpdatingProfile() { - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); - - this.driver.navigate().to("http://localhost:8081/test-app/"); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); - - // choose the identity provider - this.loginPage.clickSocial(getProviderId()); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/")); - - // log in to identity provider - this.loginPage.login("pedroigor", "password"); - - doAfterProviderAuthentication(); - - WebElement element = this.driver.findElement(By.className("kc-feedback-text")); - - assertNotNull(element); - - assertEquals("User with email already exists. Please login to account management to link the account.", element.getText()); - } - - @Test - public void testAccountManagementLinkIdentity() { - // 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 - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_ON); - accountFederatedIdentityPage.clickAddProvider(identityProviderModel.getAlias()); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/")); - this.loginPage.login("test-user", "password"); - doAfterProviderAuthentication(); - - // Assert identity linked in account management - assertTrue(accountFederatedIdentityPage.isCurrent()); - assertTrue(driver.getPageSource().contains("id=\"remove-" + identityProviderModel.getAlias() + "\"")); - - // Revoke grant in account mgmt - revokeGrant(); - - // Logout from account management - accountFederatedIdentityPage.logout(); - assertTrue(driver.getTitle().equals("Log in to realm-with-broker")); - - // Assert I am logged immediately to account management due to previously linked "test-user" identity - loginPage.clickSocial(identityProviderModel.getAlias()); - doAfterProviderAuthentication(); - assertTrue(accountFederatedIdentityPage.isCurrent()); - assertTrue(driver.getPageSource().contains("id=\"remove-" + identityProviderModel.getAlias() + "\"")); - - // Unlink my "test-user" - accountFederatedIdentityPage.clickRemoveProvider(identityProviderModel.getAlias()); - assertTrue(driver.getPageSource().contains("id=\"add-" + identityProviderModel.getAlias() + "\"")); - - // Revoke grant in account mgmt - revokeGrant(); - - // Logout from account management - System.out.println("*** logout from account management"); - accountFederatedIdentityPage.logout(); - assertTrue(driver.getTitle().equals("Log in to realm-with-broker")); - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); - - // Try to login. Previous link is not valid anymore, so now it should try to register new user - this.loginPage.clickSocial(identityProviderModel.getAlias()); - this.loginPage.login("test-user", "password"); - doAfterProviderAuthentication(); - this.updateProfilePage.assertCurrent(); - } - - @Test(expected = NoSuchElementException.class) - public void testIdentityProviderNotAllowed() { - this.driver.navigate().to("http://localhost:8081/test-app/"); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); - - driver.findElement(By.className("model-oidc-idp")); - } - - protected void configureClientRetrieveToken(String clientId) { - RealmModel realm = getRealm(); - RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE); - ClientModel client = realm.getClientByClientId(clientId); - if (!client.hasScope(readTokenRole)) client.addScopeMapping(readTokenRole); - - brokerServerRule.stopSession(session, true); - session = brokerServerRule.startSession(); - - } - - protected void configureUserRetrieveToken(String username) { - RealmModel realm = getRealm(); - UserModel user = session.users().getUserByUsername(username, realm); - RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE); - if (user != null && !user.hasRole(readTokenRole)) { - user.grantRole(readTokenRole); - } - brokerServerRule.stopSession(session, true); - session = brokerServerRule.startSession(); - - } - - protected void unconfigureClientRetrieveToken(String clientId) { - RealmModel realm = getRealm(); - RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE); - ClientModel client = realm.getClientByClientId(clientId); - if (client.hasScope(readTokenRole)) client.deleteScopeMapping(readTokenRole); - - brokerServerRule.stopSession(session, true); - session = brokerServerRule.startSession(); - - } - - protected void unconfigureUserRetrieveToken(String username) { - RealmModel realm = getRealm(); - UserModel user = session.users().getUserByUsername(username, realm); - RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE); - if (user != null && user.hasRole(readTokenRole)) { - user.deleteRoleMapping(readTokenRole); - } - brokerServerRule.stopSession(session, true); - session = brokerServerRule.startSession(); - - } - - @Test - public void testTokenStorageAndRetrievalByApplication() { - setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_ON); - IdentityProviderModel identityProviderModel = getIdentityProviderModel(); - - identityProviderModel.setStoreToken(true); - - authenticateWithIdentityProvider(identityProviderModel, "test-user", true); - - UserModel federatedUser = getFederatedUser(); - RealmModel realm = getRealm(); - Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realm); - - assertFalse(federatedIdentities.isEmpty()); - assertEquals(1, federatedIdentities.size()); - - FederatedIdentityModel identityModel = federatedIdentities.iterator().next(); - - assertNotNull(identityModel.getToken()); - - UserSessionStatus userSessionStatus = retrieveSessionStatus(); - String accessToken = userSessionStatus.getAccessTokenString(); - URI tokenEndpointUrl = Urls.identityProviderRetrieveToken(BASE_URI, getProviderId(), realm.getName()); - final String authHeader = "Bearer " + accessToken; - ClientRequestFilter authFilter = new ClientRequestFilter() { - @Override - public void filter(ClientRequestContext requestContext) throws IOException { - requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader); - } - }; - Client client = ClientBuilder.newBuilder().register(authFilter).build(); - WebTarget tokenEndpoint = client.target(tokenEndpointUrl); - Response response = tokenEndpoint.request().get(); - assertEquals(Status.OK.getStatusCode(), response.getStatus()); - assertNotNull(response.readEntity(String.class)); - revokeGrant(); - - - driver.navigate().to("http://localhost:8081/test-app/logout"); - String currentUrl = this.driver.getCurrentUrl(); - System.out.println("after logout currentUrl: " + currentUrl); - assertTrue(currentUrl.startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); - - unconfigureUserRetrieveToken(getProviderId() + ".test-user"); - loginIDP("test-user"); - //authenticateWithIdentityProvider(identityProviderModel, "test-user"); - assertEquals("http://localhost:8081/test-app", driver.getCurrentUrl()); - - userSessionStatus = retrieveSessionStatus(); - accessToken = userSessionStatus.getAccessTokenString(); - final String authHeader2 = "Bearer " + accessToken; - ClientRequestFilter authFilter2 = new ClientRequestFilter() { - @Override - public void filter(ClientRequestContext requestContext) throws IOException { - requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader2); - } - }; - client = ClientBuilder.newBuilder().register(authFilter2).build(); - tokenEndpoint = client.target(tokenEndpointUrl); - response = tokenEndpoint.request().get(); - - assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus()); - - revokeGrant(); - driver.navigate().to("http://localhost:8081/test-app/logout"); - driver.navigate().to("http://localhost:8081/test-app"); - - assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); - } - - protected abstract void doAssertTokenRetrieval(String pageSource); - - private UserModel assertSuccessfulAuthentication(IdentityProviderModel identityProviderModel, String username, String expectedEmail, boolean isProfileUpdateExpected) { + protected UserModel assertSuccessfulAuthentication(IdentityProviderModel identityProviderModel, String username, String expectedEmail, boolean isProfileUpdateExpected) { authenticateWithIdentityProvider(identityProviderModel, username, isProfileUpdateExpected); // authenticated and redirected to app @@ -722,7 +178,16 @@ public abstract class AbstractIdentityProviderTest { return federatedUser; } - private void authenticateWithIdentityProvider(IdentityProviderModel identityProviderModel, String username, boolean isProfileUpdateExpected) { + + + protected void doAssertFederatedUserNoEmail(UserModel federatedUser) { + assertEquals("kc-oidc-idp.test-user-noemail", federatedUser.getUsername()); + assertEquals(null, federatedUser.getEmail()); + assertEquals("Test", federatedUser.getFirstName()); + assertEquals("User", federatedUser.getLastName()); + } + + protected void authenticateWithIdentityProvider(IdentityProviderModel identityProviderModel, String username, boolean isProfileUpdateExpected) { loginIDP(username); @@ -738,7 +203,7 @@ public abstract class AbstractIdentityProviderTest { } - private void loginIDP(String username) { + protected void loginIDP(String username) { driver.navigate().to("http://localhost:8081/test-app"); assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); @@ -776,6 +241,7 @@ public abstract class AbstractIdentityProviderTest { protected abstract String getProviderId(); + protected IdentityProviderModel getIdentityProviderModel() { IdentityProviderModel identityProviderModel = getRealm().getIdentityProviderByAlias(getProviderId()); @@ -786,10 +252,16 @@ public abstract class AbstractIdentityProviderTest { return identityProviderModel; } - private RealmModel getRealm() { - return this.session.realms().getRealm("realm-with-broker"); + + protected RealmModel getRealm() { + return getRealm(this.session); } + protected RealmModel getRealm(KeycloakSession session) { + return session.realms().getRealm("realm-with-broker"); + } + + protected void doAssertFederatedUser(UserModel federatedUser, IdentityProviderModel identityProviderModel, String expectedEmail, boolean isProfileUpdateExpected) { if (isProfileUpdateExpected) { String userFirstName = "New first"; @@ -805,19 +277,6 @@ public abstract class AbstractIdentityProviderTest { } } - private UserSessionStatus retrieveSessionStatus() { - UserSessionStatus sessionStatus = null; - - try { - String pageSource = this.driver.getPageSource(); - - sessionStatus = JsonSerialization.readValue(pageSource.getBytes(), UserSessionStatus.class); - } catch (IOException ignore) { - ignore.printStackTrace(); - } - - return sessionStatus; - } private void removeTestUsers() { RealmModel realm = getRealm(); @@ -835,40 +294,60 @@ public abstract class AbstractIdentityProviderTest { } } } - - private String getVerificationEmailLink(MimeMessage message) throws IOException, MessagingException { - Multipart multipart = (Multipart) message.getContent(); - - final String textContentType = multipart.getBodyPart(0).getContentType(); - - assertEquals("text/plain; charset=UTF-8", textContentType); - - final String textBody = (String) multipart.getBodyPart(0).getContent(); - final String textVerificationUrl = MailUtil.getLink(textBody); - - final String htmlContentType = multipart.getBodyPart(1).getContentType(); - - assertEquals("text/html; charset=UTF-8", htmlContentType); - - final String htmlBody = (String) multipart.getBodyPart(1).getContent(); - final String htmlVerificationUrl = MailUtil.getLink(htmlBody); - - assertEquals(htmlVerificationUrl, textVerificationUrl); - return htmlVerificationUrl; - } - private void setUpdateProfileFirstLogin(final String updateProfileFirstLogin) { + protected void setUpdateProfileFirstLogin(final String updateProfileFirstLogin) { KeycloakModelUtils.runJobInTransaction(this.session.getKeycloakSessionFactory(), new KeycloakSessionTask() { @Override public void run(KeycloakSession session) { - RealmModel realm = session.realms().getRealm("realm-with-broker"); - AuthenticatorConfigModel reviewProfileConfig = realm.getAuthenticatorConfigByAlias(DefaultAuthenticationFlows.IDP_REVIEW_PROFILE_CONFIG_ALIAS); - reviewProfileConfig.getConfig().put(IdpReviewProfileAuthenticatorFactory.UPDATE_PROFILE_ON_FIRST_LOGIN, updateProfileFirstLogin); - realm.updateAuthenticatorConfig(reviewProfileConfig); + RealmModel realm = getRealm(session); + setUpdateProfileFirstLogin(realm, updateProfileFirstLogin); } }); } + + protected void setUpdateProfileFirstLogin(RealmModel realm, String updateProfileFirstLogin) { + AuthenticatorConfigModel reviewProfileConfig = realm.getAuthenticatorConfigByAlias(DefaultAuthenticationFlows.IDP_REVIEW_PROFILE_CONFIG_ALIAS); + reviewProfileConfig.getConfig().put(IdpReviewProfileAuthenticatorFactory.UPDATE_PROFILE_ON_FIRST_LOGIN, updateProfileFirstLogin); + realm.updateAuthenticatorConfig(reviewProfileConfig); + } + + + protected UserSessionStatusServlet.UserSessionStatus retrieveSessionStatus() { + UserSessionStatusServlet.UserSessionStatus sessionStatus = null; + + try { + String pageSource = this.driver.getPageSource(); + + sessionStatus = JsonSerialization.readValue(pageSource.getBytes(), UserSessionStatusServlet.UserSessionStatus.class); + } catch (IOException ignore) { + ignore.printStackTrace(); + } + + return sessionStatus; + } + + protected String getVerificationEmailLink(MimeMessage message) throws IOException, MessagingException { + Multipart multipart = (Multipart) message.getContent(); + + final String textContentType = multipart.getBodyPart(0).getContentType(); + + assertEquals("text/plain; charset=UTF-8", textContentType); + + final String textBody = (String) multipart.getBodyPart(0).getContent(); + final String textVerificationUrl = MailUtil.getLink(textBody); + + final String htmlContentType = multipart.getBodyPart(1).getContentType(); + + assertEquals("text/html; charset=UTF-8", htmlContentType); + + final String htmlBody = (String) multipart.getBodyPart(1).getContent(); + final String htmlVerificationUrl = MailUtil.getLink(htmlBody); + + assertEquals(htmlVerificationUrl, textVerificationUrl); + + return htmlVerificationUrl; + } } 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 new file mode 100644 index 00000000000..904caf62824 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractKeycloakIdentityProviderTest.java @@ -0,0 +1,519 @@ +package org.keycloak.testsuite.broker; + +import java.io.IOException; +import java.net.URI; +import java.util.Set; + +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.internet.MimeMessage; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import org.junit.Assert; +import org.junit.Test; +import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; +import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.services.Urls; +import org.keycloak.testsuite.MailUtil; +import org.keycloak.testsuite.broker.util.UserSessionStatusServlet; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * @author pedroigor + */ +public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdentityProviderTest { + + @Test + public void testSuccessfulAuthentication() { + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_ON); + + UserModel user = assertSuccessfulAuthentication(identityProviderModel, "test-user", "new@email.com", true); + Assert.assertEquals("617-666-7777", user.getFirstAttribute("mobile")); + } + + @Test + public void testSuccessfulAuthenticationUpdateProfileOnMissing_nothingMissing() { + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_MISSING); + + assertSuccessfulAuthentication(identityProviderModel, "test-user", "test-user@localhost", false); + } + + @Test + public void testSuccessfulAuthenticationUpdateProfileOnMissing_missingEmail() { + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_MISSING); + + assertSuccessfulAuthentication(identityProviderModel, "test-user-noemail", "new@email.com", true); + } + + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile() { + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); + + assertSuccessfulAuthentication(identityProviderModel, "test-user", "test-user@localhost", false); + } + + /** + * Test that verify email action is performed if email is provided and email trust is not enabled for the provider + * + * @throws MessagingException + * @throws IOException + */ + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile_emailProvided_emailVerifyEnabled() throws IOException, MessagingException { + getRealm().setVerifyEmail(true); + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + try { + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); + identityProviderModel.setTrustEmail(false); + + UserModel federatedUser = assertSuccessfulAuthenticationWithEmailVerification(identityProviderModel, "test-user", "test-user@localhost", false); + + // email is verified now + assertFalse(federatedUser.getRequiredActions().contains(UserModel.RequiredAction.VERIFY_EMAIL.name())); + + } finally { + getRealm().setVerifyEmail(false); + } + } + + private UserModel assertSuccessfulAuthenticationWithEmailVerification(IdentityProviderModel identityProviderModel, String username, String expectedEmail, + boolean isProfileUpdateExpected) + throws IOException, MessagingException { + authenticateWithIdentityProvider(identityProviderModel, username, isProfileUpdateExpected); + + // verify email is sent + Assert.assertTrue(verifyEmailPage.isCurrent()); + + // read email to take verification link from + Assert.assertEquals(1, greenMail.getReceivedMessages().length); + MimeMessage message = greenMail.getReceivedMessages()[0]; + String verificationUrl = getVerificationEmailLink(message); + + driver.navigate().to(verificationUrl.trim()); + + // authenticated and redirected to app + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); + + UserModel federatedUser = getFederatedUser(); + + assertNotNull(federatedUser); + + doAssertFederatedUser(federatedUser, identityProviderModel, expectedEmail, isProfileUpdateExpected); + + brokerServerRule.stopSession(session, true); + session = brokerServerRule.startSession(); + + RealmModel realm = getRealm(); + + Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realm); + + assertEquals(1, federatedIdentities.size()); + + FederatedIdentityModel federatedIdentityModel = federatedIdentities.iterator().next(); + + assertEquals(getProviderId(), federatedIdentityModel.getIdentityProvider()); + assertEquals(federatedUser.getUsername(), federatedIdentityModel.getIdentityProvider() + "." + federatedIdentityModel.getUserName()); + + driver.navigate().to("http://localhost:8081/test-app/logout"); + driver.navigate().to("http://localhost:8081/test-app"); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); + return federatedUser; + } + + /** + * Test for KEYCLOAK-1053 - verify email action is not performed if email is not provided, login is normal, but action stays in set to be performed later + */ + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile_emailNotProvided_emailVerifyEnabled() { + getRealm().setVerifyEmail(true); + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + try { + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); + + UserModel federatedUser = assertSuccessfulAuthentication(identityProviderModel, "test-user-noemail", null, false); + + assertTrue(federatedUser.getRequiredActions().contains(UserModel.RequiredAction.VERIFY_EMAIL.name())); + + } finally { + getRealm().setVerifyEmail(false); + } + } + + /** + * Test for KEYCLOAK-1372 - verify email action is not performed if email is provided but email trust is enabled for the provider + */ + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile_emailProvided_emailVerifyEnabled_emailTrustEnabled() { + getRealm().setVerifyEmail(true); + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + try { + identityProviderModel.setTrustEmail(true); + + UserModel federatedUser = assertSuccessfulAuthentication(identityProviderModel, "test-user", "test-user@localhost", false); + + assertFalse(federatedUser.getRequiredActions().contains(UserModel.RequiredAction.VERIFY_EMAIL.name())); + + } finally { + identityProviderModel.setTrustEmail(false); + getRealm().setVerifyEmail(false); + } + } + + /** + * Test for KEYCLOAK-1372 - verify email action is performed if email is provided and email trust is enabled for the provider, but email is changed on First login update profile page + * + * @throws MessagingException + * @throws IOException + */ + @Test + public void testSuccessfulAuthentication_emailTrustEnabled_emailVerifyEnabled_emailUpdatedOnFirstLogin() throws IOException, MessagingException { + getRealm().setVerifyEmail(true); + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + try { + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_ON); + identityProviderModel.setTrustEmail(true); + + UserModel user = assertSuccessfulAuthenticationWithEmailVerification(identityProviderModel, "test-user", "new@email.com", true); + Assert.assertEquals("617-666-7777", user.getFirstAttribute("mobile")); + } finally { + identityProviderModel.setTrustEmail(false); + getRealm().setVerifyEmail(false); + } + } + + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() { + + getRealm().setRegistrationEmailAsUsername(true); + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + try { + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); + + authenticateWithIdentityProvider(identityProviderModel, "test-user", false); + + // authenticated and redirected to app + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); + + brokerServerRule.stopSession(session, true); + session = brokerServerRule.startSession(); + + // check correct user is created with email as username and bound to correct federated identity + RealmModel realm = getRealm(); + + UserModel federatedUser = session.users().getUserByUsername("test-user@localhost", realm); + + assertNotNull(federatedUser); + + assertEquals("test-user@localhost", federatedUser.getUsername()); + + doAssertFederatedUser(federatedUser, identityProviderModel, "test-user@localhost", false); + + Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realm); + + assertEquals(1, federatedIdentities.size()); + + FederatedIdentityModel federatedIdentityModel = federatedIdentities.iterator().next(); + + assertEquals(getProviderId(), federatedIdentityModel.getIdentityProvider()); + + driver.navigate().to("http://localhost:8081/test-app/logout"); + driver.navigate().to("http://localhost:8081/test-app"); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); + + } finally { + getRealm().setRegistrationEmailAsUsername(false); + } + } + + @Test + public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername_emailNotProvided() { + + getRealm().setRegistrationEmailAsUsername(true); + brokerServerRule.stopSession(this.session, true); + this.session = brokerServerRule.startSession(); + + try { + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF); + + authenticateWithIdentityProvider(identityProviderModel, "test-user-noemail", false); + + brokerServerRule.stopSession(session, true); + session = brokerServerRule.startSession(); + + // check correct user is created with username from provider as email is not available + RealmModel realm = getRealm(); + UserModel federatedUser = getFederatedUser(); + assertNotNull(federatedUser); + + doAssertFederatedUserNoEmail(federatedUser); + + Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realm); + + assertEquals(1, federatedIdentities.size()); + + FederatedIdentityModel federatedIdentityModel = federatedIdentities.iterator().next(); + + assertEquals(getProviderId(), federatedIdentityModel.getIdentityProvider()); + revokeGrant(); + + driver.navigate().to("http://localhost:8081/test-app/logout"); + driver.navigate().to("http://localhost:8081/test-app"); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); + + } finally { + getRealm().setRegistrationEmailAsUsername(false); + } + } + + @Test + public void testDisabled() { + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + + identityProviderModel.setEnabled(false); + + this.driver.navigate().to("http://localhost:8081/test-app/"); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); + + try { + this.driver.findElement(By.className(getProviderId())); + fail("Provider [" + getProviderId() + "] not disabled."); + } catch (NoSuchElementException nsee) { + + } + } + + @Test + public void testProviderOnLoginPage() { + // Provider button is available on login page + this.driver.navigate().to("http://localhost:8081/test-app/"); + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); + loginPage.findSocialButton(getProviderId()); + } + + @Test + public void testAccountManagementLinkIdentity() { + // 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 + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_ON); + accountFederatedIdentityPage.clickAddProvider(identityProviderModel.getAlias()); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/")); + this.loginPage.login("test-user", "password"); + doAfterProviderAuthentication(); + + // Assert identity linked in account management + assertTrue(accountFederatedIdentityPage.isCurrent()); + assertTrue(driver.getPageSource().contains("id=\"remove-" + identityProviderModel.getAlias() + "\"")); + + // Revoke grant in account mgmt + revokeGrant(); + + // Logout from account management + accountFederatedIdentityPage.logout(); + assertTrue(driver.getTitle().equals("Log in to realm-with-broker")); + + // Assert I am logged immediately to account management due to previously linked "test-user" identity + loginPage.clickSocial(identityProviderModel.getAlias()); + doAfterProviderAuthentication(); + assertTrue(accountFederatedIdentityPage.isCurrent()); + assertTrue(driver.getPageSource().contains("id=\"remove-" + identityProviderModel.getAlias() + "\"")); + + // Unlink my "test-user" + accountFederatedIdentityPage.clickRemoveProvider(identityProviderModel.getAlias()); + assertTrue(driver.getPageSource().contains("id=\"add-" + identityProviderModel.getAlias() + "\"")); + + // Revoke grant in account mgmt + revokeGrant(); + + // Logout from account management + System.out.println("*** logout from account management"); + accountFederatedIdentityPage.logout(); + assertTrue(driver.getTitle().equals("Log in to realm-with-broker")); + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); + + // Try to login. Previous link is not valid anymore, so now it should try to register new user + this.loginPage.clickSocial(identityProviderModel.getAlias()); + this.loginPage.login("test-user", "password"); + doAfterProviderAuthentication(); + this.updateProfilePage.assertCurrent(); + } + + @Test(expected = NoSuchElementException.class) + public void testIdentityProviderNotAllowed() { + this.driver.navigate().to("http://localhost:8081/test-app/"); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); + + driver.findElement(By.className("model-oidc-idp")); + } + + protected void configureClientRetrieveToken(String clientId) { + RealmModel realm = getRealm(); + RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE); + ClientModel client = realm.getClientByClientId(clientId); + if (!client.hasScope(readTokenRole)) client.addScopeMapping(readTokenRole); + + brokerServerRule.stopSession(session, true); + session = brokerServerRule.startSession(); + + } + + protected void configureUserRetrieveToken(String username) { + RealmModel realm = getRealm(); + UserModel user = session.users().getUserByUsername(username, realm); + RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE); + if (user != null && !user.hasRole(readTokenRole)) { + user.grantRole(readTokenRole); + } + brokerServerRule.stopSession(session, true); + session = brokerServerRule.startSession(); + + } + + protected void unconfigureClientRetrieveToken(String clientId) { + RealmModel realm = getRealm(); + RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE); + ClientModel client = realm.getClientByClientId(clientId); + if (client.hasScope(readTokenRole)) client.deleteScopeMapping(readTokenRole); + + brokerServerRule.stopSession(session, true); + session = brokerServerRule.startSession(); + + } + + protected void unconfigureUserRetrieveToken(String username) { + RealmModel realm = getRealm(); + UserModel user = session.users().getUserByUsername(username, realm); + RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE); + if (user != null && user.hasRole(readTokenRole)) { + user.deleteRoleMapping(readTokenRole); + } + brokerServerRule.stopSession(session, true); + session = brokerServerRule.startSession(); + + } + + @Test + public void testTokenStorageAndRetrievalByApplication() { + setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_ON); + IdentityProviderModel identityProviderModel = getIdentityProviderModel(); + + identityProviderModel.setStoreToken(true); + + authenticateWithIdentityProvider(identityProviderModel, "test-user", true); + + UserModel federatedUser = getFederatedUser(); + RealmModel realm = getRealm(); + Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realm); + + assertFalse(federatedIdentities.isEmpty()); + assertEquals(1, federatedIdentities.size()); + + FederatedIdentityModel identityModel = federatedIdentities.iterator().next(); + + assertNotNull(identityModel.getToken()); + + UserSessionStatusServlet.UserSessionStatus userSessionStatus = retrieveSessionStatus(); + String accessToken = userSessionStatus.getAccessTokenString(); + URI tokenEndpointUrl = Urls.identityProviderRetrieveToken(BASE_URI, getProviderId(), realm.getName()); + final String authHeader = "Bearer " + accessToken; + ClientRequestFilter authFilter = new ClientRequestFilter() { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader); + } + }; + Client client = ClientBuilder.newBuilder().register(authFilter).build(); + WebTarget tokenEndpoint = client.target(tokenEndpointUrl); + Response response = tokenEndpoint.request().get(); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertNotNull(response.readEntity(String.class)); + revokeGrant(); + + + driver.navigate().to("http://localhost:8081/test-app/logout"); + String currentUrl = this.driver.getCurrentUrl(); + System.out.println("after logout currentUrl: " + currentUrl); + assertTrue(currentUrl.startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); + + unconfigureUserRetrieveToken(getProviderId() + ".test-user"); + loginIDP("test-user"); + //authenticateWithIdentityProvider(identityProviderModel, "test-user"); + assertEquals("http://localhost:8081/test-app", driver.getCurrentUrl()); + + userSessionStatus = retrieveSessionStatus(); + accessToken = userSessionStatus.getAccessTokenString(); + final String authHeader2 = "Bearer " + accessToken; + ClientRequestFilter authFilter2 = new ClientRequestFilter() { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader2); + } + }; + client = ClientBuilder.newBuilder().register(authFilter2).build(); + tokenEndpoint = client.target(tokenEndpointUrl); + response = tokenEndpoint.request().get(); + + assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); + + revokeGrant(); + driver.navigate().to("http://localhost:8081/test-app/logout"); + driver.navigate().to("http://localhost:8081/test-app"); + + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth")); + } + + protected abstract void doAssertTokenRetrieval(String pageSource); + +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCFirstBrokerLoginTest.java new file mode 100644 index 00000000000..d6893632e65 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/OIDCFirstBrokerLoginTest.java @@ -0,0 +1,145 @@ +package org.keycloak.testsuite.broker; + +import java.util.Set; + +import org.junit.Assert; +import org.junit.ClassRule; +import org.junit.Test; +import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.DefaultAuthenticationFlows; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.KeycloakServer; +import org.keycloak.testsuite.rule.AbstractKeycloakRule; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.openqa.selenium.NoSuchElementException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Marek Posolda + */ +public class OIDCFirstBrokerLoginTest extends AbstractFirstBrokerLoginTest { + + private static final int PORT = 8082; + + @ClassRule + public static AbstractKeycloakRule samlServerRule = 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")); + server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-saml.json")); + } + + @Override + protected String[] getTestRealms() { + return new String[] { "realm-with-oidc-identity-provider", "realm-with-saml-idp-basic" }; + } + }; + + @Override + protected String getProviderId() { + return "kc-oidc-idp"; + } + + + /** + * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by reauthentication + * with different broker already linked to his account + */ + @Test + public void testLinkAccountByReauthenticationWithDifferentBroker() throws Exception { + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW, + IdpEmailVerificationAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.DISABLED); + + setUpdateProfileFirstLogin(realmWithBroker, IdentityProviderRepresentation.UPFLM_OFF); + } + + }, APP_REALM_ID); + + // First link "pedroigor" user with SAML broker and logout + driver.navigate().to("http://localhost:8081/test-app"); + this.loginPage.clickSocial("kc-saml-idp-basic"); + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/")); + Assert.assertEquals("Log in to realm-with-saml-idp-basic", this.driver.getTitle()); + this.loginPage.login("pedroigor", "password"); + + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + this.idpConfirmLinkPage.clickLinkAccount(); + + this.loginPage.login("password"); + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); + driver.navigate().to("http://localhost:8081/test-app/logout"); + + + // login through OIDC broker now + loginIDP("pedroigor"); + + this.idpConfirmLinkPage.assertCurrent(); + Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); + this.idpConfirmLinkPage.clickLinkAccount(); + + // assert reauthentication with login page. On login page is link to kc-saml-idp-basic as user has it linked already + Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle()); + Assert.assertEquals("Authenticate as pedroigor to link your account with " + getProviderId(), this.loginPage.getSuccessMessage()); + + try { + this.loginPage.findSocialButton(getProviderId()); + Assert.fail("Not expected to see social button with " + getProviderId()); + } catch (NoSuchElementException expected) { + } + + // reauthenticate with SAML broker + this.loginPage.clickSocial("kc-saml-idp-basic"); + Assert.assertEquals("Log in to realm-with-saml-idp-basic", this.driver.getTitle()); + this.loginPage.login("pedroigor", "password"); + + + // authenticated and redirected to app. User is linked with identity provider + assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); + UserModel federatedUser = getFederatedUser(); + + assertNotNull(federatedUser); + assertEquals("pedroigor", federatedUser.getUsername()); + assertEquals("psilva@redhat.com", federatedUser.getEmail()); + + RealmModel realmWithBroker = getRealm(); + Set federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realmWithBroker); + assertEquals(2, federatedIdentities.size()); + + for (FederatedIdentityModel link : federatedIdentities) { + Assert.assertEquals("pedroigor", link.getUserName()); + Assert.assertTrue(link.getIdentityProvider().equals(getProviderId()) || link.getIdentityProvider().equals("kc-saml-idp-basic")); + } + + brokerServerRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) { + setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW, + IdpEmailVerificationAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.ALTERNATIVE); + + } + + }, APP_REALM_ID); + } + +} 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 0a7ee1602ae..5bfdc1d75d3 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 @@ -26,7 +26,7 @@ import static org.junit.Assert.fail; /** * @author pedroigor */ -public class OIDCKeyCloakServerBrokerBasicTest extends AbstractIdentityProviderTest { +public class OIDCKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityProviderTest { private static final int PORT = 8082; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java new file mode 100644 index 00000000000..66692d48db5 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java @@ -0,0 +1,40 @@ +package org.keycloak.testsuite.broker; + +import org.junit.ClassRule; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.testsuite.KeycloakServer; +import org.keycloak.testsuite.rule.AbstractKeycloakRule; + +/** + * @author Marek Posolda + */ +public class SAMLFirstBrokerLoginTest extends AbstractFirstBrokerLoginTest { + + private static final int PORT = 8082; + + @ClassRule + public static AbstractKeycloakRule samlServerRule = 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-saml.json")); + } + + @Override + protected String[] getTestRealms() { + return new String[] { "realm-with-saml-idp-basic" }; + } + }; + + @Override + protected String getProviderId() { + return "kc-saml-idp-basic"; + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java index 7bbaa204d89..8be9dcc11c0 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerBasicTest.java @@ -24,7 +24,7 @@ import static org.junit.Assert.fail; /** * @author pedroigor */ -public class SAMLKeyCloakServerBrokerBasicTest extends AbstractIdentityProviderTest { +public class SAMLKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityProviderTest { @ClassRule public static AbstractKeycloakRule samlServerRule = new AbstractKeycloakRule() { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java index 249f2e02dff..197749b170b 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLKeyCloakServerBrokerWithSignatureTest.java @@ -24,7 +24,7 @@ import static org.junit.Assert.fail; /** * @author pedroigor */ -public class SAMLKeyCloakServerBrokerWithSignatureTest extends AbstractIdentityProviderTest { +public class SAMLKeyCloakServerBrokerWithSignatureTest extends AbstractKeycloakIdentityProviderTest { @ClassRule public static AbstractKeycloakRule samlServerRule = new AbstractKeycloakRule() { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpConfirmLinkPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpConfirmLinkPage.java new file mode 100644 index 00000000000..83596a06a70 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpConfirmLinkPage.java @@ -0,0 +1,41 @@ +package org.keycloak.testsuite.pages; + +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * @author Marek Posolda + */ +public class IdpConfirmLinkPage extends AbstractPage { + + @FindBy(id = "updateProfile") + private WebElement updateProfileButton; + + @FindBy(id = "linkAccount") + private WebElement linkAccountButton; + + @FindBy(className = "instruction") + private WebElement message; + + @Override + public boolean isCurrent() { + return driver.getTitle().equals("Account already exists"); + } + + public String getMessage() { + return message.getText(); + } + + public void clickReviewProfile() { + updateProfileButton.click(); + } + + public void clickLinkAccount() { + linkAccountButton.click(); + } + + @Override + public void open() throws Exception { + throw new UnsupportedOperationException(); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java new file mode 100644 index 00000000000..91387b54874 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java @@ -0,0 +1,27 @@ +package org.keycloak.testsuite.pages; + +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * @author Marek Posolda + */ +public class IdpLinkEmailPage extends AbstractPage { + + @FindBy(id = "instruction1") + private WebElement message; + + @Override + public boolean isCurrent() { + return driver.getTitle().startsWith("Link "); + } + + @Override + public void open() throws Exception { + throw new UnsupportedOperationException(); + } + + public String getMessage() { + return message.getText(); + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java index f32953375b0..04e5dddfcdd 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginPage.java @@ -112,6 +112,10 @@ public class LoginPage extends AbstractPage { return usernameInput.getAttribute("value"); } + public boolean isUsernameInputEnabled() { + return usernameInput.isEnabled(); + } + public String getPassword() { return passwordInput.getAttribute("value"); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java index d67862c7928..c9038a4d6e2 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginUpdateProfilePage.java @@ -21,6 +21,8 @@ */ package org.keycloak.testsuite.pages; +import org.junit.Assert; +import org.openqa.selenium.By; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy;