KEYCLOAK-4899 Replace updates to user session with temporary auth session

This commit is contained in:
Hynek Mlnarik 2017-09-01 14:37:01 +02:00
parent 9032889c15
commit 24e9cbb292
13 changed files with 329 additions and 95 deletions

View file

@ -82,25 +82,28 @@ public class UserSessionAdapter implements UserSessionModel {
});
}
// Update user session
if (!removedClientUUIDS.isEmpty()) {
UserSessionUpdateTask task = new UserSessionUpdateTask() {
@Override
public void runUpdate(UserSessionEntity entity) {
for (String clientUUID : removedClientUUIDS) {
entity.getAuthenticatedClientSessions().remove(clientUUID);
}
}
};
update(task);
}
removeAuthenticatedClientSessions(removedClientUUIDS);
return Collections.unmodifiableMap(result);
}
@Override
public void removeAuthenticatedClientSessions(Iterable<String> removedClientUUIDS) {
if (removedClientUUIDS == null || ! removedClientUUIDS.iterator().hasNext()) {
return;
}
// Update user session
UserSessionUpdateTask task = new UserSessionUpdateTask() {
@Override
public void runUpdate(UserSessionEntity entity) {
removedClientUUIDS.forEach(entity.getAuthenticatedClientSessions()::remove);
}
};
update(task);
}
public String getId() {
return entity.getId();
}

View file

@ -160,6 +160,15 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
return authenticatedClientSessions;
}
@Override
public void removeAuthenticatedClientSessions(Iterable<String> removedClientUUIDS) {
if (removedClientUUIDS == null || ! removedClientUUIDS.iterator().hasNext()) {
return;
}
removedClientUUIDS.forEach(authenticatedClientSessions::remove);
}
@Override
public String getNote(String name) {
return getData().getNotes()==null ? null : getData().getNotes().get(name);

View file

@ -34,8 +34,16 @@ public interface ClientModel extends RoleContainerModel, ProtocolMapperContaine
void updateClient();
/**
* Returns client internal ID (UUID).
* @return
*/
String getId();
/**
* Returns client ID as defined by the user.
* @return
*/
String getClientId();
void setClientId(String clientId);

View file

@ -52,7 +52,17 @@ public interface UserSessionModel {
void setLastSessionRefresh(int seconds);
/**
* Returns map where key is ID of the client (its UUID) and value is the respective {@link AuthenticatedClientSessionModel} object.
* @return
*/
Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions();
/**
* Removes authenticated client sessions for all clients whose UUID is present in {@code removedClientUUIDS} parameter.
* @param removedClientUUIDS
*/
void removeAuthenticatedClientSessions(Iterable<String> removedClientUUIDS);
public String getNote(String name);
public void setNote(String name, String value);

View file

@ -27,7 +27,10 @@ import java.util.Map;
*/
public interface AuthenticationSessionProvider extends Provider {
// Generates random ID
/**
* Creates and registers a new authentication session with random ID. Authentication session
* entity will be prefilled with current timestamp, the given realm and client.
*/
AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client);
AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client);

View file

@ -59,6 +59,7 @@ public interface CommonClientSessionModel {
CODE_TO_TOKEN,
AUTHENTICATE,
LOGGED_OUT,
LOGGING_OUT,
REQUIRED_ACTIONS
}

View file

@ -66,6 +66,7 @@ import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.security.PublicKey;
import java.util.*;
import java.util.stream.Collectors;
/**
* Stateless object that manages authentication
@ -78,6 +79,11 @@ public class AuthenticationManager {
public static final String END_AFTER_REQUIRED_ACTIONS = "END_AFTER_REQUIRED_ACTIONS";
public static final String INVALIDATE_ACTION_TOKEN = "INVALIDATE_ACTION_TOKEN";
/**
* Auth session note on client logout state (when logging out)
*/
public static final String CLIENT_LOGOUT_STATE = "logout.state.";
// userSession note with authTime (time when authentication flow including requiredActions was finished)
public static final String AUTH_TIME = "AUTH_TIME";
// clientSession note with flag that clientSession was authenticated through SSO cookie
@ -165,14 +171,49 @@ public class AuthenticationManager {
boolean logoutBroker) {
if (userSession == null) return;
UserModel user = userSession.getUser();
userSession.setState(UserSessionModel.State.LOGGING_OUT);
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
userSession.setState(UserSessionModel.State.LOGGING_OUT);
}
logger.debugv("Logging out: {0} ({1})", user.getUsername(), userSession.getId());
expireUserSessionCookie(session, userSession, realm, uriInfo, headers, connection);
for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
backchannelLogoutClientSession(session, realm, clientSession, userSession, uriInfo, headers);
final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
AuthenticationSessionModel logoutAuthSession = createOrJoinLogoutSession(realm, asm, false);
try {
backchannelLogoutAll(session, realm, userSession, logoutAuthSession, uriInfo, headers, logoutBroker);
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
} finally {
asm.removeAuthenticationSession(realm, logoutAuthSession, false);
}
userSession.setState(UserSessionModel.State.LOGGED_OUT);
session.sessions().removeUserSession(realm, userSession);
}
private static AuthenticationSessionModel createOrJoinLogoutSession(RealmModel realm, final AuthenticationSessionManager asm, boolean browserCookie) {
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm);
// Try to join existing logout session if it exists and browser session is required
if (browserCookie && logoutAuthSession != null) {
if (Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), logoutAuthSession.getAction())) {
return logoutAuthSession;
}
logoutAuthSession.restartSession(realm, client);
} else {
logoutAuthSession = asm.createAuthenticationSession(realm, client, browserCookie);
}
logoutAuthSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name());
return logoutAuthSession;
}
private static void backchannelLogoutAll(KeycloakSession session, RealmModel realm,
UserSessionModel userSession, AuthenticationSessionModel logoutAuthSession, UriInfo uriInfo,
HttpHeaders headers, boolean logoutBroker) {
userSession.getAuthenticatedClientSessions().values().forEach(
clientSession -> backchannelLogoutClientSession(session, realm, clientSession, logoutAuthSession, uriInfo, headers)
);
if (logoutBroker) {
String brokerId = userSession.getNote(Details.IDENTITY_PROVIDER);
if (brokerId != null) {
@ -184,32 +225,180 @@ public class AuthenticationManager {
}
}
}
userSession.setState(UserSessionModel.State.LOGGED_OUT);
session.sessions().removeUserSession(realm, userSession);
}
public static void backchannelLogoutClientSession(KeycloakSession session, RealmModel realm, AuthenticatedClientSessionModel clientSession, UserSessionModel userSession, UriInfo uriInfo, HttpHeaders headers) {
/**
* Checks that all sessions have been removed from the user session. The list of logged out clients is determined from
* the {@code logoutAuthSession} auth session notes.
* @param realm
* @param userSession
* @param logoutAuthSession
* @return {@code true} when all clients have been logged out, {@code false} otherwise
*/
private static boolean checkUserSessionOnlyHasLoggedOutClients(RealmModel realm,
UserSessionModel userSession, AuthenticationSessionModel logoutAuthSession) {
final Map<String, AuthenticatedClientSessionModel> acs = userSession.getAuthenticatedClientSessions();
Set<AuthenticatedClientSessionModel> notLoggedOutSessions = acs.entrySet().stream()
.filter(me -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT, getClientLogoutAction(logoutAuthSession, me.getKey())))
.filter(me -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT.name(), me.getValue().getAction()))
.filter(me -> Objects.nonNull(me.getValue().getProtocol())) // Keycloak service-like accounts
.map(Map.Entry::getValue)
.collect(Collectors.toSet());
boolean allClientsLoggedOut = notLoggedOutSessions.isEmpty();
if (! allClientsLoggedOut) {
logger.warnf("Some clients have been not been logged out for user %s in %s realm: %s",
userSession.getUser().getUsername(), realm.getName(),
notLoggedOutSessions.stream()
.map(AuthenticatedClientSessionModel::getClient)
.map(ClientModel::getClientId)
.sorted()
.collect(Collectors.joining(", "))
);
} else if (logger.isDebugEnabled()) {
logger.debugf("All clients have been logged out for user %s in %s realm, session %s",
userSession.getUser().getUsername(), realm.getName(), userSession.getId());
}
return allClientsLoggedOut;
}
/**
* Logs out the given client session and records the result into {@code logoutAuthSession} if set.
* @param session
* @param realm
* @param clientSession
* @param logoutAuthSession auth session used for recording result of logout. May be {@code null}
* @param uriInfo
* @param headers
* @return {@code true} if the client was or is already being logged out, {@code false} if logout failed or it is not known how to log it out.
*/
private static boolean backchannelLogoutClientSession(KeycloakSession session, RealmModel realm,
AuthenticatedClientSessionModel clientSession, AuthenticationSessionModel logoutAuthSession,
UriInfo uriInfo, HttpHeaders headers) {
UserSessionModel userSession = clientSession.getUserSession();
ClientModel client = clientSession.getClient();
if (!client.isFrontchannelLogout() && !AuthenticatedClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) {
if (client.isFrontchannelLogout() || AuthenticationSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) {
return false;
}
final AuthenticationSessionModel.Action logoutState = getClientLogoutAction(logoutAuthSession, client.getId());
if (logoutState == AuthenticationSessionModel.Action.LOGGED_OUT || logoutState == AuthenticationSessionModel.Action.LOGGING_OUT) {
return true;
}
try {
setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGING_OUT);
String authMethod = clientSession.getProtocol();
if (authMethod == null) return; // must be a keycloak service like account
if (authMethod == null) return true; // must be a keycloak service like account
logger.debugv("backchannel logout to: {0}", client.getClientId());
LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
protocol.setRealm(realm)
.setHttpHeaders(headers)
.setUriInfo(uriInfo);
protocol.backchannelLogout(userSession, clientSession);
clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name());
}
setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGED_OUT);
return true;
} catch (Exception ex) {
ServicesLogger.LOGGER.failedToLogoutClient(ex);
return false;
}
}
// Logout all clientSessions of this user and client
public static void backchannelUserFromClient(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client, UriInfo uriInfo, HttpHeaders headers) {
private static Response frontchannelLogoutClientSession(KeycloakSession session, RealmModel realm,
AuthenticatedClientSessionModel clientSession, AuthenticationSessionModel logoutAuthSession,
UriInfo uriInfo, HttpHeaders headers) {
UserSessionModel userSession = clientSession.getUserSession();
ClientModel client = clientSession.getClient();
if (! client.isFrontchannelLogout() || AuthenticationSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) {
return null;
}
final AuthenticationSessionModel.Action logoutState = getClientLogoutAction(logoutAuthSession, client.getId());
if (logoutState == AuthenticationSessionModel.Action.LOGGED_OUT || logoutState == AuthenticationSessionModel.Action.LOGGING_OUT) {
return null;
}
try {
setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGING_OUT);
String authMethod = clientSession.getProtocol();
if (authMethod == null) return null; // must be a keycloak service like account
logger.debugv("frontchannel logout to: {0}", client.getClientId());
LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
protocol.setRealm(realm)
.setHttpHeaders(headers)
.setUriInfo(uriInfo);
Response response = protocol.frontchannelLogout(userSession, clientSession);
if (response != null) {
logger.debug("returning frontchannel logout request to client");
// setting this to logged out cuz I'm not sure protocols can always verify that the client was logged out or not
setClientLogoutAction(logoutAuthSession, client.getId(), AuthenticationSessionModel.Action.LOGGED_OUT);
return response;
}
} catch (Exception e) {
ServicesLogger.LOGGER.failedToLogoutClient(e);
}
return null;
}
/**
* Sets logout state of the particular client into the {@code logoutAuthSession}
* @param logoutAuthSession logoutAuthSession. May be {@code null} in which case this is a no-op.
* @param client Client. Must not be {@code null}
* @param state
*/
public static void setClientLogoutAction(AuthenticationSessionModel logoutAuthSession, String clientUuid, AuthenticationSessionModel.Action action) {
if (logoutAuthSession != null && clientUuid != null) {
logoutAuthSession.setAuthNote(CLIENT_LOGOUT_STATE + clientUuid, action.name());
}
}
/**
* Returns the logout state of the particular client as per the {@code logoutAuthSession}
* @param logoutAuthSession logoutAuthSession. May be {@code null} in which case this is a no-op.
* @param clientUuid Internal ID of the client. Must not be {@code null}
* @return State if it can be determined, {@code null} otherwise.
*/
public static AuthenticationSessionModel.Action getClientLogoutAction(AuthenticationSessionModel logoutAuthSession, String clientUuid) {
if (logoutAuthSession == null || clientUuid == null) {
return null;
}
String state = logoutAuthSession.getAuthNote(CLIENT_LOGOUT_STATE + clientUuid);
return state == null ? null : AuthenticationSessionModel.Action.valueOf(state);
}
/**
* Logout all clientSessions of this user and client
* @param session
* @param realm
* @param user
* @param client
* @param uriInfo
* @param headers
*/
public static void backchannelLogoutUserFromClient(KeycloakSession session, RealmModel realm, UserModel user, ClientModel client, UriInfo uriInfo, HttpHeaders headers) {
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
for (UserSessionModel userSession : userSessions) {
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
if (clientSession != null) {
AuthenticationManager.backchannelLogoutClientSession(session, realm, clientSession, userSession, uriInfo, headers);
AuthenticationManager.backchannelLogoutClientSession(session, realm, clientSession, null, uriInfo, headers);
clientSession.setAction(AuthenticationSessionModel.Action.LOGGED_OUT.name());
TokenManager.dettachClientSession(session.sessions(), realm, clientSession);
}
}
@ -217,67 +406,61 @@ public class AuthenticationManager {
public static Response browserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
if (userSession == null) return null;
UserModel user = userSession.getUser();
logger.debugv("Logging out: {0} ({1})", user.getUsername(), userSession.getId());
if (logger.isDebugEnabled()) {
UserModel user = userSession.getUser();
logger.debugv("Logging out: {0} ({1})", user.getUsername(), userSession.getId());
}
if (userSession.getState() != UserSessionModel.State.LOGGING_OUT) {
userSession.setState(UserSessionModel.State.LOGGING_OUT);
}
List<AuthenticatedClientSessionModel> redirectClients = new LinkedList<>();
for (AuthenticatedClientSessionModel clientSession : userSession.getAuthenticatedClientSessions().values()) {
ClientModel client = clientSession.getClient();
if (AuthenticatedClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) continue;
if (client.isFrontchannelLogout()) {
String authMethod = clientSession.getProtocol();
if (authMethod == null) continue; // must be a keycloak service like account
redirectClients.add(clientSession);
} else {
String authMethod = clientSession.getProtocol();
if (authMethod == null) continue; // must be a keycloak service like account
LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
protocol.setRealm(realm)
.setHttpHeaders(headers)
.setUriInfo(uriInfo);
try {
logger.debugv("backchannel logout to: {0}", client.getClientId());
protocol.backchannelLogout(userSession, clientSession);
clientSession.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name());
} catch (Exception e) {
ServicesLogger.LOGGER.failedToLogoutClient(e);
}
}
final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
AuthenticationSessionModel logoutAuthSession = createOrJoinLogoutSession(realm, asm, true);
Response response = browserLogoutAllClients(userSession, session, realm, headers, uriInfo, logoutAuthSession);
if (response != null) {
return response;
}
for (AuthenticatedClientSessionModel nextRedirectClient : redirectClients) {
String authMethod = nextRedirectClient.getProtocol();
LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod);
protocol.setRealm(realm)
.setHttpHeaders(headers)
.setUriInfo(uriInfo);
// setting this to logged out cuz I"m not sure protocols can always verify that the client was logged out or not
nextRedirectClient.setAction(AuthenticatedClientSessionModel.Action.LOGGED_OUT.name());
try {
logger.debugv("frontchannel logout to: {0}", nextRedirectClient.getClient().getClientId());
Response response = protocol.frontchannelLogout(userSession, nextRedirectClient);
if (response != null) {
logger.debug("returning frontchannel logout request to client");
return response;
}
} catch (Exception e) {
ServicesLogger.LOGGER.failedToLogoutClient(e);
}
}
String brokerId = userSession.getNote(Details.IDENTITY_PROVIDER);
if (brokerId != null) {
IdentityProvider identityProvider = IdentityBrokerService.getIdentityProvider(session, realm, brokerId);
Response response = identityProvider.keycloakInitiatedBrowserLogout(session, userSession, uriInfo, realm);
if (response != null) return response;
response = identityProvider.keycloakInitiatedBrowserLogout(session, userSession, uriInfo, realm);
if (response != null) {
return response;
}
}
return finishBrowserLogout(session, realm, userSession, uriInfo, connection, headers);
}
private static Response browserLogoutAllClients(UserSessionModel userSession, KeycloakSession session, RealmModel realm, HttpHeaders headers, UriInfo uriInfo, AuthenticationSessionModel logoutAuthSession) {
Map<Boolean, List<AuthenticatedClientSessionModel>> acss = userSession.getAuthenticatedClientSessions().values().stream()
.filter(clientSession -> ! Objects.equals(AuthenticationSessionModel.Action.LOGGED_OUT.name(), clientSession.getAction()))
.filter(clientSession -> clientSession.getProtocol() != null)
.collect(Collectors.partitioningBy(clientSession -> clientSession.getClient().isFrontchannelLogout()));
final List<AuthenticatedClientSessionModel> backendLogoutSessions = acss.get(false) == null ? Collections.emptyList() : acss.get(false);
backendLogoutSessions.forEach(acs -> backchannelLogoutClientSession(session, realm, acs, logoutAuthSession, uriInfo, headers));
final List<AuthenticatedClientSessionModel> redirectClients = acss.get(true) == null ? Collections.emptyList() : acss.get(true);
for (AuthenticatedClientSessionModel nextRedirectClient : redirectClients) {
Response response = frontchannelLogoutClientSession(session, realm, nextRedirectClient, logoutAuthSession, uriInfo, headers);
if (response != null) {
return response;
}
}
return null;
}
public static Response finishBrowserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm);
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
expireIdentityCookie(realm, uriInfo, connection);
expireRememberMeCookie(realm, uriInfo, connection);
userSession.setState(UserSessionModel.State.LOGGED_OUT);
@ -316,7 +499,7 @@ public class AuthenticationManager {
String encoded = encodeToken(keycloakSession, realm, identityToken);
boolean secureOnly = realm.getSslRequired().isRequired(connection);
int maxAge = NewCookie.DEFAULT_MAX_AGE;
if (session.isRememberMe()) {
if (session != null && session.isRememberMe()) {
maxAge = realm.getSsoSessionMaxLifespan();
}
logger.debugv("Create login cookie - name: {0}, path: {1}, max-age: {2}", KEYCLOAK_IDENTITY_COOKIE, cookiePath, maxAge);

View file

@ -45,7 +45,14 @@ public class AuthenticationSessionManager {
this.session = session;
}
/**
* Creates a fresh authentication session for the given realm and client. Optionally sets the browser
* authentication session cookie {@link #AUTH_SESSION_ID} with the ID of the new session.
* @param realm
* @param client
* @param browserCookie Set the cookie in the browser for the
* @return
*/
public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client, boolean browserCookie) {
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client);
@ -57,11 +64,20 @@ public class AuthenticationSessionManager {
}
/**
* Returns ID of current authentication session if it exists, otherwise returns {@code null}.
* @param realm
* @return
*/
public String getCurrentAuthenticationSessionId(RealmModel realm) {
return getAuthSessionCookieDecoded(realm);
}
/**
* Returns current authentication session if it exists, otherwise returns {@code null}.
* @param realm
* @return
*/
public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm) {
String authSessionId = getAuthSessionCookieDecoded(realm);
return authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId);

View file

@ -420,7 +420,7 @@ public class AccountFormService extends AbstractSecuredLocalService {
new UserSessionManager(session).revokeOfflineToken(user, client);
// Logout clientSessions for this user and client
AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);
AuthenticationManager.backchannelLogoutUserFromClient(session, realm, user, client, uriInfo, headers);
event.event(EventType.REVOKE_GRANT).client(auth.getClient()).user(auth.getUser()).detail(Details.REVOKED_CLIENT, client.getClientId()).success();
setReferrerOnPage();

View file

@ -490,7 +490,7 @@ public class UserResource {
if (revokedConsent) {
// Logout clientSessions for this user and client
AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);
AuthenticationManager.backchannelLogoutUserFromClient(session, realm, user, client, uriInfo, headers);
}
if (!revokedConsent && !revokedOfflineToken) {

View file

@ -143,7 +143,7 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
config.put(POST_BINDING_AUTHN_REQUEST, "true");
config.put(VALIDATE_SIGNATURE, "false");
config.put(WANT_AUTHN_REQUESTS_SIGNED, "false");
config.put(BACKCHANNEL_SUPPORTED, "true");
config.put(BACKCHANNEL_SUPPORTED, "false");
return idp;
}

View file

@ -25,6 +25,7 @@ import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.Retry;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.LoginPage;
@ -225,11 +226,13 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
adminClient.realm("test").users().get(user.getId()).logout();
user = adminClient.realm("test").users().get(user.getId()).toRepresentation();
Assert.assertTrue(user.getNotBefore() > 0);
Retry.execute(() -> {
UserRepresentation u = adminClient.realm("test").users().get(user.getId()).toRepresentation();
Assert.assertTrue(u.getNotBefore() > 0);
loginPage.open();
loginPage.assertCurrent();
loginPage.open();
loginPage.assertCurrent();
}, 10, 200);
}
}

View file

@ -48,11 +48,9 @@ import java.io.IOException;
import java.net.URI;
import java.util.Set;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.*;
/**
* @author pedroigor
@ -470,19 +468,19 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent
// Login as pedroigor to account management
accountFederatedIdentityPage.realm("realm-with-broker");
accountFederatedIdentityPage.open();
assertTrue(driver.getTitle().equals("Log in to realm-with-broker"));
assertThat(driver.getTitle(), is("Log in to realm-with-broker"));
loginPage.login("pedroigor", "password");
assertTrue(accountFederatedIdentityPage.isCurrent());
accountFederatedIdentityPage.assertCurrent();
// Try to link my "pedroigor" identity with "test-user" from brokered Keycloak.
accountFederatedIdentityPage.clickAddProvider(identityProvider.getAlias());
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/"));
assertThat(this.driver.getCurrentUrl(), startsWith("http://localhost:8082/auth/"));
this.loginPage.login("test-user", "password");
doAfterProviderAuthentication();
// Error is displayed in account management because federated identity"test-user" already linked to local account "test-user"
assertTrue(accountFederatedIdentityPage.isCurrent());
accountFederatedIdentityPage.assertCurrent();
assertEquals("Federated identity returned by " + getProviderId() + " is already linked to another user.", accountFederatedIdentityPage.getError());
}