This commit is contained in:
parent
f107f0596e
commit
d9c8cb30a5
27 changed files with 606 additions and 168 deletions
|
@ -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.
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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<String, String> 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;
|
||||
|
||||
|
|
|
@ -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<String, String> 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<String, String> inputData) {
|
||||
context.clearUser();
|
||||
UserModel user = getUser(context, inputData);
|
||||
return user != null && validateUser(context, user, inputData);
|
||||
}
|
||||
|
||||
private UserModel getUser(AuthenticationFlowContext context, MultivaluedMap<String, String> 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<String, String> 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<String, String> inputData) {
|
||||
return validatePassword(context, user, inputData, true);
|
||||
}
|
||||
|
||||
public boolean validatePassword(AuthenticationFlowContext context, UserModel user, MultivaluedMap<String, String> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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<IdentityProviderModel> 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<String, String> formData) {
|
||||
return validateUser(context, formData);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<IdentityProviderModel> filterIdentityProvidersByUser(List<IdentityProviderModel> providers, KeycloakSession session, RealmModel realm,
|
||||
Map<String, Object> attributes, MultivaluedMap<String, String> 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<String> federatedIdentities = session.users().getFederatedIdentitiesStream(realm, user)
|
||||
.map(federatedIdentityModel -> federatedIdentityModel.getIdentityProvider())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
List<IdentityProviderModel> result = new LinkedList<>();
|
||||
for (IdentityProviderModel idp : providers) {
|
||||
if (federatedIdentities.contains(idp.getAlias())) {
|
||||
result.add(idp);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return providers;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<IdentityProviderModel> filterIdentityProviders(Stream<IdentityProviderModel> 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<String> 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());
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -32,6 +32,11 @@ public class LoginUsernameOnlyPage extends LoginPage {
|
|||
}
|
||||
}
|
||||
|
||||
// Click button without fill anything
|
||||
public void clickSubmitButton() {
|
||||
submitButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Not supported for this implementation
|
||||
*
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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<FederatedIdentityRepresentation> 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 {
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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<IdentityProviderRepresentation> 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<RealmRepresentation> 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<String> expectedInfo = expectPresent ? is("Please re-authenticate to continue") : Matchers.nullValue(String.class);
|
||||
Assert.assertThat(loginPage.getInfoMessage(), expectedInfo);
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -8,34 +8,28 @@
|
|||
<#if realm.password>
|
||||
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}"
|
||||
method="post">
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<label for="username"
|
||||
class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
|
||||
<#if !usernameHidden??>
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<label for="username"
|
||||
class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
|
||||
|
||||
<#if usernameEditDisabled??>
|
||||
<input tabindex="1" id="username"
|
||||
aria-invalid="<#if message?has_content && message.type = 'error'>true</#if>"
|
||||
class="${properties.kcInputClass!}" name="username"
|
||||
value="${(login.username!'')}"
|
||||
type="text" disabled/>
|
||||
<#else>
|
||||
<input tabindex="1" id="username"
|
||||
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
|
||||
class="${properties.kcInputClass!}" name="username"
|
||||
value="${(login.username!'')}"
|
||||
type="text" autofocus autocomplete="off"/>
|
||||
</#if>
|
||||
|
||||
<#if messagesPerField.existsError('username')>
|
||||
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
|
||||
${kcSanitize(messagesPerField.get('username'))?no_esc}
|
||||
</span>
|
||||
</#if>
|
||||
</div>
|
||||
<#if messagesPerField.existsError('username')>
|
||||
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
|
||||
${kcSanitize(messagesPerField.get('username'))?no_esc}
|
||||
</span>
|
||||
</#if>
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
|
||||
<div id="kc-form-options">
|
||||
<#if realm.rememberMe && !usernameEditDisabled??>
|
||||
<#if realm.rememberMe && !usernameHidden??>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<#if login.rememberMe??>
|
||||
|
|
|
@ -7,12 +7,10 @@
|
|||
<div id="kc-form-wrapper">
|
||||
<#if realm.password>
|
||||
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post">
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
|
||||
<#if !usernameHidden??>
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
|
||||
|
||||
<#if usernameEditDisabled??>
|
||||
<input tabindex="1" id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')}" type="text" disabled />
|
||||
<#else>
|
||||
<input tabindex="1" id="username" class="${properties.kcInputClass!}" name="username" value="${(login.username!'')}" type="text" autofocus autocomplete="off"
|
||||
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
|
||||
/>
|
||||
|
@ -22,8 +20,9 @@
|
|||
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
|
||||
</span>
|
||||
</#if>
|
||||
</#if>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</#if>
|
||||
|
||||
<div class="${properties.kcFormGroupClass!}">
|
||||
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
|
||||
|
@ -31,11 +30,18 @@
|
|||
<input tabindex="2" id="password" class="${properties.kcInputClass!}" name="password" type="password" autocomplete="off"
|
||||
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
|
||||
/>
|
||||
|
||||
<#if usernameHidden?? && messagesPerField.existsError('username','password')>
|
||||
<span id="input-error" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
|
||||
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
|
||||
</span>
|
||||
</#if>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
|
||||
<div id="kc-form-options">
|
||||
<#if realm.rememberMe && !usernameEditDisabled??>
|
||||
<#if realm.rememberMe && !usernameHidden??>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<#if login.rememberMe??>
|
||||
|
|
|
@ -33,6 +33,7 @@ loginTotpTitle=Mobile Authenticator Setup
|
|||
loginProfileTitle=Update Account Information
|
||||
loginIdpReviewProfileTitle=Update Account Information
|
||||
loginTimeout=Your login attempt timed out. Login will start from the beginning.
|
||||
reauthenticate=Please re-authenticate to continue
|
||||
oauthGrantTitle=Grant Access to {0}
|
||||
oauthGrantTitleHtml={0}
|
||||
oauthGrantInformation=Make sure you trust {0} by learning how {0} will handle your data.
|
||||
|
|
Loading…
Reference in a new issue