diff --git a/server-spi-private/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java b/server-spi-private/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java index 2faced9e119..867f0e4220e 100755 --- a/server-spi-private/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java +++ b/server-spi-private/src/main/java/org/keycloak/authentication/AbstractAuthenticationFlowContext.java @@ -111,6 +111,21 @@ public interface AbstractAuthenticationFlowContext { */ FormMessage getForwardedSuccessMessage(); + /** + * This could be an info message forwarded from another authenticator. This info message will be usually displayed only once on the + * first screen shown to the user during authentication. The authenticator forwarding the info message does not know which the screen would be. + * For example during user re-authentication, the user should see info message like "Please re-authenticate", but at the beginning of the + * authentication, it is not 100% clear which screen will be the first shown screen where this message should be displayed + */ + FormMessage getForwardedInfoMessage(); + + /** + * @see #getForwardedInfoMessage() + * @param message to be forwarded + * @param parameters parameters of the message if any + */ + void setForwardedInfoMessage(String message, Object... parameters); + /** * Generates access code and updates clientsession timestamp * Access codes must be included in form action callbacks as a query parameter. diff --git a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java index ca868c0deac..9726dea8f45 100755 --- a/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/forms/login/LoginFormsProvider.java @@ -38,7 +38,7 @@ public interface LoginFormsProvider extends Provider { String IDENTITY_PROVIDER_BROKER_CONTEXT = "identityProviderBrokerCtx"; - String USERNAME_EDIT_DISABLED = "usernameEditDisabled"; + String USERNAME_HIDDEN = "usernameHidden"; String REGISTRATION_DISABLED = "registrationDisabled"; diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index f2774c28af6..c2b4ed54a08 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -105,6 +105,11 @@ public class AuthenticationProcessor { */ protected ForwardedFormMessageStore forwardedSuccessMessageStore = new ForwardedFormMessageStore(ForwardedFormMessageType.SUCCESS); + /** + * This could be an success message forwarded from another authenticator + */ + protected ForwardedFormMessageStore forwardedInfoMessageStore = new ForwardedFormMessageStore(ForwardedFormMessageType.INFO); + // Used for client authentication protected ClientModel client; protected Map clientAuthAttributes = new HashMap<>(); @@ -232,6 +237,11 @@ public class AuthenticationProcessor { return this; } + public AuthenticationProcessor setForwardedInfoMessage(FormMessage forwardedInfoMessage) { + this.forwardedInfoMessageStore.setForwardedMessage(forwardedInfoMessage); + return this; + } + public String generateCode() { ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getAuthenticationSession()); authenticationSession.getParentSession().setTimestamp(Time.currentTime()); @@ -528,6 +538,9 @@ public class AuthenticationProcessor { } else if (getForwardedSuccessMessage() != null) { provider.addSuccess(getForwardedSuccessMessage()); forwardedSuccessMessageStore.removeForwardedMessage(); + } else if (getForwardedInfoMessage() != null) { + provider.setInfo(getForwardedInfoMessage().getMessage(), getForwardedInfoMessage().getParameters()); + forwardedInfoMessageStore.removeForwardedMessage(); } return provider; } @@ -642,6 +655,16 @@ public class AuthenticationProcessor { return AuthenticationProcessor.this.forwardedSuccessMessageStore.getForwardedMessage(); } + @Override + public void setForwardedInfoMessage(String message, Object... parameters) { + AuthenticationProcessor.this.setForwardedInfoMessage(new FormMessage(message, parameters)); + } + + @Override + public FormMessage getForwardedInfoMessage() { + return AuthenticationProcessor.this.forwardedInfoMessageStore.getForwardedMessage(); + } + public FormMessage getErrorMessage() { return errorMessage; } @@ -1139,7 +1162,7 @@ public class AuthenticationProcessor { } private enum ForwardedFormMessageType { - SUCCESS("fwMessageSuccess"), ERROR("fwMessageError"); + SUCCESS("fwMessageSuccess"), ERROR("fwMessageError"), INFO("fwMessageInfo"); private final String key; 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 4505c6377ba..6f02b2b988d 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 @@ -54,6 +54,9 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth public static final String REGISTRATION_FORM_ACTION = "registration_form"; public static final String ATTEMPTED_USERNAME = "ATTEMPTED_USERNAME"; + // Flag is true if user was already set in the authContext before this authenticator was triggered. In this case we skip clearing of the user after unsuccessful password authentication + protected static final String USER_SET_BEFORE_USERNAME_PASSWORD_AUTH = "USER_SET_BEFORE_USERNAME_PASSWORD_AUTH"; + @Override public void action(AuthenticationFlowContext context) { @@ -142,18 +145,30 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth public boolean validateUserAndPassword(AuthenticationFlowContext context, MultivaluedMap inputData) { - context.clearUser(); UserModel user = getUser(context, inputData); - return user != null && validatePassword(context, user, inputData) && validateUser(context, user, inputData); + boolean shouldClearUserFromCtxAfterBadPassword = !isUserAlreadySetBeforeUsernamePasswordAuth(context); + return user != null && validatePassword(context, user, inputData, shouldClearUserFromCtxAfterBadPassword) && validateUser(context, user, inputData); } public boolean validateUser(AuthenticationFlowContext context, MultivaluedMap inputData) { - context.clearUser(); UserModel user = getUser(context, inputData); return user != null && validateUser(context, user, inputData); } private UserModel getUser(AuthenticationFlowContext context, MultivaluedMap inputData) { + if (isUserAlreadySetBeforeUsernamePasswordAuth(context)) { + // Get user from the authentication context in case he was already set before this authenticator + UserModel user = context.getUser(); + testInvalidUser(context, user); + return user; + } else { + // Normal login. In this case this authenticator is supposed to establish identity of the user from the provided username + context.clearUser(); + return getUserFromForm(context, inputData); + } + } + + private UserModel getUserFromForm(AuthenticationFlowContext context, MultivaluedMap inputData) { String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME); if (username == null) { context.getEvent().error(Errors.USER_NOT_FOUND); @@ -203,10 +218,6 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth return true; } - public boolean validatePassword(AuthenticationFlowContext context, UserModel user, MultivaluedMap inputData) { - return validatePassword(context, user, inputData, true); - } - public boolean validatePassword(AuthenticationFlowContext context, UserModel user, MultivaluedMap inputData, boolean clearUser) { String password = inputData.getFirst(CredentialRepresentation.PASSWORD); if (password == null || password.isEmpty()) { @@ -226,6 +237,13 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth private boolean badPasswordHandler(AuthenticationFlowContext context, UserModel user, boolean clearUser,boolean isEmptyPassword) { context.getEvent().user(user); context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); + + if (isUserAlreadySetBeforeUsernamePasswordAuth(context)) { + LoginFormsProvider form = context.form(); + form.setAttribute(LoginFormsProvider.USERNAME_HIDDEN, true); + form.setAttribute(LoginFormsProvider.REGISTRATION_DISABLED, true); + } + Response challengeResponse = challenge(context, getDefaultChallengeMessage(context), FIELD_PASSWORD); if(isEmptyPassword) { context.forceChallenge(challengeResponse); @@ -252,6 +270,15 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth } protected String getDefaultChallengeMessage(AuthenticationFlowContext context) { - return Messages.INVALID_USER; + if (isUserAlreadySetBeforeUsernamePasswordAuth(context)) { + return Messages.INVALID_PASSWORD; + } else { + return Messages.INVALID_USER; + } + } + + protected boolean isUserAlreadySetBeforeUsernamePasswordAuth(AuthenticationFlowContext context) { + String userSet = context.getAuthenticationSession().getAuthNote(USER_SET_BEFORE_USERNAME_PASSWORD_AUTH); + return Boolean.parseBoolean(userSet); } } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java index 76a8fa61c71..9c58f6420ed 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/CookieAuthenticator.java @@ -26,6 +26,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.protocol.LoginProtocol; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.messages.Messages; import org.keycloak.sessions.AuthenticationSessionModel; /** @@ -55,6 +56,7 @@ public class CookieAuthenticator implements Authenticator { if (protocol.requireReauthentication(authResult.getSession(), authSession)) { // Full re-authentication, so we start with no loa authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(Constants.NO_LOA)); + context.setForwardedInfoMessage(Messages.REAUTHENTICATE); context.attempted(); } else if (!AuthenticatorUtil.isLevelOfAuthenticationSatisfied(authSession)) { // Step-up authentication, we keep the loa from the existing user session. diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java index a4e8700f923..2249198aea0 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernameForm.java @@ -17,8 +17,12 @@ package org.keycloak.authentication.authenticators.browser; +import java.util.List; + import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.forms.login.freemarker.LoginFormsUtil; +import org.keycloak.models.IdentityProviderModel; import org.keycloak.services.messages.Messages; import javax.ws.rs.core.MultivaluedMap; @@ -26,6 +30,20 @@ import javax.ws.rs.core.Response; public final class UsernameForm extends UsernamePasswordForm { + @Override + public void authenticate(AuthenticationFlowContext context) { + if (context.getUser() != null) { + // We can skip the form when user is re-authenticating. Unless current user has some IDP set, so he can re-authenticate with that IDP + List identityProviders = LoginFormsUtil + .filterIdentityProviders(context.getRealm().getIdentityProvidersStream(), context.getSession(), context); + if (identityProviders.isEmpty()) { + context.success(); + return; + } + } + super.authenticate(context); + } + @Override protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap formData) { return validateUser(context, formData); 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 204f504692a..c126371971c 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 @@ -62,12 +62,20 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl String rememberMeUsername = AuthenticationManager.getRememberMeUsername(context.getRealm(), context.getHttpRequest().getHttpHeaders()); - if (loginHint != null || rememberMeUsername != null) { - if (loginHint != null) { - formData.add(AuthenticationManager.FORM_USERNAME, loginHint); - } else { - formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername); - formData.add("rememberMe", "on"); + if (context.getUser() != null) { + LoginFormsProvider form = context.form(); + form.setAttribute(LoginFormsProvider.USERNAME_HIDDEN, true); + form.setAttribute(LoginFormsProvider.REGISTRATION_DISABLED, true); + context.getAuthenticationSession().setAuthNote(USER_SET_BEFORE_USERNAME_PASSWORD_AUTH, "true"); + } else { + context.getAuthenticationSession().removeAuthNote(USER_SET_BEFORE_USERNAME_PASSWORD_AUTH); + if (loginHint != null || rememberMeUsername != null) { + if (loginHint != null) { + formData.add(AuthenticationManager.FORM_USERNAME, loginHint); + } else { + formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername); + formData.add("rememberMe", "on"); + } } } Response challengeResponse = challenge(context, formData); diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/LoginFormsUtil.java b/services/src/main/java/org/keycloak/forms/login/freemarker/LoginFormsUtil.java index 15d69a33ef2..0605a510b97 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/LoginFormsUtil.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/LoginFormsUtil.java @@ -43,50 +43,33 @@ import java.util.stream.Stream; */ public class LoginFormsUtil { - // Display just those identityProviders on login screen, which are already linked to "known" established user - public static List filterIdentityProvidersByUser(List providers, KeycloakSession session, RealmModel realm, - Map attributes, MultivaluedMap formData) { - - Boolean usernameEditDisabled = (Boolean) attributes.get(LoginFormsProvider.USERNAME_EDIT_DISABLED); - if (usernameEditDisabled != null && usernameEditDisabled) { - String username = formData.getFirst(UserModel.USERNAME); - if (username == null) { - throw new IllegalStateException("USERNAME_EDIT_DISABLED but username not known"); - } - - UserModel user = session.users().getUserByUsername(realm, username); - if (user == null || !user.isEnabled()) { - throw new IllegalStateException("User " + username + " not found or disabled"); - } - - Set federatedIdentities = session.users().getFederatedIdentitiesStream(realm, user) - .map(federatedIdentityModel -> federatedIdentityModel.getIdentityProvider()) - .collect(Collectors.toSet()); - - List result = new LinkedList<>(); - for (IdentityProviderModel idp : providers) { - if (federatedIdentities.contains(idp.getAlias())) { - result.add(idp); - } - } - return result; - } else { - return providers; - } - } - public static List filterIdentityProviders(Stream providers, KeycloakSession session, AuthenticationFlowContext context) { if (context != null) { AuthenticationSessionModel authSession = context.getAuthenticationSession(); SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); - if (serializedCtx != null) { - IdentityProviderModel idp = serializedCtx.deserialize(session, authSession).getIdpConfig(); - return providers - .filter(p -> !Objects.equals(p.getAlias(), idp.getAlias())) - .collect(Collectors.toList()); + final IdentityProviderModel existingIdp = (serializedCtx == null) ? null : serializedCtx.deserialize(session, authSession).getIdpConfig(); + + final Set federatedIdentities; + if (context.getUser() != null) { + federatedIdentities = session.users().getFederatedIdentitiesStream(session.getContext().getRealm(), context.getUser()) + .map(federatedIdentityModel -> federatedIdentityModel.getIdentityProvider()) + .collect(Collectors.toSet()); + } else { + federatedIdentities = null; } + + return providers + .filter(p -> { // Filter current IDP during first-broker-login flow. Re-authentication with the "linked" broker should not be possible + if (existingIdp == null) return true; + return !Objects.equals(p.getAlias(), existingIdp.getAlias()); + }) + .filter(idp -> { // In case that we already have user established in authentication session, we show just providers already linked to this user + if (federatedIdentities == null) return true; + return federatedIdentities.contains(idp.getAlias()); + }) + .collect(Collectors.toList()); } return providers.collect(Collectors.toList()); } diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java index f5f9ca532f7..96be3c0ea5f 100755 --- a/services/src/main/java/org/keycloak/services/messages/Messages.java +++ b/services/src/main/java/org/keycloak/services/messages/Messages.java @@ -24,6 +24,8 @@ public class Messages { public static final String DISPLAY_UNSUPPORTED = "displayUnsupported"; public static final String LOGIN_TIMEOUT = "loginTimeout"; + public static final String REAUTHENTICATE = "reauthenticate"; + public static final String INVALID_USER = "invalidUserMessage"; public static final String INVALID_USERNAME = "invalidUsernameMessage"; 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 604141985a1..6b5122e7ed8 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -59,6 +59,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.AuthenticationFlowResolver; import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.KeycloakModelUtils; @@ -231,6 +232,14 @@ public class LoginActionsService { flowPath = AUTHENTICATE_PATH; } + // See if we already have userSession attached to authentication session. This means restart of authentication session during re-authentication + // We logout userSession in this case + UserSessionModel userSession = new AuthenticationSessionManager(session).getUserSession(authSession); + if (userSession != null) { + logger.debugf("Logout of user session %s when restarting flow during re-authentication", userSession.getId()); + AuthenticationManager.backchannelLogout(session, userSession, false); + } + AuthenticationProcessor.resetFlow(authSession, flowPath); URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getClient().getClientId(), tabId); @@ -849,7 +858,6 @@ public class LoginActionsService { /** * OAuth grant page. You should not invoked this directly! * - * @param formData * @return */ @Path("consent") diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java index f44bd8fc25f..b72035aae17 100755 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginPage.java @@ -122,6 +122,18 @@ public class LoginPage extends LanguageComboboxAwarePage { return usernameInput.isEnabled(); } + public boolean isUsernameInputPresent() { + return !driver.findElements(By.id("username")).isEmpty(); + } + + public boolean isRegisterLinkPresent() { + return !driver.findElements(By.linkText("Register")).isEmpty(); + } + + public boolean isRememberMeCheckboxPresent() { + return !driver.findElements(By.id("rememberMe")).isEmpty(); + } + public String getPassword() { return passwordInput.getAttribute("value"); } @@ -154,7 +166,11 @@ public class LoginPage extends LanguageComboboxAwarePage { return loginSuccessMessage != null ? loginSuccessMessage.getText() : null; } public String getInfoMessage() { - return loginInfoMessage != null ? loginInfoMessage.getText() : null; + try { + return getTextFromElement(loginInfoMessage); + } catch (NoSuchElementException e) { + return null; + } } @@ -187,6 +203,11 @@ public class LoginPage extends LanguageComboboxAwarePage { return DroneUtils.getCurrentDriver().findElement(By.id(id)); } + public boolean isSocialButtonPresent(String alias) { + String id = "social-" + alias; + return !DroneUtils.getCurrentDriver().findElements(By.id(id)).isEmpty(); + } + public void resetPassword() { clickLink(resetPasswordLink); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUsernameOnlyPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUsernameOnlyPage.java index 0f09e045817..eec1286f5ef 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUsernameOnlyPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/pages/LoginUsernameOnlyPage.java @@ -32,6 +32,11 @@ public class LoginUsernameOnlyPage extends LoginPage { } } + // Click button without fill anything + public void clickSubmitButton() { + submitButton.click(); + } + /** * Not supported for this implementation * diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java index 6ca70e02ade..9ec0fcfd5f8 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/AppInitiatedActionResetPasswordTest.java @@ -19,6 +19,7 @@ package org.keycloak.testsuite.actions; import org.jboss.arquillian.drone.api.annotation.Drone; import org.jboss.arquillian.graphene.page.Page; import org.junit.After; +import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.keycloak.admin.client.resource.UserResource; @@ -114,7 +115,8 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct doAIA(); loginPage.assertCurrent(); - loginPage.login("test-user@localhost", "password"); + Assert.assertEquals("test-user@localhost", loginPage.getAttemptedUsername()); + loginPage.login("password"); changePasswordPage.assertCurrent(); assertTrue(changePasswordPage.isCancelDisplayed()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkTest.java index c8f79b41524..9218858d7c3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/ClientInitiatedAccountLinkTest.java @@ -538,52 +538,6 @@ public class ClientInitiatedAccountLinkTest extends AbstractServletsAdapterTest } - @Test - @DisableFeature(value = Profile.Feature.ACCOUNT2, skipRestart = true) // TODO remove this (KEYCLOAK-16228) - public void testAccountNotLinkedAutomatically() throws Exception { - RealmResource realm = adminClient.realms().realm(CHILD_IDP); - List links = realm.users().get(childUserId).getFederatedIdentity(); - Assert.assertTrue(links.isEmpty()); - - // Login to account mgmt first - profilePage.open(CHILD_IDP); - WaitUtils.waitForPageToLoad(); - - Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); - loginPage.login("child", "password"); - profilePage.assertCurrent(); - - // Now in another tab, open login screen with "prompt=login" . Login screen will be displayed even if I have SSO cookie - UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString()) - .path("nosuch"); - String linkUrl = linkBuilder.clone() - .queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN) - .build().toString(); - - navigateTo(linkUrl); - Assert.assertTrue(loginPage.isCurrent(CHILD_IDP)); - loginPage.clickSocial(PARENT_IDP); - Assert.assertTrue(loginPage.isCurrent(PARENT_IDP)); - loginPage.login(PARENT_USERNAME, "password"); - - // Test I was not automatically linked. - links = realm.users().get(childUserId).getFederatedIdentity(); - Assert.assertTrue(links.isEmpty()); - - loginUpdateProfilePage.assertCurrent(); - loginUpdateProfilePage.update("Joe", "Doe", "joe@parent.com"); - - errorPage.assertCurrent(); - Assert.assertEquals("You are already authenticated as different user 'child' in this session. Please sign out first.", errorPage.getError()); - - logoutAll(); - - // Remove newly created user - String newUserId = ApiUtil.findUserByUsername(realm, "parent").getId(); - getCleanup("child").addUserId(newUserId); - } - - @Test @DisableFeature(value = Profile.Feature.ACCOUNT2, skipRestart = true) // TODO remove this (KEYCLOAK-16228) public void testAccountLinkingExpired() throws Exception { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java index 57fedae6c14..fac4482376b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/DemoServletsAdapterTest.java @@ -855,7 +855,9 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest { String appUri = tokenMinTTLPage.getUriBuilder().queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN).build().toString(); URLUtils.navigateToUri(appUri); assertCurrentUrlStartsWithLoginUrlOf(testRealmPage); - testRealmLoginPage.form().login("bburke@redhat.com", "password"); + WaitUtils.waitForPageToLoad(); + testRealmLoginPage.form().setPassword("password"); + testRealmLoginPage.form().login(); AccessToken token = tokenMinTTLPage.getAccessToken(); int authTime = token.getAuthTime(); assertThat(authTime, is(greaterThanOrEqualTo(currentTime + 10))); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBaseBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBaseBrokerTest.java index d473a26e96e..1a27e03fea9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBaseBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractBaseBrokerTest.java @@ -226,6 +226,21 @@ public abstract class AbstractBaseBrokerTest extends AbstractKeycloakTest { logInWithBroker(bc); } + // We are re-authenticating to the IDP. Hence it is assumed that "username" field is not visible on the login form on the IDP side + protected void logInAsUserInIDPWithReAuthenticate() { + driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName())); + + waitForPage(driver, "sign in to", true); + log.debug("Clicking social " + bc.getIDPAlias()); + loginPage.clickSocial(bc.getIDPAlias()); + waitForPage(driver, "sign in to", true); + + // We are re-authenticating. Username field not visible + log.debug("Reauthenticating"); + Assert.assertFalse(loginPage.isUsernameInputPresent()); + loginPage.login(bc.getUserPassword()); + } + protected void logInWithBroker(BrokerConfiguration bc) { logInWithIdp(bc.getIDPAlias(), bc.getUserLogin(), bc.getUserPassword()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java index 232e39624b5..de8bff83e77 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java @@ -461,7 +461,7 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa waitForPage(driver, "account already exists", false); } catch (Exception e) { // this is a workaround to make this test work for both oidc and saml. when doing oidc the browser is redirected to the login page to finish the linking - loginPage.login(bc.getUserLogin(), bc.getUserPassword()); + loginPage.login(bc.getUserPassword()); } waitForPage(driver, "account already exists", false); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java index 4387d165450..726298ddde4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOIDCBrokerWithSignatureTest.java @@ -111,7 +111,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest { // Set time offset. New keys can be downloaded. Check that user is able to login. setTimeOffset(20); - logInAsUserInIDP(); + logInAsUserInIDPWithReAuthenticate(); assertLoggedInAccountManagement(); } @@ -159,7 +159,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest { // Even after time offset is user not able to login, because it uses old key hardcoded in identityProvider config setTimeOffset(20); - logInAsUserInIDP(); + logInAsUserInIDPWithReAuthenticate(); assertErrorPage("Unexpected error when authenticating with identity provider"); } @@ -193,7 +193,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest { // Set key id to a valid one cfg.setPublicKeySignatureVerifierKeyId(expectedKeyId); updateIdentityProvider(idpRep); - logInAsUserInIDP(); + logInAsUserInIDPWithReAuthenticate(); assertLoggedInAccountManagement(); logoutFromRealm(getConsumerRoot(), bc.consumerRealmName()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ReAuthenticationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ReAuthenticationTest.java new file mode 100644 index 00000000000..6f2e0372502 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/ReAuthenticationTest.java @@ -0,0 +1,349 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.testsuite.forms; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.jboss.arquillian.drone.api.annotation.Drone; +import org.jboss.arquillian.graphene.page.Page; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.junit.Rule; +import org.junit.Test; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.authentication.authenticators.browser.OTPFormAuthenticatorFactory; +import org.keycloak.authentication.authenticators.browser.PasswordFormFactory; +import org.keycloak.authentication.authenticators.browser.UsernameFormFactory; +import org.keycloak.authentication.authenticators.conditional.ConditionalUserConfiguredAuthenticatorFactory; +import org.keycloak.events.Details; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.representations.IDToken; +import org.keycloak.representations.idm.EventRepresentation; +import org.keycloak.representations.idm.FederatedIdentityRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; +import org.keycloak.testsuite.auth.page.login.OneTimeCode; +import org.keycloak.testsuite.broker.SocialLoginTest; +import org.keycloak.testsuite.pages.AppPage; +import org.keycloak.testsuite.pages.ErrorPage; +import org.keycloak.testsuite.pages.LoginPage; +import org.keycloak.testsuite.pages.LoginTotpPage; +import org.keycloak.testsuite.pages.LoginUsernameOnlyPage; +import org.keycloak.testsuite.pages.PasswordPage; +import org.keycloak.testsuite.util.FederatedIdentityBuilder; +import org.keycloak.testsuite.util.FlowUtil; +import org.keycloak.testsuite.util.OAuthClient; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +import static org.hamcrest.CoreMatchers.is; +import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson; +import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE; +import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITHUB; +import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GITLAB; +import static org.keycloak.testsuite.broker.SocialLoginTest.Provider.GOOGLE; + +/** + * Test for various scenarios with user re-authentication + * + * @author Marek Posolda + */ +public class ReAuthenticationTest extends AbstractTestRealmKeycloakTest { + + @ArquillianResource + protected OAuthClient oauth; + + @Drone + protected WebDriver driver; + + @Page + protected LoginPage loginPage; + + @Page + protected LoginUsernameOnlyPage loginUsernameOnlyPage; + + @Page + protected PasswordPage passwordPage; + + @Page + protected ErrorPage errorPage; + + @Page + protected LoginTotpPage loginTotpPage; + + @Page + protected OneTimeCode oneTimeCodePage; + + @Page + protected AppPage appPage; + + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } + + private RealmRepresentation loadTestRealm() { + RealmRepresentation res = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class); + res.setBrowserFlow("browser"); + res.setRememberMe(true); + + // Add some sample dummy GitHub, Gitlab & Google social providers to the testing realm. Those are dummy providers for test if they are visible (clickable) + // on the login pages + List idps = new ArrayList<>(); + for (SocialLoginTest.Provider provider : Arrays.asList(GITHUB, GOOGLE)) { + SocialLoginTest socialLoginTest = new SocialLoginTest(); + idps.add(socialLoginTest.buildIdp(provider)); + } + res.setIdentityProviders(idps); + + return res; + } + + @Override + public void addTestRealms(List testRealms) { + log.debug("Adding test realm for import from testrealm.json"); + testRealms.add(loadTestRealm()); + } + + + @Test + public void usernamePasswordFormReauthentication() { + // Add fake github link to user account + UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost"); + FederatedIdentityRepresentation fedLink = FederatedIdentityBuilder.create() + .identityProvider("github") + .userId("123") + .userName("test") + .build(); + user.addFederatedIdentity("github", fedLink); + + // Login user + loginPage.open(); + loginPage.assertCurrent(); + assertUsernameFieldAndOtherFields(true); + assertSocialButtonsPresent(true, true); + loginPage.login("test-user@localhost", "password"); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + // Set time offset + setTimeOffset(10); + + // Request re-authentication + oauth.maxAge("1"); + loginPage.open(); + loginPage.assertCurrent(); + + // Username input hidden as well as register and rememberMe. Info message should be shown + assertUsernameFieldAndOtherFields(false); + assertInfoMessageAboutReAuthenticate(true); + + // Assert github link present as it is linked to user account. Google link should be hidden + assertSocialButtonsPresent(true, false); + + // Try bad password and assert things still hidden + loginPage.login("bad-password"); + loginPage.assertCurrent(); + Assert.assertEquals("Invalid password.", loginPage.getInputError()); + assertUsernameFieldAndOtherFields(false); + assertInfoMessageAboutReAuthenticate(false); + + loginPage.login("password"); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + // Remove link + user.removeFederatedIdentity("github"); + } + + // Case when user press the link "Restart login" during re-authentication + @Test + public void usernamePasswordFormReauthenticationWithResetFlow() { + // Login user + loginPage.open(); + loginPage.assertCurrent(); + assertUsernameFieldAndOtherFields(true); + assertSocialButtonsPresent(true, true); + loginPage.login("test-user@localhost", "password"); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + // Set time offset + setTimeOffset(10); + + // Request re-authentication + oauth.maxAge("1"); + loginPage.open(); + loginPage.assertCurrent(); + + // Username input hidden as well as register and rememberMe. Info message should be shown + assertUsernameFieldAndOtherFields(false); + assertInfoMessageAboutReAuthenticate(true); + + // Assert none of github link and google link present. As none of the providers is linked to user account + assertSocialButtonsPresent(false, false); + + // Try click "Reset password" . This will start login page from the beginning due SSO logout + Assert.assertEquals("test-user@localhost", loginPage.getAttemptedUsername()); + loginPage.clickResetLogin(); + + // Username field should be back. Attempted username should not be shown + loginPage.assertCurrent(); + assertUsernameFieldAndOtherFields(true); + assertInfoMessageAboutReAuthenticate(false); + + // Both social buttons should be present + assertSocialButtonsPresent(true, true); + + // Successfully login as different user. It should be possible due previous SSO session was removed + loginPage.login("john-doh@localhost", "password"); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + } + + // Re-authentication with user form separate to the password form. The username form would be skipped + @Test + @AuthServerContainerExclude(REMOTE) + public void identityFirstFormReauthentication() { + // Set identity-first as realm flow + setupIdentityFirstFlow(); + + // Login user + loginPage.open(); + loginUsernameOnlyPage.assertCurrent(); + assertUsernameFieldAndOtherFields(true); + assertSocialButtonsPresent(true, true); + loginUsernameOnlyPage.login("test-user@localhost"); + passwordPage.assertCurrent(); + passwordPage.login("password"); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + // Set time offset + setTimeOffset(10); + + // Request re-authentication + oauth.maxAge("1"); + loginPage.open(); + + // User directly on the password page. Info message should be shown here + passwordPage.assertCurrent(); + Assert.assertEquals("test-user@localhost", passwordPage.getAttemptedUsername()); + assertInfoMessageAboutReAuthenticate(true); + + passwordPage.login("bad-password"); + Assert.assertEquals("Invalid password.", passwordPage.getPasswordError()); + passwordPage.login("password"); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + // Revert flows + BrowserFlowTest.revertFlows(testRealm(), "browser - identity first"); + } + + // Re-authentication with user form separate to the password form. The username form is shown due the user linked with "github" + @Test + @AuthServerContainerExclude(REMOTE) + public void identityFirstFormReauthenticationWithGithubLink() { + // Set identity-first as realm flow + setupIdentityFirstFlow(); + + // Add fake federated link to the user + UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost"); + FederatedIdentityRepresentation fedLink = FederatedIdentityBuilder.create() + .identityProvider("github") + .userId("123") + .userName("test") + .build(); + user.addFederatedIdentity("github", fedLink); + + // Login user + loginPage.open(); + loginUsernameOnlyPage.assertCurrent(); + loginUsernameOnlyPage.login("test-user@localhost"); + passwordPage.assertCurrent(); + passwordPage.login("password"); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + // See that user can re-authenticate with the github link present on the page as user has link to github social provider + setTimeOffset(10); + oauth.maxAge("1"); + + loginPage.open(); + + // Username input hidden as well as register and rememberMe. Info message should be present + loginPage.assertCurrent(); + assertUsernameFieldAndOtherFields(false); + assertInfoMessageAboutReAuthenticate(true); + + // Check there is NO password field + Assert.assertThat(true, is(driver.findElements(By.id("password")).isEmpty())); + + // Github present, Google hidden + assertSocialButtonsPresent(true, false); + + // Confirm login with password + loginUsernameOnlyPage.clickSubmitButton(); + + // Login with password. Info message should not be there anymore + passwordPage.assertCurrent(); + passwordPage.login("password"); + assertInfoMessageAboutReAuthenticate(false); + Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); + + // Remove link and flow + user.removeFederatedIdentity("github"); + BrowserFlowTest.revertFlows(testRealm(), "browser - identity first"); + } + + private void setupIdentityFirstFlow() { + String newFlowAlias = "browser - identity first"; + testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session).copyBrowserFlow(newFlowAlias)); + testingClient.server("test").run(session -> FlowUtil.inCurrentRealm(session) + .selectFlow(newFlowAlias) + .inForms(forms -> forms + .clear() + .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, UsernameFormFactory.PROVIDER_ID) + .addAuthenticatorExecution(AuthenticationExecutionModel.Requirement.REQUIRED, PasswordFormFactory.PROVIDER_ID) + ).defineAsBrowserFlow() // Activate this new flow + ); + } + + + private void assertUsernameFieldAndOtherFields(boolean expectPresent) { + Assert.assertThat(expectPresent, is(loginPage.isUsernameInputPresent())); + Assert.assertThat(expectPresent, is(loginPage.isRegisterLinkPresent())); + Assert.assertThat(expectPresent, is(loginPage.isRememberMeCheckboxPresent())); + } + + private void assertSocialButtonsPresent(boolean expectGithubPresent, boolean expectGooglePresent) { + Assert.assertThat(expectGithubPresent, is(loginPage.isSocialButtonPresent("github"))); + Assert.assertThat(expectGooglePresent, is(loginPage.isSocialButtonPresent("google"))); + } + + private void assertInfoMessageAboutReAuthenticate(boolean expectPresent) { + Matcher expectedInfo = expectPresent ? is("Please re-authenticate to continue") : Matchers.nullValue(String.class); + Assert.assertThat(loginPage.getInfoMessage(), expectedInfo); + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java index d419836ad82..00656691693 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/LogoutTest.java @@ -17,6 +17,7 @@ package org.keycloak.testsuite.oauth; +import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -37,6 +38,7 @@ import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.util.*; import java.util.List; @@ -62,6 +64,9 @@ public class LogoutTest extends AbstractKeycloakTest { @Rule public AssertEvents events = new AssertEvents(this); + @Page + protected LoginPage loginPage; + @Override public void beforeAbstractKeycloakTest() throws Exception { super.beforeAbstractKeycloakTest(); @@ -160,7 +165,8 @@ public class LogoutTest extends AbstractKeycloakTest { setTimeOffset(2); - oauth.fillLoginForm("test-user@localhost", "password"); + WaitUtils.waitForPageToLoad(); + loginPage.login("password"); Assert.assertFalse(loginPage.isCurrent()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java index 218cfd00f45..c2b37f1d56a 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/RefreshTokenTest.java @@ -753,7 +753,8 @@ public class RefreshTokenTest extends AbstractKeycloakTest { setTimeOffset(2); // Continue with login - oauth.fillLoginForm("test-user@localhost", "password"); + WaitUtils.waitForPageToLoad(); + loginPage.login("password"); assertFalse(loginPage.isCurrent()); @@ -786,7 +787,8 @@ public class RefreshTokenTest extends AbstractKeycloakTest { setTimeOffset(2); // Continue with login - oauth.fillLoginForm("test-user@localhost", "password"); + WaitUtils.waitForPageToLoad(); + loginPage.login("password"); assertFalse(loginPage.isCurrent()); @@ -822,7 +824,8 @@ public class RefreshTokenTest extends AbstractKeycloakTest { setTimeOffset(2); // Continue with login - oauth.fillLoginForm("test-user@localhost", "password"); + WaitUtils.waitForPageToLoad(); + loginPage.login("password"); assertFalse(loginPage.isCurrent()); @@ -1500,6 +1503,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest { driver.navigate().to(loginFormUri); loginPage.assertCurrent(); + Assert.assertEquals("test-user@localhost", loginPage.getAttemptedUsername()); return refreshToken; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java index 750cf2564da..219888573c2 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/TokenIntrospectionTest.java @@ -27,6 +27,7 @@ import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicNameValuePair; +import org.jboss.arquillian.graphene.page.Page; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; @@ -51,10 +52,12 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.oidc.OIDCScopeTest; import org.keycloak.testsuite.oidc.AbstractOIDCScopeTest; +import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.util.KeycloakModelUtils; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse; import org.keycloak.testsuite.util.TokenSignatureUtil; +import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.JsonSerialization; @@ -80,6 +83,9 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest { @Rule public AssertEvents events = new AssertEvents(this); + @Page + protected LoginPage loginPage; + @Override public void configureTestRealm(RealmRepresentation testRealm) { ClientRepresentation confApp = KeycloakModelUtils.createClient(testRealm, "confidential-cli"); @@ -227,7 +233,8 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest { setTimeOffset(2); - oauth.fillLoginForm("test-user@localhost", "password"); + WaitUtils.waitForPageToLoad(); + loginPage.login("password"); events.expectLogin().assertEvent(); Assert.assertFalse(loginPage.isCurrent()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java index 787ac1be1ac..e667a73631d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java @@ -101,6 +101,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -222,8 +223,11 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest // Now open login form with maxAge=1 oauth.maxAge("1"); - // Assert I need to login again through the login form - oauth.doLogin("test-user@localhost", "password"); + // Assert I need to login again through the login form. But username field is not present + oauth.openLoginForm(); + loginPage.assertCurrent(); + Assert.assertThat(false, is(loginPage.isUsernameInputPresent())); + loginPage.login("password"); loginEvent = events.expectLogin().assertEvent(); idToken = sendTokenRequestAndGetIDToken(loginEvent); @@ -399,9 +403,9 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest // Assert need to re-authenticate with prompt=login driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=login"); - loginPage.assertCurrent(); - loginPage.login("test-user@localhost", "password"); + Assert.assertThat(false, is(loginPage.isUsernameInputPresent())); + loginPage.login("password"); Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); @@ -416,31 +420,6 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest } - @Test - public void promptLoginDifferentUser() throws Exception { - String sss = oauth.getLoginFormUrl(); - System.out.println(sss); - - // Login user - loginPage.open(); - loginPage.login("test-user@localhost", "password"); - Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType()); - - EventRepresentation loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent(); - IDToken idToken = sendTokenRequestAndGetIDToken(loginEvent); - - // Assert need to re-authenticate with prompt=login - driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=login"); - - // Authenticate as different user - loginPage.assertCurrent(); - loginPage.login("john-doh@localhost", "password"); - - errorPage.assertCurrent(); - Assert.assertTrue(errorPage.getError().startsWith("You are already authenticated as different user")); - } - - // prompt=consent @Test public void promptConsent() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java index 0ef72b787eb..e8d15db64a5 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/UserInfoTest.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.oidc; import org.hamcrest.CoreMatchers; import org.hamcrest.Matchers; +import org.jboss.arquillian.graphene.page.Page; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -41,6 +42,7 @@ import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.util.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; @@ -59,6 +61,7 @@ import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.TokenSignatureUtil; import org.keycloak.testsuite.util.UserInfoClientUtil; +import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.JsonSerialization; import org.keycloak.utils.MediaType; @@ -95,6 +98,9 @@ public class UserInfoTest extends AbstractKeycloakTest { @Rule public AssertEvents events = new AssertEvents(this); + @Page + protected LoginPage loginPage; + @Override public void beforeAbstractKeycloakTest() throws Exception { super.beforeAbstractKeycloakTest(); @@ -380,7 +386,8 @@ public class UserInfoTest extends AbstractKeycloakTest { setTimeOffset(2); - oauth.fillLoginForm("test-user@localhost", "password"); + WaitUtils.waitForPageToLoad(); + loginPage.login("password"); events.expectLogin().assertEvent(); Assert.assertFalse(loginPage.isCurrent()); diff --git a/themes/src/main/resources/theme/base/login/login-username.ftl b/themes/src/main/resources/theme/base/login/login-username.ftl index 6e481e0ea4a..366090cad59 100755 --- a/themes/src/main/resources/theme/base/login/login-username.ftl +++ b/themes/src/main/resources/theme/base/login/login-username.ftl @@ -8,34 +8,28 @@ <#if realm.password>
-
- + <#if !usernameHidden??> +
+ - <#if usernameEditDisabled??> - - <#else> - - <#if messagesPerField.existsError('username')> - - ${kcSanitize(messagesPerField.get('username'))?no_esc} - - -
+ <#if messagesPerField.existsError('username')> + + ${kcSanitize(messagesPerField.get('username'))?no_esc} + + +
+
- <#if realm.rememberMe && !usernameEditDisabled??> + <#if realm.rememberMe && !usernameHidden??>