KEYCLOAK-4626 Support for sticky sessions with AUTH_SESSION_ID cookie. Clustering tests with embedded undertow. Last fixes.

This commit is contained in:
mposolda 2017-05-04 10:42:43 +02:00
parent b8262a9f02
commit 7d8796e614
57 changed files with 1543 additions and 260 deletions

View file

@ -33,6 +33,7 @@
<module name="org.infinispan.commons"/>
<module name="org.infinispan.cachestore.remote"/>
<module name="org.infinispan.client.hotrod"/>
<module name="org.jgroups"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies>

View file

@ -27,6 +27,7 @@ import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
/**

View file

@ -133,6 +133,12 @@ and provide password `secret`
Now when you access `http://localhost:8081/auth/realms/master/account` you should be logged in automatically as user `hnelson` .
Simple loadbalancer
-------------------
You can run class `SimpleUndertowLoadBalancer` from IDE. By default, it executes the embedded undertow loadbalancer running on `http://localhost:8180`, which communicates with 2 backend Keycloak nodes
running on `http://localhost:8181` and `http://localhost:8182` . See javadoc for more details.
Create many users or offline sessions
-------------------------------------

View file

@ -19,6 +19,8 @@ package org.keycloak.connections.infinispan;
import java.util.concurrent.TimeUnit;
import org.infinispan.commons.util.FileLookup;
import org.infinispan.commons.util.FileLookupFactory;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
@ -28,10 +30,12 @@ import org.infinispan.eviction.EvictionType;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder;
import org.infinispan.remoting.transport.jgroups.JGroupsTransport;
import org.infinispan.transaction.LockingMode;
import org.infinispan.transaction.TransactionMode;
import org.infinispan.transaction.lookup.DummyTransactionManagerLookup;
import org.jboss.logging.Logger;
import org.jgroups.JChannel;
import org.keycloak.Config;
import org.keycloak.cluster.infinispan.KeycloakHotRodMarshallerFactory;
import org.keycloak.models.KeycloakSession;
@ -139,7 +143,8 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
boolean allowDuplicateJMXDomains = config.getBoolean("allowDuplicateJMXDomains", true);
if (clustered) {
gcb.transport().defaultTransport();
String nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
configureTransport(gcb, nodeName);
}
gcb.globalJmxStatistics().allowDuplicateDomains(allowDuplicateJMXDomains);
@ -184,6 +189,13 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME, sessionCacheConfiguration);
cacheManager.defineConfiguration(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, sessionCacheConfiguration);
// Retrieve caches to enforce rebalance
cacheManager.getCache(InfinispanConnectionProvider.SESSION_CACHE_NAME, true);
cacheManager.getCache(InfinispanConnectionProvider.OFFLINE_SESSION_CACHE_NAME, true);
cacheManager.getCache(InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME, true);
cacheManager.getCache(InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME, true);
cacheManager.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME, true);
ConfigurationBuilder replicationConfigBuilder = new ConfigurationBuilder();
if (clustered) {
replicationConfigBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC);
@ -288,4 +300,26 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
return cb.build();
}
protected void configureTransport(GlobalConfigurationBuilder gcb, String nodeName) {
if (nodeName == null) {
gcb.transport().defaultTransport();
} else {
FileLookup fileLookup = FileLookupFactory.newInstance();
try {
// Compatibility with Wildfly
JChannel channel = new JChannel(fileLookup.lookupFileLocation("default-configs/default-jgroups-udp.xml", this.getClass().getClassLoader()));
channel.setName(nodeName);
JGroupsTransport transport = new JGroupsTransport(channel);
gcb.transport().nodeName(nodeName);
gcb.transport().transport(transport);
logger.infof("Configured jgroups transport with the channel name: %s", nodeName);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}

View file

@ -49,6 +49,9 @@ public interface InfinispanConnectionProvider extends Provider {
int KEYS_CACHE_DEFAULT_MAX = 1000;
int KEYS_CACHE_MAX_IDLE_SECONDS = 3600;
// System property used on Wildfly to identify distributedCache address and sticky session route
String JBOSS_NODE_NAME = "jboss.node.name";
<K, V> Cache<K, V> getCache(String name);

View file

@ -96,7 +96,7 @@ public class AuthenticatedClientSessionAdapter implements AuthenticatedClientSes
@Override
public String getId() {
return entity.getId();
return null;
}
@Override

View file

@ -0,0 +1,78 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache;
import org.infinispan.distribution.DistributionManager;
import org.infinispan.remoting.transport.Address;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.sessions.StickySessionEncoderProvider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanStickySessionEncoderProvider implements StickySessionEncoderProvider {
private final KeycloakSession session;
private final String myNodeName;
public InfinispanStickySessionEncoderProvider(KeycloakSession session, String myNodeName) {
this.session = session;
this.myNodeName = myNodeName;
}
@Override
public String encodeSessionId(String sessionId) {
String nodeName = getNodeName(sessionId);
if (nodeName != null) {
return sessionId + '.' + nodeName;
} else {
return sessionId;
}
}
@Override
public String decodeSessionId(String encodedSessionId) {
int index = encodedSessionId.indexOf('.');
return index == -1 ? encodedSessionId : encodedSessionId.substring(0, index);
}
@Override
public void close() {
}
private String getNodeName(String sessionId) {
InfinispanConnectionProvider ispnProvider = session.getProvider(InfinispanConnectionProvider.class);
Cache cache = ispnProvider.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME);
DistributionManager distManager = cache.getAdvancedCache().getDistributionManager();
if (distManager != null) {
// Sticky session to the node, who owns this authenticationSession
Address address = distManager.getPrimaryLocation(sessionId);
return address.toString();
} else {
// Fallback to jbossNodeName if authSession cache is local
return myNodeName;
}
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.sessions.infinispan;
import org.keycloak.Config;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.sessions.StickySessionEncoderProvider;
import org.keycloak.sessions.StickySessionEncoderProviderFactory;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class InfinispanStickySessionEncoderProviderFactory implements StickySessionEncoderProviderFactory {
private String myNodeName;
@Override
public StickySessionEncoderProvider create(KeycloakSession session) {
return new InfinispanStickySessionEncoderProvider(session, myNodeName);
}
@Override
public void init(Config.Scope config) {
myNodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME));
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "infinispan";
}
}

View file

@ -26,7 +26,6 @@ import java.util.Set;
*/
public class AuthenticatedClientSessionEntity implements Serializable {
private String id;
private String authMethod;
private String redirectUri;
private int timestamp;
@ -36,14 +35,6 @@ public class AuthenticatedClientSessionEntity implements Serializable {
private Set<String> protocolMappers;
private Map<String, String> notes;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getAuthMethod() {
return authMethod;
}

View file

@ -0,0 +1,18 @@
#
# Copyright 2016 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.keycloak.models.sessions.infinispan.InfinispanStickySessionEncoderProviderFactory

View file

@ -0,0 +1,31 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sessions;
import org.keycloak.provider.Provider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface StickySessionEncoderProvider extends Provider {
String encodeSessionId(String sessionId);
String decodeSessionId(String encodedSessionId);
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sessions;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public interface StickySessionEncoderProviderFactory extends ProviderFactory<StickySessionEncoderProvider> {
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.sessions;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class StickySessionEncoderSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "stickySessionEncoder";
}
@Override
public Class<? extends Provider> getProviderClass() {
return StickySessionEncoderProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return StickySessionEncoderProviderFactory.class;
}
}

View file

@ -34,6 +34,7 @@ org.keycloak.scripting.ScriptingSpi
org.keycloak.services.managers.BruteForceProtectorSpi
org.keycloak.services.resource.RealmResourceSPI
org.keycloak.sessions.AuthenticationSessionSpi
org.keycloak.sessions.StickySessionEncoderSpi
org.keycloak.protocol.ClientInstallationSpi
org.keycloak.protocol.LoginProtocolSpi
org.keycloak.protocol.ProtocolMapperSpi

View file

@ -115,7 +115,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
authSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString(); // TODO:mposolda It seems that this should be taken from client rather then hardcoded to account?
String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
authSession.setRedirectUri(redirectUri);
authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
authSession.setClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);

View file

@ -86,6 +86,7 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
return tokenContext.getSession().getProvider(LoginFormsProvider.class)
.setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, token.getIdentityProviderAlias(), token.getIdentityProviderUsername())
.setAttribute("skipLink", true)
.createInfoPage();
}

View file

@ -47,9 +47,6 @@ public abstract class AbstractIdpAuthenticator implements Authenticator {
// The clientSession note flag to indicate that email provided by identityProvider was changed on updateProfile page
public static final String UPDATE_PROFILE_EMAIL_CHANGED = "UPDATE_PROFILE_EMAIL_CHANGED";
// The clientSession note flag to indicate if re-authentication after first broker login happened in different browser window. This can happen for example during email verification
public static final String IS_DIFFERENT_BROWSER = "IS_DIFFERENT_BROWSER"; // TODO:mposolda can reuse the END_AFTER_REQUIRED_ACTIONS instead?
// The clientSession note flag to indicate that updateProfile page will be always displayed even if "updateProfileOnFirstLogin" is off
public static final String ENFORCE_UPDATE_PROFILE = "ENFORCE_UPDATE_PROFILE";

View file

@ -31,6 +31,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@ -81,7 +82,13 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
UserModel existingUser = getExistingUser(session, realm, authSession);
// Do not allow resending e-mail by simple page refresh
if (! Objects.equals(authSession.getAuthNote(Constants.VERIFY_EMAIL_KEY), existingUser.getEmail())) {
authSession.setAuthNote(Constants.VERIFY_EMAIL_KEY, existingUser.getEmail());
sendVerifyEmail(session, context, existingUser, brokerContext);
} else {
showEmailSentPage(context, brokerContext);
}
}
@Override
@ -89,7 +96,8 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
logger.debugf("Re-sending email requested for user, details follow");
// This will allow user to re-send email again
context.getAuthenticationSession().removeAuthNote(VERIFY_ACCOUNT_IDP_USERNAME);
context.getAuthenticationSession().removeAuthNote(Constants.VERIFY_EMAIL_KEY);
authenticateImpl(context, serializedCtx, brokerContext);
}
@ -146,6 +154,11 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
return;
}
showEmailSentPage(context, brokerContext);
}
protected void showEmailSentPage(AuthenticationFlowContext context, BrokeredIdentityContext brokerContext) {
String accessCode = context.generateAccessCode();
URI action = context.getActionUrl(accessCode);
@ -156,4 +169,5 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
.createIdpLinkEmailPage();
context.forceChallenge(challenge);
}
}

View file

@ -197,31 +197,26 @@ public class TokenManager {
return false;
}
UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
if (AuthenticationManager.isSessionValid(realm, userSession)) {
ClientSessionModel clientSession = session.sessions().getClientSession(realm, token.getClientSession());
if (clientSession != null) {
return true;
}
}
<<<<<<< f392e79ad781014387c9fe5724815b24eab7a35f
userSession = session.sessions().getOfflineUserSession(realm, token.getSessionState());
if (AuthenticationManager.isOfflineSessionValid(realm, userSession)) {
ClientSessionModel clientSession = session.sessions().getOfflineClientSession(realm, token.getClientSession());
if (clientSession != null) {
return true;
}
=======
ClientModel client = realm.getClientByClientId(token.getIssuedFor());
if (client == null || !client.isEnabled()) {
return false;
}
UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
if (AuthenticationManager.isSessionValid(realm, userSession)) {
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
if (clientSession == null) {
return false;
>>>>>>> KEYCLOAK-4626 AuthenticationSessions: start
if (clientSession != null) {
return true;
}
}
userSession = session.sessions().getOfflineUserSession(realm, token.getSessionState());
if (AuthenticationManager.isOfflineSessionValid(realm, userSession)) {
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
if (clientSession != null) {
return true;
}
}
return false;

View file

@ -140,24 +140,7 @@ public class UserInfoEndpoint {
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Token invalid: " + e.getMessage(), Response.Status.UNAUTHORIZED);
}
UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
ClientSessionModel clientSession = session.sessions().getClientSession(token.getClientSession());
if( userSession == null ) {
userSession = session.sessions().getOfflineUserSession(realm, token.getSessionState());
if( AuthenticationManager.isOfflineSessionValid(realm, userSession)) {
clientSession = session.sessions().getOfflineClientSession(realm, token.getClientSession());
} else {
userSession = null;
clientSession = null;
}
}
if (userSession == null) {
event.error(Errors.USER_SESSION_NOT_FOUND);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User session not found", Response.Status.BAD_REQUEST);
}
event.session(userSession);
UserSessionModel userSession = findValidSession(token, event);
UserModel userModel = userSession.getUser();
if (userModel == null) {
@ -169,11 +152,6 @@ public class UserInfoEndpoint {
.detail(Details.USERNAME, userModel.getUsername());
if (clientSession == null || !AuthenticationManager.isSessionValid(realm, userSession)) {
event.error(Errors.SESSION_EXPIRED);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED);
}
ClientModel clientModel = realm.getClientByClientId(token.getIssuedFor());
if (clientModel == null) {
event.error(Errors.CLIENT_NOT_FOUND);
@ -187,6 +165,12 @@ public class UserInfoEndpoint {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client disabled", Response.Status.BAD_REQUEST);
}
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(clientModel.getId());
if (clientSession == null) {
event.error(Errors.SESSION_EXPIRED);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED);
}
AccessToken userInfo = new AccessToken();
tokenManager.transformUserInfoAccessToken(session, userInfo, realm, clientModel, userModel, userSession, clientSession);
@ -225,4 +209,34 @@ public class UserInfoEndpoint {
return Cors.add(request, responseBuilder).auth().allowedOrigins(token).build();
}
private UserSessionModel findValidSession(AccessToken token, EventBuilder event) {
UserSessionModel userSession = session.sessions().getUserSession(realm, token.getSessionState());
UserSessionModel offlineUserSession = null;
if (AuthenticationManager.isSessionValid(realm, userSession)) {
event.session(userSession);
return userSession;
} else {
offlineUserSession = session.sessions().getOfflineUserSession(realm, token.getSessionState());
if (AuthenticationManager.isOfflineSessionValid(realm, offlineUserSession)) {
event.session(offlineUserSession);
return offlineUserSession;
}
}
if (userSession == null && offlineUserSession == null) {
event.error(Errors.USER_SESSION_NOT_FOUND);
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User session not found", Response.Status.BAD_REQUEST);
}
if (userSession != null) {
event.session(userSession);
} else {
event.session(offlineUserSession);
}
event.error(Errors.SESSION_EXPIRED);
throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "Session expired", Response.Status.UNAUTHORIZED);
}
}

View file

@ -513,7 +513,7 @@ public class AuthenticationManager {
if (actionTokenKey != null) {
ActionTokenStoreProvider actionTokenStore = session.getProvider(ActionTokenStoreProvider.class);
actionTokenStore.put(actionTokenKey, null);
actionTokenStore.put(actionTokenKey, null); // Token is invalidated
}
}

View file

@ -28,13 +28,14 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.StickySessionEncoderProvider;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class AuthenticationSessionManager {
private static final String AUTH_SESSION_ID = "AUTH_SESSION_ID";
public static final String AUTH_SESSION_ID = "AUTH_SESSION_ID";
private static final Logger log = Logger.getLogger(AuthenticationSessionManager.class);
@ -57,12 +58,12 @@ public class AuthenticationSessionManager {
public String getCurrentAuthenticationSessionId(RealmModel realm) {
return getAuthSessionCookie();
return getAuthSessionCookieDecoded(realm);
}
public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm) {
String authSessionId = getAuthSessionCookie();
String authSessionId = getAuthSessionCookieDecoded(realm);
return authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
}
@ -72,22 +73,37 @@ public class AuthenticationSessionManager {
String cookiePath = AuthenticationManager.getRealmCookiePath(realm, uriInfo);
boolean sslRequired = realm.getSslRequired().isRequired(session.getContext().getConnection());
CookieHelper.addCookie(AUTH_SESSION_ID, authSessionId, cookiePath, null, null, -1, sslRequired, true);
log.debugf("Set AUTH_SESSION_ID cookie with value %s", authSessionId);
StickySessionEncoderProvider encoder = session.getProvider(StickySessionEncoderProvider.class);
String encodedAuthSessionId = encoder.encodeSessionId(authSessionId);
CookieHelper.addCookie(AUTH_SESSION_ID, encodedAuthSessionId, cookiePath, null, null, -1, sslRequired, true);
log.debugf("Set AUTH_SESSION_ID cookie with value %s", encodedAuthSessionId);
}
public String getAuthSessionCookie() {
private String getAuthSessionCookieDecoded(RealmModel realm) {
String cookieVal = CookieHelper.getCookieValue(AUTH_SESSION_ID);
if (cookieVal != null) {
log.debugf("Found AUTH_SESSION_ID cookie with value %s", cookieVal);
} else {
log.debugf("Not found AUTH_SESSION_ID cookie");
StickySessionEncoderProvider encoder = session.getProvider(StickySessionEncoderProvider.class);
String decodedAuthSessionId = encoder.decodeSessionId(cookieVal);
// Check if owner of this authentication session changed due to re-hashing (usually node failover or addition of new node)
String reencoded = encoder.encodeSessionId(decodedAuthSessionId);
if (!reencoded.equals(cookieVal)) {
log.debugf("Route changed. Will update authentication session cookie");
setAuthSessionCookie(decodedAuthSessionId, realm);
}
return cookieVal;
return decodedAuthSessionId;
} else {
log.debugf("Not found AUTH_SESSION_ID cookie");
return null;
}
}

View file

@ -41,7 +41,6 @@ import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.AccountRoles;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.AuthenticationFlowModel;
@ -751,17 +750,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
UserModel federatedUser = authSession.getAuthenticatedUser();
if (wasFirstBrokerLogin) {
String isDifferentBrowser = authSession.getAuthNote(AbstractIdpAuthenticator.IS_DIFFERENT_BROWSER);
if (Boolean.parseBoolean(isDifferentBrowser)) {
new AuthenticationSessionManager(session).removeAuthenticationSession(realmModel, authSession, true);
return session.getProvider(LoginFormsProvider.class)
.setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, context.getIdpConfig().getAlias(), context.getUsername())
.createInfoPage();
} else {
return finishBrokerAuthentication(context, federatedUser, authSession, providerId);
}
} else {
boolean firstBrokerLoginInProgress = (authSession.getAuthNote(AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE) != null);

View file

@ -338,7 +338,7 @@ public class LoginActionsService {
return ErrorPage.error(session, Messages.RESET_CREDENTIAL_NOT_ALLOWED);
}
authSession = createAuthenticationSessionForClient(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
authSession = createAuthenticationSessionForClient();
return processResetCredentials(false, null, authSession);
}
@ -346,17 +346,17 @@ public class LoginActionsService {
return resetCredentials(code, execution);
}
AuthenticationSessionModel createAuthenticationSessionForClient(String clientId)
AuthenticationSessionModel createAuthenticationSessionForClient()
throws UriBuilderException, IllegalArgumentException {
AuthenticationSessionModel authSession;
// set up the account service as the endpoint to call.
ClientModel client = realm.getClientByClientId(clientId == null ? Constants.ACCOUNT_MANAGEMENT_CLIENT_ID : clientId);
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
authSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
//authSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString(); // TODO:mposolda It seems that this should be taken from client rather then hardcoded to account?
String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString();
authSession.setRedirectUri(redirectUri);
authSession.setClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);

View file

@ -72,9 +72,8 @@ public abstract class BrowserHistoryHelper {
}
// For now, handle just status 200 with String body. See if more is needed...
if (response.getStatus() == 200) {
Object entity = response.getEntity();
if (entity instanceof String) {
if (entity != null && entity instanceof String) {
String responseString = (String) entity;
URI lastExecutionURL = new AuthenticationFlowURLHelper(session, session.getContext().getRealm(), session.getContext().getUri()).getLastExecutionUrl(authSession);
@ -84,7 +83,7 @@ public abstract class BrowserHistoryHelper {
return Response.fromResponse(response).entity(responseWithJavascript).build();
}
}
return response;
}

View file

@ -257,20 +257,20 @@ mvn -f testsuite/integration-arquillian/tests/other/console/pom.xml \
## Welcome Page tests
The Welcome Page tests need to be run on WildFly/EAP and with `-Dskip.add.user.json` switch. So that they are disabled by default and are meant to be run separately.
```
# Prepare servers
mvn -f testsuite/integration-arquillian/servers/pom.xml \
# Prepare servers
mvn -f testsuite/integration-arquillian/servers/pom.xml \
clean install \
-Pauth-server-wildfly \
-Papp-server-wildfly
# Run tests
mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
# Run tests
mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
clean test \
-Dtest=WelcomePageTest \
-Dskip.add.user.json \
-Pauth-server-wildfly
```
## Social Login
The social login tests require setup of all social networks including an example social user. These details can't be
@ -342,3 +342,79 @@ To run the X.509 client certificate authentication tests:
-Dbrowser=phantomjs \
"-Dtest=*.x509.*"
## Cluster tests
Cluster tests use 2 backend servers (Keycloak on Wildfly/EAP) and 1 frontend loadbalancer server node. Invalidation tests don't use loadbalancer.
The browser usually communicates directly with the backend node1 and after doing some change here (eg. updating user), it verifies that the change is visible on node2 and user is updated here as well.
Failover tests use loadbalancer and they require the setup with the distributed infinispan caches switched to have 2 owners (default value is 1 owner). Otherwise failover won't reliably work.
The setup includes:
* a `mod_cluster` load balancer on Wildfly
* two clustered nodes of Keycloak server on Wildfly/EAP
Clustering tests require MULTICAST to be enabled on machine's `loopback` network interface.
This can be done by running the following commands under root privileges:
route add -net 224.0.0.0 netmask 240.0.0.0 dev lo
ifconfig lo multicast
Then after build the sources, distribution and setup of clean shared database (replace command according your DB), you can use this command to setup servers:
export DB_HOST=localhost
mvn -f testsuite/integration-arquillian/servers/pom.xml \
-Pauth-server-wildfly,auth-server-cluster,jpa \
-Dsession.cache.owners=2 \
-Djdbc.mvn.groupId=mysql \
-Djdbc.mvn.version=5.1.29 \
-Djdbc.mvn.artifactId=mysql-connector-java \
-Dkeycloak.connectionsJpa.url=jdbc:mysql://$DB_HOST/keycloak \
-Dkeycloak.connectionsJpa.user=keycloak \
-Dkeycloak.connectionsJpa.password=keycloak \
clean install
And then this to run the cluster tests:
mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
-Pauth-server-wildfly,auth-server-cluster \
-Dsession.cache.owners=2 \
-Dbackends.console.output=true \
-Dauth.server.log.check=false \
-Dfrontend.console.output=true \
-Dtest=org.keycloak.testsuite.cluster.**.*Test clean install
### Cluster tests with embedded undertow
#### Run cluster tests from IDE
The test uses Undertow loadbalancer on `http://localhost:8180` and two embedded backend Undertow servers with Keycloak on `http://localhost:8181` and `http://localhost:8182` .
You can use any cluster test (eg. AuthenticationSessionFailoverClusterTest) and run from IDE with those system properties (replace with your DB settings):
-Dauth.server.undertow=false -Dauth.server.undertow.cluster=true -Dauth.server.cluster=true
-Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak -Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver
-Dkeycloak.connectionsJpa.user=keycloak -Dkeycloak.connectionsJpa.password=keycloak -Dkeycloak.connectionsInfinispan.clustered=true -Dresources
-Dkeycloak.connectionsInfinispan.sessionsOwners=2 -Dsession.cache.owners=2
Invalidation tests (subclass of `AbstractInvalidationClusterTest`) don't need last two properties.
#### Run cluster environment from IDE
This mode is useful for develop/manual tests of clustering features. You will need to manually run keycloak backend nodes and loadbalancer.
1) Run KeycloakServer server1 with:
-Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak -Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver
-Dkeycloak.connectionsJpa.user=keycloak -Dkeycloak.connectionsJpa.password=keycloak -Dkeycloak.connectionsInfinispan.clustered=true
-Dkeycloak.connectionsInfinispan.sessionsOwners=2 -Dresources
and argument: `-p 8181`
2) Run KeycloakServer server2 with same parameters but argument: `-p 8182`
3) Run loadbalancer (class `SimpleUndertowLoadBalancer`) without arguments and system properties. Loadbalancer runs on port 8180, so you can access Keycloak on `http://localhost:8180/auth`

View file

@ -18,6 +18,11 @@
<xsl:value-of select="$sessionCacheOwners"/>
</xsl:attribute>
</xsl:template>
<xsl:template match="//i:cache-container/i:distributed-cache[@name='authenticationSessions']/@owners">
<xsl:attribute name="owners">
<xsl:value-of select="$sessionCacheOwners"/>
</xsl:attribute>
</xsl:template>
<xsl:template match="//i:cache-container/i:distributed-cache[@name='offlineSessions']/@owners">
<xsl:attribute name="owners">
<xsl:value-of select="$offlineSessionCacheOwners"/>

View file

@ -41,8 +41,11 @@ import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.jboss.shrinkwrap.descriptor.api.Descriptor;
import org.jboss.shrinkwrap.undertow.api.UndertowWebArchive;
import org.keycloak.common.util.reflections.Reflections;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.services.filters.KeycloakSessionServletFilter;
import org.keycloak.services.managers.ApplianceBootstrap;
import org.keycloak.services.resources.KeycloakApplication;
import javax.servlet.DispatcherType;
@ -106,6 +109,11 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
@Override
public ProtocolMetaData deploy(Archive<?> archive) throws DeploymentException {
if (isRemoteMode()) {
log.infof("Skipped deployment of '%s' as we are in remote mode!", archive.getName());
return new ProtocolMetaData();
}
DeploymentInfo di = getDeplotymentInfoFromArchive(archive);
ClassLoader parentCl = Thread.currentThread().getContextClassLoader();
@ -152,7 +160,7 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
return;
}
log.info("Starting auth server on embedded Undertow.");
log.infof("Starting auth server on embedded Undertow on: http://%s:%d", configuration.getBindAddress(), configuration.getBindHttpPort());
long start = System.currentTimeMillis();
if (undertow == null) {
@ -164,13 +172,37 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
.setIoThreads(configuration.getWorkerThreads() / 8)
);
if (configuration.getRoute() != null) {
log.info("Using route: " + configuration.getRoute());
}
SetSystemProperty setRouteProperty = new SetSystemProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME, configuration.getRoute());
try {
DeploymentInfo di = createAuthServerDeploymentInfo();
undertow.deploy(di);
ResteasyDeployment deployment = (ResteasyDeployment) di.getServletContextAttributes().get(ResteasyDeployment.class.getName());
sessionFactory = ((KeycloakApplication) deployment.getApplication()).getSessionFactory();
setupDevConfig();
log.info("Auth server started in " + (System.currentTimeMillis() - start) + " ms\n");
} finally {
setRouteProperty.revert();
}
}
protected void setupDevConfig() {
KeycloakSession session = sessionFactory.create();
try {
session.getTransactionManager().begin();
if (new ApplianceBootstrap(session).isNoMasterUser()) {
new ApplianceBootstrap(session).createMasterRealmUser("admin", "admin");
}
session.getTransactionManager().commit();
} finally {
session.close();
}
}
@Override
@ -187,11 +219,16 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
private boolean isRemoteMode() {
//return true;
return "true".equals(System.getProperty("remote.mode"));
return configuration.isRemoteMode();
}
@Override
public void undeploy(Archive<?> archive) throws DeploymentException {
if (isRemoteMode()) {
log.infof("Skipped undeployment of '%s' as we are in remote mode!", archive.getName());
return;
}
Field containerField = Reflections.findDeclaredField(UndertowJaxrsServer.class, "container");
Reflections.setAccessible(containerField);
ServletContainer container = (ServletContainer) Reflections.getFieldValue(containerField, undertow);

View file

@ -2,6 +2,7 @@ package org.keycloak.testsuite.arquillian.undertow;
import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
import org.jboss.arquillian.core.spi.LoadableExtension;
import org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancerContainer;
/**
*
@ -12,6 +13,7 @@ public class KeycloakOnUndertowArquillianExtension implements LoadableExtension
@Override
public void register(ExtensionBuilder builder) {
builder.service(DeployableContainer.class, KeycloakOnUndertow.class);
builder.service(DeployableContainer.class, SimpleUndertowLoadBalancerContainer.class);
}
}

View file

@ -19,11 +19,18 @@ package org.keycloak.testsuite.arquillian.undertow;
import org.arquillian.undertow.UndertowContainerConfiguration;
import org.jboss.arquillian.container.spi.ConfigurationException;
import org.jboss.logging.Logger;
public class KeycloakOnUndertowConfiguration extends UndertowContainerConfiguration {
protected static final Logger log = Logger.getLogger(KeycloakOnUndertowConfiguration.class);
private int workerThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2) * 8;
private String resourcesHome;
private boolean remoteMode;
private String route;
private int bindHttpPortOffset = 0;
public int getWorkerThreads() {
return workerThreads;
@ -41,10 +48,39 @@ public class KeycloakOnUndertowConfiguration extends UndertowContainerConfigurat
this.resourcesHome = resourcesHome;
}
public int getBindHttpPortOffset() {
return bindHttpPortOffset;
}
public void setBindHttpPortOffset(int bindHttpPortOffset) {
this.bindHttpPortOffset = bindHttpPortOffset;
}
public String getRoute() {
return route;
}
public void setRoute(String route) {
this.route = route;
}
public boolean isRemoteMode() {
return remoteMode;
}
public void setRemoteMode(boolean remoteMode) {
this.remoteMode = remoteMode;
}
@Override
public void validate() throws ConfigurationException {
super.validate();
int basePort = getBindHttpPort();
int newPort = basePort + bindHttpPortOffset;
setBindHttpPort(newPort);
log.info("KeycloakOnUndertow will listen on port: " + newPort);
// TODO validate workerThreads
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.arquillian.undertow;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
class SetSystemProperty {
private String name;
private String oldValue;
public SetSystemProperty(String name, String value) {
this.name = name;
this.oldValue = System.getProperty(name);
if (value == null) {
if (oldValue != null) {
System.getProperties().remove(name);
}
} else {
System.setProperty(name, value);
}
}
public void revert() {
String value = System.getProperty(name);
if (oldValue == null) {
if (value != null) {
System.getProperties().remove(name);
}
} else {
System.setProperty(name, oldValue);
}
}
}

View file

@ -0,0 +1,298 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.arquillian.undertow.lb;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import io.undertow.Undertow;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.ResponseCodeHandler;
import io.undertow.server.handlers.proxy.ExclusivityChecker;
import io.undertow.server.handlers.proxy.LoadBalancingProxyClient;
import io.undertow.server.handlers.proxy.ProxyCallback;
import io.undertow.server.handlers.proxy.ProxyClient;
import io.undertow.server.handlers.proxy.ProxyConnection;
import io.undertow.server.handlers.proxy.ProxyHandler;
import io.undertow.util.AttachmentKey;
import io.undertow.util.Headers;
import org.jboss.logging.Logger;
import org.keycloak.services.managers.AuthenticationSessionManager;
/**
* Loadbalancer on embedded undertow. Supports sticky session over "AUTH_SESSION_ID" cookie and failover to different node when sticky node not available.
* Status 503 is returned just if all backend nodes are unavailable.
*
* To configure backend nodes, you can use system property like : -Dkeycloak.nodes="node1=http://localhost:8181,node2=http://localhost:8182"
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SimpleUndertowLoadBalancer {
private static final Logger log = Logger.getLogger(SimpleUndertowLoadBalancer.class);
static final String DEFAULT_NODES = "node1=http://localhost:8181,node2=http://localhost:8182";
private final String host;
private final int port;
private final String nodesString;
private Undertow undertow;
public static void main(String[] args) throws Exception {
String nodes = System.getProperty("keycloak.nodes", DEFAULT_NODES);
SimpleUndertowLoadBalancer lb = new SimpleUndertowLoadBalancer("localhost", 8180, nodes);
lb.start();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
lb.stop();
}
});
}
public SimpleUndertowLoadBalancer(String host, int port, String nodesString) {
this.host = host;
this.port = port;
this.nodesString = nodesString;
log.infof("Keycloak nodes: %s", nodesString);
}
public void start() {
Map<String, String> nodes = parseNodes(nodesString);
try {
HttpHandler proxyHandler = createHandler(nodes);
undertow = Undertow.builder()
.addHttpListener(port, host)
.setHandler(proxyHandler)
.build();
undertow.start();
log.infof("Loadbalancer started and ready to serve requests on http://%s:%d", host, port);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void stop() {
undertow.stop();
}
static Map<String, String> parseNodes(String nodes) {
String[] nodesArray = nodes.split(",");
Map<String, String> result = new HashMap<>();
for (String nodeStr : nodesArray) {
String[] node = nodeStr.trim().split("=");
if (node.length != 2) {
throw new IllegalArgumentException("Illegal node format in the configuration: " + nodeStr);
}
result.put(node[0].trim(), node[1].trim());
}
return result;
}
private HttpHandler createHandler(Map<String, String> backendNodes) throws Exception {
// TODO: configurable options if needed
String sessionCookieNames = AuthenticationSessionManager.AUTH_SESSION_ID;
int connectionsPerThread = 20;
int problemServerRetry = 5; // In case of unavailable node, we will try to ping him every 5 seconds to check if it's back
int maxTime = 3600000; // 1 hour for proxy request timeout, so we can debug the backend keycloak servers
int requestQueueSize = 10;
int cachedConnectionsPerThread = 10;
int connectionIdleTimeout = 60;
int maxRetryAttempts = backendNodes.size() - 1;
final LoadBalancingProxyClient lb = new CustomLoadBalancingClient(new ExclusivityChecker() {
@Override
public boolean isExclusivityRequired(HttpServerExchange exchange) {
//we always create a new connection for upgrade requests
return exchange.getRequestHeaders().contains(Headers.UPGRADE);
}
}, maxRetryAttempts)
.setConnectionsPerThread(connectionsPerThread)
.setMaxQueueSize(requestQueueSize)
.setSoftMaxConnectionsPerThread(cachedConnectionsPerThread)
.setTtl(connectionIdleTimeout)
.setProblemServerRetry(problemServerRetry);
String[] sessionIds = sessionCookieNames.split(",");
for (String id : sessionIds) {
lb.addSessionCookieName(id);
}
for (Map.Entry<String, String> node : backendNodes.entrySet()) {
String route = node.getKey();
URI uri = new URI(node.getValue());
lb.addHost(uri, route);
log.infof("Added host: %s, route: %s", uri.toString(), route);
}
ProxyHandler handler = new ProxyHandler(lb, maxTime, ResponseCodeHandler.HANDLE_404);
return handler;
}
private class CustomLoadBalancingClient extends LoadBalancingProxyClient {
private final int maxRetryAttempts;
public CustomLoadBalancingClient(ExclusivityChecker checker, int maxRetryAttempts) {
super(checker);
this.maxRetryAttempts = maxRetryAttempts;
}
@Override
protected Host selectHost(HttpServerExchange exchange) {
Host host = super.selectHost(exchange);
log.debugf("Selected host: %s, host available: %b", host.getUri().toString(), host.isAvailable());
exchange.putAttachment(SELECTED_HOST, host);
return host;
}
@Override
protected Host findStickyHost(HttpServerExchange exchange) {
Host stickyHost = super.findStickyHost(exchange);
if (stickyHost != null) {
if (!stickyHost.isAvailable()) {
log.infof("Sticky host %s not available. Trying different hosts", stickyHost.getUri());
return null;
} else {
log.infof("Sticky host %s found and looks available", stickyHost.getUri());
}
}
return stickyHost;
}
@Override
public void getConnection(ProxyTarget target, HttpServerExchange exchange, ProxyCallback<ProxyConnection> callback, long timeout, TimeUnit timeUnit) {
long timeoutMs = timeUnit.toMillis(timeout);
ProxyCallbackDelegate callbackDelegate = new ProxyCallbackDelegate(this, callback, timeoutMs, maxRetryAttempts);
super.getConnection(target, exchange, callbackDelegate, timeout, timeUnit);
}
}
private static final AttachmentKey<LoadBalancingProxyClient.Host> SELECTED_HOST = AttachmentKey.create(LoadBalancingProxyClient.Host.class);
private static final AttachmentKey<Integer> REMAINING_RETRY_ATTEMPTS = AttachmentKey.create(Integer.class);
private class ProxyCallbackDelegate implements ProxyCallback<ProxyConnection> {
private final ProxyClient proxyClient;
private final ProxyCallback<ProxyConnection> delegate;
private final long timeoutMs;
private final int maxRetryAttempts;
public ProxyCallbackDelegate(ProxyClient proxyClient, ProxyCallback<ProxyConnection> delegate, long timeoutMs, int maxRetryAttempts) {
this.proxyClient = proxyClient;
this.delegate = delegate;
this.timeoutMs = timeoutMs;
this.maxRetryAttempts = maxRetryAttempts;
}
@Override
public void completed(HttpServerExchange exchange, ProxyConnection result) {
LoadBalancingProxyClient.Host host = exchange.getAttachment(SELECTED_HOST);
if (host == null) {
// shouldn't happen
log.error("Host is null!!!");
} else {
// Host was restored
if (!host.isAvailable()) {
log.infof("Host %s available again", host.getUri());
host.clearError();
}
}
delegate.completed(exchange, result);
}
@Override
public void failed(HttpServerExchange exchange) {
final long time = System.currentTimeMillis();
Integer remainingAttempts = exchange.getAttachment(REMAINING_RETRY_ATTEMPTS);
if (remainingAttempts == null) {
remainingAttempts = maxRetryAttempts;
} else {
remainingAttempts--;
}
exchange.putAttachment(REMAINING_RETRY_ATTEMPTS, remainingAttempts);
log.infof("Failed request to selected host. Remaining attempts: %d", remainingAttempts);
if (remainingAttempts > 0) {
if (timeoutMs > 0 && time > timeoutMs) {
delegate.failed(exchange);
} else {
ProxyClient.ProxyTarget target = proxyClient.findTarget(exchange);
if (target != null) {
final long remaining = timeoutMs > 0 ? timeoutMs - time : -1;
proxyClient.getConnection(target, exchange, this, remaining, TimeUnit.MILLISECONDS);
} else {
couldNotResolveBackend(exchange); // The context was registered when we started, so return 503
}
}
} else {
couldNotResolveBackend(exchange);
}
}
@Override
public void couldNotResolveBackend(HttpServerExchange exchange) {
delegate.couldNotResolveBackend(exchange);
}
@Override
public void queuedRequestFailed(HttpServerExchange exchange) {
delegate.queuedRequestFailed(exchange);
}
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.arquillian.undertow.lb;
import org.arquillian.undertow.UndertowContainerConfiguration;
import org.jboss.arquillian.container.spi.ConfigurationException;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SimpleUndertowLoadBalancerConfiguration extends UndertowContainerConfiguration {
private String nodes = SimpleUndertowLoadBalancer.DEFAULT_NODES;
public String getNodes() {
return nodes;
}
public void setNodes(String nodes) {
this.nodes = nodes;
}
@Override
public void validate() throws ConfigurationException {
super.validate();
try {
SimpleUndertowLoadBalancer.parseNodes(nodes);
} catch (Exception e) {
throw new ConfigurationException(e);
}
}
}

View file

@ -0,0 +1,87 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.arquillian.undertow.lb;
import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
import org.jboss.arquillian.container.spi.client.container.DeploymentException;
import org.jboss.arquillian.container.spi.client.container.LifecycleException;
import org.jboss.arquillian.container.spi.client.protocol.ProtocolDescription;
import org.jboss.arquillian.container.spi.client.protocol.metadata.ProtocolMetaData;
import org.jboss.logging.Logger;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.descriptor.api.Descriptor;
/**
* Arquillian container over {@link SimpleUndertowLoadBalancer}
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class SimpleUndertowLoadBalancerContainer implements DeployableContainer<SimpleUndertowLoadBalancerConfiguration> {
private static final Logger log = Logger.getLogger(SimpleUndertowLoadBalancerContainer.class);
private SimpleUndertowLoadBalancerConfiguration configuration;
private SimpleUndertowLoadBalancer container;
@Override
public Class<SimpleUndertowLoadBalancerConfiguration> getConfigurationClass() {
return SimpleUndertowLoadBalancerConfiguration.class;
}
@Override
public void setup(SimpleUndertowLoadBalancerConfiguration configuration) {
this.configuration = configuration;
}
@Override
public void start() throws LifecycleException {
this.container = new SimpleUndertowLoadBalancer(configuration.getBindAddress(), configuration.getBindHttpPort(), configuration.getNodes());
this.container.start();
}
@Override
public void stop() throws LifecycleException {
log.info("Going to stop loadbalancer");
this.container.stop();
}
@Override
public ProtocolDescription getDefaultProtocol() {
return new ProtocolDescription("Servlet 3.1");
}
@Override
public ProtocolMetaData deploy(Archive<?> archive) throws DeploymentException {
throw new UnsupportedOperationException("Not implemented");
}
@Override
public void undeploy(Archive<?> archive) throws DeploymentException {
throw new UnsupportedOperationException("Not implemented");
}
@Override
public void deploy(Descriptor descriptor) throws DeploymentException {
throw new UnsupportedOperationException("Not implemented");
}
@Override
public void undeploy(Descriptor descriptor) throws DeploymentException {
throw new UnsupportedOperationException("Not implemented");
}
}

View file

@ -71,6 +71,8 @@ public class AuthServerTestEnricher {
private static final String AUTH_SERVER_CLUSTER_PROPERTY = "auth.server.cluster";
public static final boolean AUTH_SERVER_CLUSTER = Boolean.parseBoolean(System.getProperty(AUTH_SERVER_CLUSTER_PROPERTY, "false"));
private static final boolean AUTH_SERVER_UNDERTOW_CLUSTER = Boolean.parseBoolean(System.getProperty("auth.server.undertow.cluster", "false"));
private static final Boolean START_MIGRATION_CONTAINER = "auto".equals(System.getProperty("migration.mode")) ||
"manual".equals(System.getProperty("migration.mode"));
@ -112,9 +114,25 @@ public class AuthServerTestEnricher {
suiteContext = new SuiteContext(containers);
String authServerFrontend = AUTH_SERVER_CLUSTER
? "auth-server-balancer-wildfly" // if cluster mode enabled, load-balancer is the frontend
: AUTH_SERVER_CONTAINER; // single-node mode
String authServerFrontend = null;
if (AUTH_SERVER_CLUSTER) {
// if cluster mode enabled, load-balancer is the frontend
for (ContainerInfo c : containers) {
if (c.getQualifier().startsWith("auth-server-balancer")) {
authServerFrontend = c.getQualifier();
}
}
if (authServerFrontend != null) {
log.info("Using frontend container: " + authServerFrontend);
} else {
throw new IllegalStateException("Not found frontend container");
}
} else {
authServerFrontend = AUTH_SERVER_CONTAINER; // single-node mode
}
String authServerBackend = AUTH_SERVER_CONTAINER + "-backend";
int backends = 0;
for (ContainerInfo container : suiteContext.getContainers()) {
@ -130,6 +148,11 @@ public class AuthServerTestEnricher {
}
}
// Setup with 2 undertow backend nodes and no loadbalancer.
// if (AUTH_SERVER_UNDERTOW_CLUSTER && suiteContext.getAuthServerInfo() == null && !suiteContext.getAuthServerBackendsInfo().isEmpty()) {
// suiteContext.setAuthServerInfo(suiteContext.getAuthServerBackendsInfo().get(0));
// }
// validate auth server setup
if (suiteContext.getAuthServerInfo() == null) {
throw new RuntimeException(String.format("No auth server container matching '%s' found in arquillian.xml.", authServerFrontend));

View file

@ -33,6 +33,7 @@ import org.jboss.resteasy.plugins.providers.jackson.ResteasyJackson2Provider;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.models.Constants;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.arquillian.SuiteContext;
import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
@ -41,7 +42,7 @@ import static org.keycloak.testsuite.util.IOUtil.PROJECT_BUILD_DIRECTORY;
public class AdminClientUtil {
public static Keycloak createAdminClient(boolean ignoreUnknownProperties) throws Exception {
public static Keycloak createAdminClient(boolean ignoreUnknownProperties, String authServerContextRoot) throws Exception {
SSLContext ssl = null;
if ("true".equals(System.getProperty("auth.server.ssl.required"))) {
File trustore = new File(PROJECT_BUILD_DIRECTORY, "dependency/keystore/keycloak.truststore");
@ -61,12 +62,12 @@ public class AdminClientUtil {
jacksonProvider.setMapper(objectMapper);
}
return Keycloak.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth",
return Keycloak.getInstance(authServerContextRoot + "/auth",
MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID, null, ssl, jacksonProvider);
}
public static Keycloak createAdminClient() throws Exception {
return createAdminClient(false);
return createAdminClient(false, AuthServerTestEnricher.getAuthServerContextRoot());
}
private static SSLContext getSSLContextWithTrustore(File file, String password) throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException {

View file

@ -18,26 +18,19 @@ package org.keycloak.testsuite;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.http.ssl.SSLContexts;
import org.keycloak.common.util.KeycloakUriBuilder;
import org.keycloak.common.util.Time;
import org.keycloak.testsuite.arquillian.KcArquillian;
import org.keycloak.testsuite.arquillian.TestContext;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.ws.rs.NotFoundException;
import org.jboss.arquillian.container.test.api.RunAsClient;
import org.jboss.arquillian.drone.api.annotation.Drone;
@ -53,7 +46,6 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.RealmsResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.models.Constants;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
@ -77,7 +69,6 @@ import org.openqa.selenium.WebDriver;
import static org.keycloak.testsuite.admin.Users.setPasswordFor;
import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
import static org.keycloak.testsuite.auth.page.AuthRealm.MASTER;
import static org.keycloak.testsuite.util.IOUtil.PROJECT_BUILD_DIRECTORY;
/**
*
@ -135,7 +126,8 @@ public abstract class AbstractKeycloakTest {
public void beforeAbstractKeycloakTest() throws Exception {
adminClient = testContext.getAdminClient();
if (adminClient == null) {
adminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting());
String authServerContextRoot = suiteContext.getAuthServerInfo().getContextRoot().toString();
adminClient = AdminClientUtil.createAdminClient(suiteContext.isAdapterCompatTesting(), authServerContextRoot);
testContext.setAdminClient(adminClient);
}
@ -147,10 +139,9 @@ public abstract class AbstractKeycloakTest {
TestEventsLogger.setDriver(driver);
if (!suiteContext.isAdminPasswordUpdated()) {
log.debug("updating admin password");
// The backend cluster nodes may not be yet started. Password will be updated later for cluster setup.
if (!AuthServerTestEnricher.AUTH_SERVER_CLUSTER) {
updateMasterAdminPassword();
suiteContext.setAdminPasswordUpdated(true);
}
if (testContext.getTestRealmReps() == null) {
@ -202,11 +193,17 @@ public abstract class AbstractKeycloakTest {
return false;
}
private void updateMasterAdminPassword() {
protected void updateMasterAdminPassword() {
if (!suiteContext.isAdminPasswordUpdated()) {
log.debug("updating admin password");
welcomePage.navigateTo();
if (!welcomePage.isPasswordSet()) {
welcomePage.setPassword("admin", "admin");
}
suiteContext.setAdminPasswordUpdated(true);
}
}
public void deleteAllCookiesForMasterRealm() {
@ -236,7 +233,8 @@ public abstract class AbstractKeycloakTest {
if (testingClient == null) {
testingClient = testContext.getTestingClient();
if (testingClient == null) {
testingClient = KeycloakTestingClient.getInstance(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth");
String authServerContextRoot = suiteContext.getAuthServerInfo().getContextRoot().toString();
testingClient = KeycloakTestingClient.getInstance(authServerContextRoot + "/auth");
testContext.setTestingClient(testingClient);
}
}

View file

@ -28,6 +28,8 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import javax.ws.rs.core.Response;
import org.jboss.arquillian.container.test.api.Deployer;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.junit.BeforeClass;
@ -199,7 +201,8 @@ public abstract class AbstractServletAuthzAdapterTest extends AbstractExampleAda
assertFalse(policy.getUsers().isEmpty());
getAuthorizationResource().policies().user().create(policy);
Response response = getAuthorizationResource().policies().user().create(policy);
response.close();
}
protected interface ExceptionRunnable {

View file

@ -23,6 +23,8 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import javax.ws.rs.core.Response;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
@ -289,7 +291,8 @@ public abstract class AbstractServletAuthzFunctionalAdapterTest extends Abstract
policy.addClient("admin-cli");
ClientPoliciesResource policyResource = getAuthorizationResource().policies().client();
policyResource.create(policy);
Response response = policyResource.create(policy);
response.close();
policy = policyResource.findByName(policy.getName());
updatePermissionPolicies("Protected Resource Permission", policy.getName());

View file

@ -46,8 +46,6 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserFederationMapperRepresentation;
import org.keycloak.representations.idm.UserFederationProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
@ -64,7 +62,6 @@ import org.keycloak.testsuite.util.FederatedIdentityBuilder;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.IdentityProviderBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.TestCleanup;
import org.keycloak.testsuite.util.UserBuilder;
import javax.ws.rs.ClientErrorException;

View file

@ -28,6 +28,8 @@ import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
import javax.ws.rs.core.Response;
import org.junit.Before;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
@ -39,7 +41,6 @@ import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
@ -131,7 +132,10 @@ public abstract class AbstractPolicyManagementTest extends AbstractKeycloakTest
resources.add(new ResourceRepresentation("Resource B", scopes));
resources.add(new ResourceRepresentation("Resource C", scopes));
resources.forEach(resource -> getClient().authorization().resources().create(resource));
resources.forEach(resource -> {
Response response = getClient().authorization().resources().create(resource);
response.close();
});
}
private void createPolicies(RealmResource realm, ClientResource client) throws IOException {
@ -147,7 +151,8 @@ public abstract class AbstractPolicyManagementTest extends AbstractKeycloakTest
representation.setName(name);
representation.addUser(userId);
client.authorization().policies().user().create(representation);
Response response = client.authorization().policies().user().create(representation);
response.close();
}
protected ClientResource getClient() {
@ -161,7 +166,7 @@ public abstract class AbstractPolicyManagementTest extends AbstractKeycloakTest
protected RealmResource getRealm() {
try {
return AdminClientUtil.createAdminClient().realm("authz-test");
return adminClient.realm("authz-test");
} catch (Exception cause) {
throw new RuntimeException("Failed to create admin client", cause);
}

View file

@ -112,6 +112,7 @@ public class ClientPolicyManagementTest extends AbstractPolicyManagementTest {
ClientPoliciesResource policies = authorization.policies().client();
Response response = policies.create(representation);
ClientPolicyRepresentation created = response.readEntity(ClientPolicyRepresentation.class);
response.close();
policies.findById(created.getId()).remove();
@ -136,6 +137,7 @@ public class ClientPolicyManagementTest extends AbstractPolicyManagementTest {
ClientPoliciesResource policies = authorization.policies().client();
Response response = policies.create(representation);
ClientPolicyRepresentation created = response.readEntity(ClientPolicyRepresentation.class);
response.close();
PolicyResource policy = authorization.policies().policy(created.getId());
PolicyRepresentation genericConfig = policy.toRepresentation();
@ -152,6 +154,7 @@ public class ClientPolicyManagementTest extends AbstractPolicyManagementTest {
ClientPoliciesResource permissions = authorization.policies().client();
Response response = permissions.create(representation);
ClientPolicyRepresentation created = response.readEntity(ClientPolicyRepresentation.class);
response.close();
ClientPolicyResource permission = permissions.findById(created.getId());
assertRepresentation(representation, permission);
}

View file

@ -50,7 +50,6 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
@ -140,7 +139,7 @@ public class ConflictingScopePermissionTest extends AbstractKeycloakTest {
}
private RealmResource getRealm() throws Exception {
return AdminClientUtil.createAdminClient().realm("authz-test");
return adminClient.realm("authz-test");
}
private ClientResource getClient(RealmResource realm) {

View file

@ -24,6 +24,8 @@ import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import javax.ws.rs.core.Response;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.admin.client.resource.AuthorizationResource;
@ -43,7 +45,6 @@ import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.RoleBuilder;
@ -77,14 +78,16 @@ public class RequireUmaAuthorizationScopeTest extends AbstractKeycloakTest {
AuthorizationResource authorization = client.authorization();
ResourceRepresentation resource = new ResourceRepresentation("Resource A");
authorization.resources().create(resource);
Response response = authorization.resources().create(resource);
response.close();
JSPolicyRepresentation policy = new JSPolicyRepresentation();
policy.setName("Default Policy");
policy.setCode("$evaluation.grant();");
authorization.policies().js().create(policy);
response = authorization.policies().js().create(policy);
response.close();
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
@ -92,7 +95,8 @@ public class RequireUmaAuthorizationScopeTest extends AbstractKeycloakTest {
permission.addResource(resource.getName());
permission.addPolicy(policy.getName());
authorization.permissions().resource().create(permission);
response = authorization.permissions().resource().create(permission);
response.close();
}
@Test
@ -140,7 +144,7 @@ public class RequireUmaAuthorizationScopeTest extends AbstractKeycloakTest {
}
private RealmResource getRealm() throws Exception {
return AdminClientUtil.createAdminClient().realm("authz-test");
return adminClient.realm("authz-test");
}
private ClientResource getClient(RealmResource realm) {

View file

@ -45,6 +45,12 @@ public abstract class AbstractClusterTest extends AbstractKeycloakTest {
logFailoverSetup();
}
// Assume that route like "node6" will have corresponding backend container like "auth-server-wildfly-backend6"
protected void setCurrentFailNodeForRoute(String route) {
String routeNumber = route.substring(route.length() - 1);
currentFailNodeIndex = Integer.parseInt(routeNumber) - 1;
}
protected ContainerInfo getCurrentFailNode() {
return backendNode(currentFailNodeIndex);
}
@ -111,9 +117,13 @@ public abstract class AbstractClusterTest extends AbstractKeycloakTest {
}
protected Keycloak getAdminClientFor(ContainerInfo node) {
return node.equals(suiteContext.getAuthServerInfo())
? adminClient // frontend client
: backendAdminClients.get(node);
Keycloak adminClient = backendAdminClients.get(node);
if (adminClient == null && node.equals(suiteContext.getAuthServerInfo())) {
adminClient = this.adminClient;
}
return adminClient;
}
@Before

View file

@ -0,0 +1,112 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.cluster;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.page.AbstractPage;
import org.keycloak.testsuite.page.PageWithLogOutAction;
import org.openqa.selenium.Cookie;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
import static org.keycloak.testsuite.util.WaitUtils.pause;
public abstract class AbstractFailoverClusterTest extends AbstractClusterTest {
public static final String KEYCLOAK_SESSION_COOKIE = "KEYCLOAK_SESSION";
public static final Integer SESSION_CACHE_OWNERS = Integer.parseInt(System.getProperty("session.cache.owners", "1"));
public static final Integer OFFLINE_SESSION_CACHE_OWNERS = Integer.parseInt(System.getProperty("offline.session.cache.owners", "1"));
public static final Integer LOGIN_FAILURES_CACHE_OWNERS = Integer.parseInt(System.getProperty("login.failure.cache.owners", "1"));
public static final Integer REBALANCE_WAIT = Integer.parseInt(System.getProperty("rebalance.wait", "5000"));
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
}
/**
* failure --> failback --> failure of next node
*/
protected void switchFailedNode() {
assertFalse(controller.isStarted(getCurrentFailNode().getQualifier()));
failback();
pause(REBALANCE_WAIT);
iterateCurrentFailNode();
failure();
pause(REBALANCE_WAIT);
assertFalse(controller.isStarted(getCurrentFailNode().getQualifier()));
}
protected Cookie login(AbstractPage targetPage) {
targetPage.navigateTo();
assertCurrentUrlStartsWith(loginPage);
loginPage.form().login(ADMIN, ADMIN);
assertCurrentUrlStartsWith(targetPage);
Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
assertNotNull(sessionCookie);
return sessionCookie;
}
protected void logout(AbstractPage targetPage) {
if (!(targetPage instanceof PageWithLogOutAction)) {
throw new IllegalArgumentException(targetPage.getClass().getSimpleName() + " must implement PageWithLogOutAction interface");
}
targetPage.navigateTo();
assertCurrentUrlStartsWith(targetPage);
((PageWithLogOutAction) targetPage).logOut();
}
protected Cookie verifyLoggedIn(AbstractPage targetPage, Cookie sessionCookieForVerification) {
// verify on realm path
masterRealmPage.navigateTo();
Cookie sessionCookieOnRealmPath = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
assertNotNull(sessionCookieOnRealmPath);
assertEquals(sessionCookieOnRealmPath.getValue(), sessionCookieForVerification.getValue());
// verify on target page
targetPage.navigateTo();
assertCurrentUrlStartsWith(targetPage);
Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
assertNotNull(sessionCookie);
assertEquals(sessionCookie.getValue(), sessionCookieForVerification.getValue());
return sessionCookie;
}
protected void verifyLoggedOut(AbstractPage targetPage) {
// verify on target page
targetPage.navigateTo();
driver.navigate().refresh();
assertCurrentUrlStartsWith(loginPage);
Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
assertNull(sessionCookie);
}
}

View file

@ -0,0 +1,171 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.cluster;
import java.io.IOException;
import java.util.List;
import javax.mail.MessagingException;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.Cookie;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
import static org.keycloak.testsuite.util.WaitUtils.pause;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class AuthenticationSessionFailoverClusterTest extends AbstractFailoverClusterTest {
private String userId;
@Page
protected LoginPage loginPage;
@Page
protected LoginPasswordUpdatePage updatePasswordPage;
@Page
protected LoginUpdateProfilePage updateProfilePage;
@Page
protected AppPage appPage;
@Before
public void setup() {
try {
adminClient.realm("test").remove();
} catch (Exception ignore) {
}
RealmRepresentation testRealm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
adminClient.realms().create(testRealm);
UserRepresentation user = UserBuilder.create()
.username("login-test")
.email("login@test.com")
.enabled(true)
.requiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString())
.requiredAction(UserModel.RequiredAction.UPDATE_PROFILE.toString())
.build();
userId = ApiUtil.createUserAndResetPasswordWithAdminClient(adminClient.realm("test"), user, "password");
getCleanup().addUserId(userId);
oauth.clientId("test-app");
}
@After
public void after() {
adminClient.realm("test").remove();
}
@Test
public void failoverDuringAuthentication() throws Exception {
boolean expectSuccessfulFailover = SESSION_CACHE_OWNERS >= 2;
log.info("AUTHENTICATION FAILOVER TEST: cluster size = " + getClusterSize() + ", session-cache owners = " + SESSION_CACHE_OWNERS
+ " --> Testsing for " + (expectSuccessfulFailover ? "" : "UN") + "SUCCESSFUL session failover.");
assertEquals(2, getClusterSize());
failoverTest(expectSuccessfulFailover);
}
protected void failoverTest(boolean expectSuccessfulFailover) throws IOException, MessagingException {
loginPage.open();
String cookieValue1 = getAuthSessionCookieValue();
// Login and assert on "updatePassword" page
loginPage.login("login-test", "password");
updatePasswordPage.assertCurrent();
// Route didn't change
Assert.assertEquals(cookieValue1, getAuthSessionCookieValue());
log.info("Authentication session cookie: " + cookieValue1);
setCurrentFailNodeForRoute(cookieValue1);
failure();
pause(REBALANCE_WAIT);
logFailoverSetup();
// Trigger the action now
updatePasswordPage.changePassword("password", "password");
if (expectSuccessfulFailover) {
//Action was successful
updateProfilePage.assertCurrent();
String cookieValue2 = getAuthSessionCookieValue();
log.info("Authentication session cookie after failover: " + cookieValue2);
// Cookie was moved to the second node
Assert.assertEquals(cookieValue1.substring(0, 36), cookieValue2.substring(0, 36));
Assert.assertNotEquals(cookieValue1, cookieValue2);
} else {
loginPage.assertCurrent();
String error = loginPage.getError();
log.info("Failover not successful as expected. Error on login page: " + error);
Assert.assertNotNull(error);
loginPage.login("login-test", "password");
updatePasswordPage.changePassword("password", "password");
}
updateProfilePage.assertCurrent();
// Successfully update profile and assert user logged
updateProfilePage.update("John", "Doe3", "john@doe3.com");
appPage.assertCurrent();
}
private String getAuthSessionCookieValue() {
Cookie authSessionCookie = driver.manage().getCookieNamed(AuthenticationSessionManager.AUTH_SESSION_ID);
Assert.assertNotNull(authSessionCookie);
return authSessionCookie.getValue();
}
}

View file

@ -2,38 +2,16 @@ package org.keycloak.testsuite.cluster;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.page.AbstractPage;
import org.keycloak.testsuite.page.PageWithLogOutAction;
import org.openqa.selenium.Cookie;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.keycloak.testsuite.auth.page.AuthRealm.ADMIN;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
import static org.keycloak.testsuite.util.WaitUtils.pause;
/**
*
* @author tkyjovsk
*/
public class SessionFailoverClusterTest extends AbstractClusterTest {
public static final String KEYCLOAK_SESSION_COOKIE = "KEYCLOAK_SESSION";
public static final Integer SESSION_CACHE_OWNERS = Integer.parseInt(System.getProperty("session.cache.owners", "1"));
public static final Integer OFFLINE_SESSION_CACHE_OWNERS = Integer.parseInt(System.getProperty("offline.session.cache.owners", "1"));
public static final Integer LOGIN_FAILURES_CACHE_OWNERS = Integer.parseInt(System.getProperty("login.failure.cache.owners", "1"));
public static final Integer REBALANCE_WAIT = Integer.parseInt(System.getProperty("rebalance.wait", "5000"));
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
}
public class SessionFailoverClusterTest extends AbstractFailoverClusterTest {
@Before
public void beforeSessionFailover() {
@ -45,7 +23,7 @@ public class SessionFailoverClusterTest extends AbstractClusterTest {
@Test
public void sessionFailover() {
boolean expectSuccessfulFailover = SESSION_CACHE_OWNERS >= getClusterSize();
boolean expectSuccessfulFailover = SESSION_CACHE_OWNERS >= 2;
log.info("SESSION FAILOVER TEST: cluster size = " + getClusterSize() + ", session-cache owners = " + SESSION_CACHE_OWNERS
+ " --> Testsing for " + (expectSuccessfulFailover ? "" : "UN") + "SUCCESSFUL session failover.");
@ -91,64 +69,4 @@ public class SessionFailoverClusterTest extends AbstractClusterTest {
}
/**
* failure --> failback --> failure of next node
*/
protected void switchFailedNode() {
assertFalse(controller.isStarted(getCurrentFailNode().getQualifier()));
failback();
pause(REBALANCE_WAIT);
iterateCurrentFailNode();
failure();
pause(REBALANCE_WAIT);
assertFalse(controller.isStarted(getCurrentFailNode().getQualifier()));
}
protected Cookie login(AbstractPage targetPage) {
targetPage.navigateTo();
assertCurrentUrlStartsWith(loginPage);
loginPage.form().login(ADMIN, ADMIN);
assertCurrentUrlStartsWith(targetPage);
Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
assertNotNull(sessionCookie);
return sessionCookie;
}
protected void logout(AbstractPage targetPage) {
if (!(targetPage instanceof PageWithLogOutAction)) {
throw new IllegalArgumentException(targetPage.getClass().getSimpleName() + " must implement PageWithLogOutAction interface");
}
targetPage.navigateTo();
assertCurrentUrlStartsWith(targetPage);
((PageWithLogOutAction) targetPage).logOut();
}
protected Cookie verifyLoggedIn(AbstractPage targetPage, Cookie sessionCookieForVerification) {
// verify on realm path
masterRealmPage.navigateTo();
Cookie sessionCookieOnRealmPath = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
assertNotNull(sessionCookieOnRealmPath);
assertEquals(sessionCookieOnRealmPath.getValue(), sessionCookieForVerification.getValue());
// verify on target page
targetPage.navigateTo();
assertCurrentUrlStartsWith(targetPage);
Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
assertNotNull(sessionCookie);
assertEquals(sessionCookie.getValue(), sessionCookieForVerification.getValue());
return sessionCookie;
}
protected void verifyLoggedOut(AbstractPage targetPage) {
// verify on target page
targetPage.navigateTo();
driver.navigate().refresh();
assertCurrentUrlStartsWith(loginPage);
Cookie sessionCookie = driver.manage().getCookieNamed(KEYCLOAK_SESSION_COOKIE);
assertNull(sessionCookie);
}
}

View file

@ -108,8 +108,9 @@
"connectionsInfinispan": {
"default": {
"clustered": "${keycloak.connectionsInfinispan.clustered:false}",
"async": "${keycloak.connectionsInfinispan.async:true}",
"sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}",
"async": "${keycloak.connectionsInfinispan.async:false}",
"sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}",
"l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:600000}",
"remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
"remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}",
"remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"

View file

@ -53,6 +53,7 @@
<property name="bindAddress">localhost</property>
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
<property name="bindHttpPort">${auth.server.http.port}</property>
<property name="remoteMode">${undertow.remote}</property>
</configuration>
</container>
@ -127,6 +128,43 @@
</container>
</group>
<!-- Clustering with embedded undertow -->
<group qualifier="auth-server-undertow-cluster">
<container qualifier="auth-server-undertow-backend1" mode="manual" >
<configuration>
<property name="enabled">${auth.server.undertow.cluster}</property>
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
<property name="bindAddress">localhost</property>
<property name="bindHttpPort">${auth.server.http.port}</property>
<property name="bindHttpPortOffset">1</property>
<property name="route">node1</property>
<property name="remoteMode">${undertow.remote}</property>
</configuration>
</container>
<container qualifier="auth-server-undertow-backend2" mode="manual" >
<configuration>
<property name="enabled">${auth.server.undertow.cluster}</property>
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
<property name="bindAddress">localhost</property>
<property name="bindHttpPort">${auth.server.http.port}</property>
<property name="bindHttpPortOffset">2</property>
<property name="route">node2</property>
<property name="remoteMode">${undertow.remote}</property>
</configuration>
</container>
<container qualifier="auth-server-balancer-undertow" mode="suite" >
<configuration>
<property name="enabled">${auth.server.undertow.cluster}</property>
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.lb.SimpleUndertowLoadBalancerContainer</property>
<property name="bindAddress">localhost</property>
<property name="bindHttpPort">${auth.server.http.port}</property>
<property name="nodes">node1=http://localhost:8181,node2=http://localhost:8182</property>
</configuration>
</container>
</group>
<container qualifier="auth-server-balancer-wildfly" mode="suite" >
<configuration>
<property name="enabled">${auth.server.cluster}</property>

View file

@ -27,6 +27,7 @@ import org.jboss.logging.Logger;
import org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher;
import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer;
import org.jboss.resteasy.spi.ResteasyDeployment;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
@ -37,12 +38,15 @@ import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.testsuite.util.cli.TestsuiteCLI;
import org.keycloak.util.JsonSerialization;
import org.mvel2.util.Make;
import javax.servlet.DispatcherType;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
@ -187,6 +191,8 @@ public class KeycloakServer {
config.setWorkerThreads(undertowWorkerThreads);
}
detectNodeName(config);
final KeycloakServer keycloak = new KeycloakServer(config);
keycloak.sysout = true;
keycloak.start();
@ -369,4 +375,24 @@ public class KeycloakServer {
return new File(s.toString());
}
private static void detectNodeName(KeycloakServerConfig config) {
String nodeName = System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME);
if (nodeName == null) {
// Try to autodetect "jboss.node.name" from the port
Map<Integer, String> nodesCfg = new HashMap<>();
nodesCfg.put(8181, "node1");
nodesCfg.put(8182, "node2");
nodeName = nodesCfg.get(config.getPort());
if (nodeName != null) {
System.setProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME, nodeName);
}
}
if (nodeName != null) {
log.infof("Node name: %s", nodeName);
}
}
}

View file

@ -346,7 +346,7 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
driver.navigate().to(linkFromMail.trim());
infoPage.assertCurrent();
Assert.assertThat(infoPage.getInfo(), startsWith("Your account was successfully linked with " + getProviderId() + " account pedroigor"));
Assert.assertThat(infoPage.getInfo(), startsWith("You successfully verified your email. Please go back to your original browser and continue there with the login."));
}
/**
@ -384,15 +384,13 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
// authenticated, but not redirected to app. Just seeing info page.
infoPage2.assertCurrent();
Assert.assertThat(infoPage2.getInfo(), startsWith("Your account was successfully linked with " + getProviderId() + " account pedroigor"));
Assert.assertThat(infoPage2.getInfo(), startsWith("You successfully verified your email. Please go back to your original browser and continue there with the login."));
} finally {
// Revert everything
webRule2.after();
}
driver.navigate().refresh();
this.loginExpiredPage.assertCurrent();
this.loginExpiredPage.clickLoginContinueLink();
this.idpLinkEmailPage.clickContinueFlowLink();
// authenticated and redirected to app. User is linked with identity provider
assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");

View file

@ -31,6 +31,9 @@ public class IdpLinkEmailPage extends AbstractPage {
@FindBy(linkText = "Click here")
private WebElement resendEmailLink;
@FindBy(linkText = "Click here") // Actually same link like "resendEmailLink"
private WebElement continueFlowLink;
@Override
public boolean isCurrent() {
return driver.getTitle().startsWith("Link ");
@ -40,8 +43,8 @@ public class IdpLinkEmailPage extends AbstractPage {
resendEmailLink.click();
}
public String getResendEmailLink() {
return resendEmailLink.getAttribute("href");
public void clickContinueFlowLink() {
continueFlowLink.click();
}
@Override

View file

@ -90,7 +90,7 @@
"sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}",
"l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:600000}",
"remoteStoreEnabled": "${keycloak.connectionsInfinispan.remoteStoreEnabled:false}",
"remoteStoreHost": "${keycloak.connectionsInfinispan.remoteStoreHost:localhost}",
"remoteStoreHost": "${keycloak.connectionsjen neInfinispan.remoteStoreHost:localhost}",
"remoteStorePort": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
}
},

View file

@ -83,3 +83,5 @@ log4j.logger.org.apache.directory.server.ldap.LdapProtocolHandler=error
log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
log4j.logger.org.keycloak.services.resources.IdentityBrokerService=trace
log4j.logger.org.keycloak.broker=trace
# log4j.logger.io.undertow=trace

View file

@ -11,5 +11,8 @@
<p id="instruction2" class="instruction">
${msg("emailLinkIdp2")} <a href="${url.loginAction}">${msg("doClickHere")}</a> ${msg("emailLinkIdp3")}
</p>
<p id="instruction3" class="instruction">
${msg("emailLinkIdp4")} <a href="${url.loginAction}">${msg("doClickHere")}</a> ${msg("emailLinkIdp5")}
</p>
</#if>
</@layout.registrationLayout>

View file

@ -83,6 +83,8 @@ emailLinkIdpTitle=Link {0}
emailLinkIdp1=An email with instructions to link {0} account {1} with your {2} account has been sent to you.
emailLinkIdp2=Haven''t received a verification code in your email?
emailLinkIdp3=to re-send the email.
emailLinkIdp4=If you already verified the email in different browser
emailLinkIdp5=to continue.
backToLogin=&laquo; Back to Login
@ -208,7 +210,7 @@ sessionNotActiveMessage=Session not active.
invalidCodeMessage=An error occurred, please login again through your application.
identityProviderUnexpectedErrorMessage=Unexpected error when authenticating with identity provider
identityProviderNotFoundMessage=Could not find an identity provider with the identifier.
identityProviderLinkSuccess=Your account was successfully linked with {0} account {1} .
identityProviderLinkSuccess=You successfully verified your email. Please go back to your original browser and continue there with the login.
staleCodeMessage=This page is no longer valid, please go back to your application and login again
realmSupportsNoCredentialsMessage=Realm does not support any credential type.
identityProviderNotUniqueMessage=Realm supports multiple identity providers. Could not determine which identity provider should be used to authenticate with.