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();
|
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
|
* Generates access code and updates clientsession timestamp
|
||||||
* Access codes must be included in form action callbacks as a query parameter.
|
* 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 IDENTITY_PROVIDER_BROKER_CONTEXT = "identityProviderBrokerCtx";
|
||||||
|
|
||||||
String USERNAME_EDIT_DISABLED = "usernameEditDisabled";
|
String USERNAME_HIDDEN = "usernameHidden";
|
||||||
|
|
||||||
String REGISTRATION_DISABLED = "registrationDisabled";
|
String REGISTRATION_DISABLED = "registrationDisabled";
|
||||||
|
|
||||||
|
|
|
@ -105,6 +105,11 @@ public class AuthenticationProcessor {
|
||||||
*/
|
*/
|
||||||
protected ForwardedFormMessageStore forwardedSuccessMessageStore = new ForwardedFormMessageStore(ForwardedFormMessageType.SUCCESS);
|
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
|
// Used for client authentication
|
||||||
protected ClientModel client;
|
protected ClientModel client;
|
||||||
protected Map<String, String> clientAuthAttributes = new HashMap<>();
|
protected Map<String, String> clientAuthAttributes = new HashMap<>();
|
||||||
|
@ -232,6 +237,11 @@ public class AuthenticationProcessor {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AuthenticationProcessor setForwardedInfoMessage(FormMessage forwardedInfoMessage) {
|
||||||
|
this.forwardedInfoMessageStore.setForwardedMessage(forwardedInfoMessage);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public String generateCode() {
|
public String generateCode() {
|
||||||
ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getAuthenticationSession());
|
ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getAuthenticationSession());
|
||||||
authenticationSession.getParentSession().setTimestamp(Time.currentTime());
|
authenticationSession.getParentSession().setTimestamp(Time.currentTime());
|
||||||
|
@ -528,6 +538,9 @@ public class AuthenticationProcessor {
|
||||||
} else if (getForwardedSuccessMessage() != null) {
|
} else if (getForwardedSuccessMessage() != null) {
|
||||||
provider.addSuccess(getForwardedSuccessMessage());
|
provider.addSuccess(getForwardedSuccessMessage());
|
||||||
forwardedSuccessMessageStore.removeForwardedMessage();
|
forwardedSuccessMessageStore.removeForwardedMessage();
|
||||||
|
} else if (getForwardedInfoMessage() != null) {
|
||||||
|
provider.setInfo(getForwardedInfoMessage().getMessage(), getForwardedInfoMessage().getParameters());
|
||||||
|
forwardedInfoMessageStore.removeForwardedMessage();
|
||||||
}
|
}
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
@ -642,6 +655,16 @@ public class AuthenticationProcessor {
|
||||||
return AuthenticationProcessor.this.forwardedSuccessMessageStore.getForwardedMessage();
|
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() {
|
public FormMessage getErrorMessage() {
|
||||||
return errorMessage;
|
return errorMessage;
|
||||||
}
|
}
|
||||||
|
@ -1139,7 +1162,7 @@ public class AuthenticationProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum ForwardedFormMessageType {
|
private enum ForwardedFormMessageType {
|
||||||
SUCCESS("fwMessageSuccess"), ERROR("fwMessageError");
|
SUCCESS("fwMessageSuccess"), ERROR("fwMessageError"), INFO("fwMessageInfo");
|
||||||
|
|
||||||
private final String key;
|
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 REGISTRATION_FORM_ACTION = "registration_form";
|
||||||
public static final String ATTEMPTED_USERNAME = "ATTEMPTED_USERNAME";
|
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
|
@Override
|
||||||
public void action(AuthenticationFlowContext context) {
|
public void action(AuthenticationFlowContext context) {
|
||||||
|
|
||||||
|
@ -142,18 +145,30 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
|
||||||
|
|
||||||
|
|
||||||
public boolean validateUserAndPassword(AuthenticationFlowContext context, MultivaluedMap<String, String> inputData) {
|
public boolean validateUserAndPassword(AuthenticationFlowContext context, MultivaluedMap<String, String> inputData) {
|
||||||
context.clearUser();
|
|
||||||
UserModel user = getUser(context, inputData);
|
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) {
|
public boolean validateUser(AuthenticationFlowContext context, MultivaluedMap<String, String> inputData) {
|
||||||
context.clearUser();
|
|
||||||
UserModel user = getUser(context, inputData);
|
UserModel user = getUser(context, inputData);
|
||||||
return user != null && validateUser(context, user, inputData);
|
return user != null && validateUser(context, user, inputData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private UserModel getUser(AuthenticationFlowContext context, MultivaluedMap<String, String> 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);
|
String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME);
|
||||||
if (username == null) {
|
if (username == null) {
|
||||||
context.getEvent().error(Errors.USER_NOT_FOUND);
|
context.getEvent().error(Errors.USER_NOT_FOUND);
|
||||||
|
@ -203,10 +218,6 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
|
||||||
return true;
|
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) {
|
public boolean validatePassword(AuthenticationFlowContext context, UserModel user, MultivaluedMap<String, String> inputData, boolean clearUser) {
|
||||||
String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
|
String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
|
||||||
if (password == null || password.isEmpty()) {
|
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) {
|
private boolean badPasswordHandler(AuthenticationFlowContext context, UserModel user, boolean clearUser,boolean isEmptyPassword) {
|
||||||
context.getEvent().user(user);
|
context.getEvent().user(user);
|
||||||
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
|
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);
|
Response challengeResponse = challenge(context, getDefaultChallengeMessage(context), FIELD_PASSWORD);
|
||||||
if(isEmptyPassword) {
|
if(isEmptyPassword) {
|
||||||
context.forceChallenge(challengeResponse);
|
context.forceChallenge(challengeResponse);
|
||||||
|
@ -252,6 +270,15 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
protected String getDefaultChallengeMessage(AuthenticationFlowContext context) {
|
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.models.UserModel;
|
||||||
import org.keycloak.protocol.LoginProtocol;
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,6 +56,7 @@ public class CookieAuthenticator implements Authenticator {
|
||||||
if (protocol.requireReauthentication(authResult.getSession(), authSession)) {
|
if (protocol.requireReauthentication(authResult.getSession(), authSession)) {
|
||||||
// Full re-authentication, so we start with no loa
|
// Full re-authentication, so we start with no loa
|
||||||
authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(Constants.NO_LOA));
|
authSession.setAuthNote(Constants.LEVEL_OF_AUTHENTICATION, String.valueOf(Constants.NO_LOA));
|
||||||
|
context.setForwardedInfoMessage(Messages.REAUTHENTICATE);
|
||||||
context.attempted();
|
context.attempted();
|
||||||
} else if (!AuthenticatorUtil.isLevelOfAuthenticationSatisfied(authSession)) {
|
} else if (!AuthenticatorUtil.isLevelOfAuthenticationSatisfied(authSession)) {
|
||||||
// Step-up authentication, we keep the loa from the existing user session.
|
// Step-up authentication, we keep the loa from the existing user session.
|
||||||
|
|
|
@ -17,8 +17,12 @@
|
||||||
|
|
||||||
package org.keycloak.authentication.authenticators.browser;
|
package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
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 org.keycloak.services.messages.Messages;
|
||||||
|
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
@ -26,6 +30,20 @@ import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
public final class UsernameForm extends UsernamePasswordForm {
|
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
|
@Override
|
||||||
protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
|
protected boolean validateForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData) {
|
||||||
return validateUser(context, formData);
|
return validateUser(context, formData);
|
||||||
|
|
|
@ -62,12 +62,20 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl
|
||||||
|
|
||||||
String rememberMeUsername = AuthenticationManager.getRememberMeUsername(context.getRealm(), context.getHttpRequest().getHttpHeaders());
|
String rememberMeUsername = AuthenticationManager.getRememberMeUsername(context.getRealm(), context.getHttpRequest().getHttpHeaders());
|
||||||
|
|
||||||
if (loginHint != null || rememberMeUsername != null) {
|
if (context.getUser() != null) {
|
||||||
if (loginHint != null) {
|
LoginFormsProvider form = context.form();
|
||||||
formData.add(AuthenticationManager.FORM_USERNAME, loginHint);
|
form.setAttribute(LoginFormsProvider.USERNAME_HIDDEN, true);
|
||||||
} else {
|
form.setAttribute(LoginFormsProvider.REGISTRATION_DISABLED, true);
|
||||||
formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername);
|
context.getAuthenticationSession().setAuthNote(USER_SET_BEFORE_USERNAME_PASSWORD_AUTH, "true");
|
||||||
formData.add("rememberMe", "on");
|
} 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);
|
Response challengeResponse = challenge(context, formData);
|
||||||
|
|
|
@ -43,50 +43,33 @@ import java.util.stream.Stream;
|
||||||
*/
|
*/
|
||||||
public class LoginFormsUtil {
|
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) {
|
public static List<IdentityProviderModel> filterIdentityProviders(Stream<IdentityProviderModel> providers, KeycloakSession session, AuthenticationFlowContext context) {
|
||||||
|
|
||||||
if (context != null) {
|
if (context != null) {
|
||||||
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
|
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
|
||||||
|
|
||||||
if (serializedCtx != null) {
|
final IdentityProviderModel existingIdp = (serializedCtx == null) ? null : serializedCtx.deserialize(session, authSession).getIdpConfig();
|
||||||
IdentityProviderModel idp = serializedCtx.deserialize(session, authSession).getIdpConfig();
|
|
||||||
return providers
|
final Set<String> federatedIdentities;
|
||||||
.filter(p -> !Objects.equals(p.getAlias(), idp.getAlias()))
|
if (context.getUser() != null) {
|
||||||
.collect(Collectors.toList());
|
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());
|
return providers.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,8 @@ public class Messages {
|
||||||
public static final String DISPLAY_UNSUPPORTED = "displayUnsupported";
|
public static final String DISPLAY_UNSUPPORTED = "displayUnsupported";
|
||||||
public static final String LOGIN_TIMEOUT = "loginTimeout";
|
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_USER = "invalidUserMessage";
|
||||||
|
|
||||||
public static final String INVALID_USERNAME = "invalidUsernameMessage";
|
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.RealmModel;
|
||||||
import org.keycloak.models.UserConsentModel;
|
import org.keycloak.models.UserConsentModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.utils.AuthenticationFlowResolver;
|
import org.keycloak.models.utils.AuthenticationFlowResolver;
|
||||||
import org.keycloak.models.utils.FormMessage;
|
import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
@ -231,6 +232,14 @@ public class LoginActionsService {
|
||||||
flowPath = AUTHENTICATE_PATH;
|
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);
|
AuthenticationProcessor.resetFlow(authSession, flowPath);
|
||||||
|
|
||||||
URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getClient().getClientId(), tabId);
|
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!
|
* OAuth grant page. You should not invoked this directly!
|
||||||
*
|
*
|
||||||
* @param formData
|
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@Path("consent")
|
@Path("consent")
|
||||||
|
|
|
@ -122,6 +122,18 @@ public class LoginPage extends LanguageComboboxAwarePage {
|
||||||
return usernameInput.isEnabled();
|
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() {
|
public String getPassword() {
|
||||||
return passwordInput.getAttribute("value");
|
return passwordInput.getAttribute("value");
|
||||||
}
|
}
|
||||||
|
@ -154,7 +166,11 @@ public class LoginPage extends LanguageComboboxAwarePage {
|
||||||
return loginSuccessMessage != null ? loginSuccessMessage.getText() : null;
|
return loginSuccessMessage != null ? loginSuccessMessage.getText() : null;
|
||||||
}
|
}
|
||||||
public String getInfoMessage() {
|
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));
|
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() {
|
public void resetPassword() {
|
||||||
clickLink(resetPasswordLink);
|
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
|
* 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.drone.api.annotation.Drone;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
|
import org.junit.Assert;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.admin.client.resource.UserResource;
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
|
@ -114,7 +115,8 @@ public class AppInitiatedActionResetPasswordTest extends AbstractAppInitiatedAct
|
||||||
doAIA();
|
doAIA();
|
||||||
|
|
||||||
loginPage.assertCurrent();
|
loginPage.assertCurrent();
|
||||||
loginPage.login("test-user@localhost", "password");
|
Assert.assertEquals("test-user@localhost", loginPage.getAttemptedUsername());
|
||||||
|
loginPage.login("password");
|
||||||
|
|
||||||
changePasswordPage.assertCurrent();
|
changePasswordPage.assertCurrent();
|
||||||
assertTrue(changePasswordPage.isCancelDisplayed());
|
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
|
@Test
|
||||||
@DisableFeature(value = Profile.Feature.ACCOUNT2, skipRestart = true) // TODO remove this (KEYCLOAK-16228)
|
@DisableFeature(value = Profile.Feature.ACCOUNT2, skipRestart = true) // TODO remove this (KEYCLOAK-16228)
|
||||||
public void testAccountLinkingExpired() throws Exception {
|
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();
|
String appUri = tokenMinTTLPage.getUriBuilder().queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN).build().toString();
|
||||||
URLUtils.navigateToUri(appUri);
|
URLUtils.navigateToUri(appUri);
|
||||||
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
|
assertCurrentUrlStartsWithLoginUrlOf(testRealmPage);
|
||||||
testRealmLoginPage.form().login("bburke@redhat.com", "password");
|
WaitUtils.waitForPageToLoad();
|
||||||
|
testRealmLoginPage.form().setPassword("password");
|
||||||
|
testRealmLoginPage.form().login();
|
||||||
AccessToken token = tokenMinTTLPage.getAccessToken();
|
AccessToken token = tokenMinTTLPage.getAccessToken();
|
||||||
int authTime = token.getAuthTime();
|
int authTime = token.getAuthTime();
|
||||||
assertThat(authTime, is(greaterThanOrEqualTo(currentTime + 10)));
|
assertThat(authTime, is(greaterThanOrEqualTo(currentTime + 10)));
|
||||||
|
|
|
@ -226,6 +226,21 @@ public abstract class AbstractBaseBrokerTest extends AbstractKeycloakTest {
|
||||||
logInWithBroker(bc);
|
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) {
|
protected void logInWithBroker(BrokerConfiguration bc) {
|
||||||
logInWithIdp(bc.getIDPAlias(), bc.getUserLogin(), bc.getUserPassword());
|
logInWithIdp(bc.getIDPAlias(), bc.getUserLogin(), bc.getUserPassword());
|
||||||
}
|
}
|
||||||
|
|
|
@ -461,7 +461,7 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa
|
||||||
waitForPage(driver, "account already exists", false);
|
waitForPage(driver, "account already exists", false);
|
||||||
} catch (Exception e) {
|
} 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
|
// 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);
|
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.
|
// Set time offset. New keys can be downloaded. Check that user is able to login.
|
||||||
setTimeOffset(20);
|
setTimeOffset(20);
|
||||||
|
|
||||||
logInAsUserInIDP();
|
logInAsUserInIDPWithReAuthenticate();
|
||||||
assertLoggedInAccountManagement();
|
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
|
// Even after time offset is user not able to login, because it uses old key hardcoded in identityProvider config
|
||||||
setTimeOffset(20);
|
setTimeOffset(20);
|
||||||
|
|
||||||
logInAsUserInIDP();
|
logInAsUserInIDPWithReAuthenticate();
|
||||||
assertErrorPage("Unexpected error when authenticating with identity provider");
|
assertErrorPage("Unexpected error when authenticating with identity provider");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,7 +193,7 @@ public class KcOIDCBrokerWithSignatureTest extends AbstractBaseBrokerTest {
|
||||||
// Set key id to a valid one
|
// Set key id to a valid one
|
||||||
cfg.setPublicKeySignatureVerifierKeyId(expectedKeyId);
|
cfg.setPublicKeySignatureVerifierKeyId(expectedKeyId);
|
||||||
updateIdentityProvider(idpRep);
|
updateIdentityProvider(idpRep);
|
||||||
logInAsUserInIDP();
|
logInAsUserInIDPWithReAuthenticate();
|
||||||
assertLoggedInAccountManagement();
|
assertLoggedInAccountManagement();
|
||||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
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;
|
package org.keycloak.testsuite.oauth;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -37,6 +38,7 @@ import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
import org.keycloak.testsuite.Assert;
|
import org.keycloak.testsuite.Assert;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
import org.keycloak.testsuite.util.*;
|
import org.keycloak.testsuite.util.*;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -62,6 +64,9 @@ public class LogoutTest extends AbstractKeycloakTest {
|
||||||
@Rule
|
@Rule
|
||||||
public AssertEvents events = new AssertEvents(this);
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeAbstractKeycloakTest() throws Exception {
|
public void beforeAbstractKeycloakTest() throws Exception {
|
||||||
super.beforeAbstractKeycloakTest();
|
super.beforeAbstractKeycloakTest();
|
||||||
|
@ -160,7 +165,8 @@ public class LogoutTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
setTimeOffset(2);
|
setTimeOffset(2);
|
||||||
|
|
||||||
oauth.fillLoginForm("test-user@localhost", "password");
|
WaitUtils.waitForPageToLoad();
|
||||||
|
loginPage.login("password");
|
||||||
|
|
||||||
Assert.assertFalse(loginPage.isCurrent());
|
Assert.assertFalse(loginPage.isCurrent());
|
||||||
|
|
||||||
|
|
|
@ -753,7 +753,8 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
setTimeOffset(2);
|
setTimeOffset(2);
|
||||||
|
|
||||||
// Continue with login
|
// Continue with login
|
||||||
oauth.fillLoginForm("test-user@localhost", "password");
|
WaitUtils.waitForPageToLoad();
|
||||||
|
loginPage.login("password");
|
||||||
|
|
||||||
assertFalse(loginPage.isCurrent());
|
assertFalse(loginPage.isCurrent());
|
||||||
|
|
||||||
|
@ -786,7 +787,8 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
setTimeOffset(2);
|
setTimeOffset(2);
|
||||||
|
|
||||||
// Continue with login
|
// Continue with login
|
||||||
oauth.fillLoginForm("test-user@localhost", "password");
|
WaitUtils.waitForPageToLoad();
|
||||||
|
loginPage.login("password");
|
||||||
|
|
||||||
assertFalse(loginPage.isCurrent());
|
assertFalse(loginPage.isCurrent());
|
||||||
|
|
||||||
|
@ -822,7 +824,8 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
setTimeOffset(2);
|
setTimeOffset(2);
|
||||||
|
|
||||||
// Continue with login
|
// Continue with login
|
||||||
oauth.fillLoginForm("test-user@localhost", "password");
|
WaitUtils.waitForPageToLoad();
|
||||||
|
loginPage.login("password");
|
||||||
|
|
||||||
assertFalse(loginPage.isCurrent());
|
assertFalse(loginPage.isCurrent());
|
||||||
|
|
||||||
|
@ -1500,6 +1503,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
||||||
driver.navigate().to(loginFormUri);
|
driver.navigate().to(loginFormUri);
|
||||||
|
|
||||||
loginPage.assertCurrent();
|
loginPage.assertCurrent();
|
||||||
|
Assert.assertEquals("test-user@localhost", loginPage.getAttemptedUsername());
|
||||||
|
|
||||||
return refreshToken;
|
return refreshToken;
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
import org.apache.http.client.methods.HttpPost;
|
import org.apache.http.client.methods.HttpPost;
|
||||||
import org.apache.http.impl.client.HttpClientBuilder;
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
import org.apache.http.message.BasicNameValuePair;
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
@ -51,10 +52,12 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
import org.keycloak.testsuite.admin.ApiUtil;
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
import org.keycloak.testsuite.oidc.OIDCScopeTest;
|
import org.keycloak.testsuite.oidc.OIDCScopeTest;
|
||||||
import org.keycloak.testsuite.oidc.AbstractOIDCScopeTest;
|
import org.keycloak.testsuite.oidc.AbstractOIDCScopeTest;
|
||||||
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
||||||
import org.keycloak.testsuite.util.OAuthClient;
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
|
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
|
||||||
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
||||||
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
import org.keycloak.util.BasicAuthHelper;
|
import org.keycloak.util.BasicAuthHelper;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
@ -80,6 +83,9 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest {
|
||||||
@Rule
|
@Rule
|
||||||
public AssertEvents events = new AssertEvents(this);
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
ClientRepresentation confApp = KeycloakModelUtils.createClient(testRealm, "confidential-cli");
|
ClientRepresentation confApp = KeycloakModelUtils.createClient(testRealm, "confidential-cli");
|
||||||
|
@ -227,7 +233,8 @@ public class TokenIntrospectionTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
setTimeOffset(2);
|
setTimeOffset(2);
|
||||||
|
|
||||||
oauth.fillLoginForm("test-user@localhost", "password");
|
WaitUtils.waitForPageToLoad();
|
||||||
|
loginPage.login("password");
|
||||||
events.expectLogin().assertEvent();
|
events.expectLogin().assertEvent();
|
||||||
|
|
||||||
Assert.assertFalse(loginPage.isCurrent());
|
Assert.assertFalse(loginPage.isCurrent());
|
||||||
|
|
|
@ -101,6 +101,7 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.is;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertNull;
|
||||||
|
@ -222,8 +223,11 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
|
||||||
// Now open login form with maxAge=1
|
// Now open login form with maxAge=1
|
||||||
oauth.maxAge("1");
|
oauth.maxAge("1");
|
||||||
|
|
||||||
// Assert I need to login again through the login form
|
// Assert I need to login again through the login form. But username field is not present
|
||||||
oauth.doLogin("test-user@localhost", "password");
|
oauth.openLoginForm();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
Assert.assertThat(false, is(loginPage.isUsernameInputPresent()));
|
||||||
|
loginPage.login("password");
|
||||||
loginEvent = events.expectLogin().assertEvent();
|
loginEvent = events.expectLogin().assertEvent();
|
||||||
|
|
||||||
idToken = sendTokenRequestAndGetIDToken(loginEvent);
|
idToken = sendTokenRequestAndGetIDToken(loginEvent);
|
||||||
|
@ -399,9 +403,9 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
|
||||||
|
|
||||||
// Assert need to re-authenticate with prompt=login
|
// Assert need to re-authenticate with prompt=login
|
||||||
driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=login");
|
driver.navigate().to(oauth.getLoginFormUrl() + "&prompt=login");
|
||||||
|
|
||||||
loginPage.assertCurrent();
|
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());
|
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
||||||
loginEvent = events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
|
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
|
// prompt=consent
|
||||||
@Test
|
@Test
|
||||||
public void promptConsent() {
|
public void promptConsent() {
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.testsuite.oidc;
|
||||||
|
|
||||||
import org.hamcrest.CoreMatchers;
|
import org.hamcrest.CoreMatchers;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
@ -41,6 +42,7 @@ import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||||
import org.keycloak.representations.idm.RoleRepresentation;
|
import org.keycloak.representations.idm.RoleRepresentation;
|
||||||
|
import org.keycloak.testsuite.pages.LoginPage;
|
||||||
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
||||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
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.RealmBuilder;
|
||||||
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
||||||
import org.keycloak.testsuite.util.UserInfoClientUtil;
|
import org.keycloak.testsuite.util.UserInfoClientUtil;
|
||||||
|
import org.keycloak.testsuite.util.WaitUtils;
|
||||||
import org.keycloak.util.BasicAuthHelper;
|
import org.keycloak.util.BasicAuthHelper;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
import org.keycloak.utils.MediaType;
|
import org.keycloak.utils.MediaType;
|
||||||
|
@ -95,6 +98,9 @@ public class UserInfoTest extends AbstractKeycloakTest {
|
||||||
@Rule
|
@Rule
|
||||||
public AssertEvents events = new AssertEvents(this);
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beforeAbstractKeycloakTest() throws Exception {
|
public void beforeAbstractKeycloakTest() throws Exception {
|
||||||
super.beforeAbstractKeycloakTest();
|
super.beforeAbstractKeycloakTest();
|
||||||
|
@ -380,7 +386,8 @@ public class UserInfoTest extends AbstractKeycloakTest {
|
||||||
|
|
||||||
setTimeOffset(2);
|
setTimeOffset(2);
|
||||||
|
|
||||||
oauth.fillLoginForm("test-user@localhost", "password");
|
WaitUtils.waitForPageToLoad();
|
||||||
|
loginPage.login("password");
|
||||||
events.expectLogin().assertEvent();
|
events.expectLogin().assertEvent();
|
||||||
|
|
||||||
Assert.assertFalse(loginPage.isCurrent());
|
Assert.assertFalse(loginPage.isCurrent());
|
||||||
|
|
|
@ -8,34 +8,28 @@
|
||||||
<#if realm.password>
|
<#if realm.password>
|
||||||
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}"
|
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}"
|
||||||
method="post">
|
method="post">
|
||||||
<div class="${properties.kcFormGroupClass!}">
|
<#if !usernameHidden??>
|
||||||
<label for="username"
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
|
<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"
|
<input tabindex="1" id="username"
|
||||||
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
|
aria-invalid="<#if messagesPerField.existsError('username')>true</#if>"
|
||||||
class="${properties.kcInputClass!}" name="username"
|
class="${properties.kcInputClass!}" name="username"
|
||||||
value="${(login.username!'')}"
|
value="${(login.username!'')}"
|
||||||
type="text" autofocus autocomplete="off"/>
|
type="text" autofocus autocomplete="off"/>
|
||||||
</#if>
|
|
||||||
|
|
||||||
<#if messagesPerField.existsError('username')>
|
<#if messagesPerField.existsError('username')>
|
||||||
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
|
<span id="input-error-username" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
|
||||||
${kcSanitize(messagesPerField.get('username'))?no_esc}
|
${kcSanitize(messagesPerField.get('username'))?no_esc}
|
||||||
</span>
|
</span>
|
||||||
</#if>
|
</#if>
|
||||||
</div>
|
</div>
|
||||||
|
</#if>
|
||||||
|
|
||||||
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
|
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
|
||||||
<div id="kc-form-options">
|
<div id="kc-form-options">
|
||||||
<#if realm.rememberMe && !usernameEditDisabled??>
|
<#if realm.rememberMe && !usernameHidden??>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<#if login.rememberMe??>
|
<#if login.rememberMe??>
|
||||||
|
|
|
@ -7,12 +7,10 @@
|
||||||
<div id="kc-form-wrapper">
|
<div id="kc-form-wrapper">
|
||||||
<#if realm.password>
|
<#if realm.password>
|
||||||
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post">
|
<form id="kc-form-login" onsubmit="login.disabled = true; return true;" action="${url.loginAction}" method="post">
|
||||||
<div class="${properties.kcFormGroupClass!}">
|
<#if !usernameHidden??>
|
||||||
<label for="username" class="${properties.kcLabelClass!}"><#if !realm.loginWithEmailAllowed>${msg("username")}<#elseif !realm.registrationEmailAsUsername>${msg("usernameOrEmail")}<#else>${msg("email")}</#if></label>
|
<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"
|
<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>"
|
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
|
||||||
/>
|
/>
|
||||||
|
@ -22,8 +20,9 @@
|
||||||
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
|
${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}
|
||||||
</span>
|
</span>
|
||||||
</#if>
|
</#if>
|
||||||
</#if>
|
|
||||||
</div>
|
</div>
|
||||||
|
</#if>
|
||||||
|
|
||||||
<div class="${properties.kcFormGroupClass!}">
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
<label for="password" class="${properties.kcLabelClass!}">${msg("password")}</label>
|
<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"
|
<input tabindex="2" id="password" class="${properties.kcInputClass!}" name="password" type="password" autocomplete="off"
|
||||||
aria-invalid="<#if messagesPerField.existsError('username','password')>true</#if>"
|
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>
|
||||||
|
|
||||||
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
|
<div class="${properties.kcFormGroupClass!} ${properties.kcFormSettingClass!}">
|
||||||
<div id="kc-form-options">
|
<div id="kc-form-options">
|
||||||
<#if realm.rememberMe && !usernameEditDisabled??>
|
<#if realm.rememberMe && !usernameHidden??>
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<label>
|
||||||
<#if login.rememberMe??>
|
<#if login.rememberMe??>
|
||||||
|
|
|
@ -33,6 +33,7 @@ loginTotpTitle=Mobile Authenticator Setup
|
||||||
loginProfileTitle=Update Account Information
|
loginProfileTitle=Update Account Information
|
||||||
loginIdpReviewProfileTitle=Update Account Information
|
loginIdpReviewProfileTitle=Update Account Information
|
||||||
loginTimeout=Your login attempt timed out. Login will start from the beginning.
|
loginTimeout=Your login attempt timed out. Login will start from the beginning.
|
||||||
|
reauthenticate=Please re-authenticate to continue
|
||||||
oauthGrantTitle=Grant Access to {0}
|
oauthGrantTitle=Grant Access to {0}
|
||||||
oauthGrantTitleHtml={0}
|
oauthGrantTitleHtml={0}
|
||||||
oauthGrantInformation=Make sure you trust {0} by learning how {0} will handle your data.
|
oauthGrantInformation=Make sure you trust {0} by learning how {0} will handle your data.
|
||||||
|
|
Loading…
Reference in a new issue