KEYCLOAK-4626 AuthenticationSessions - brokering works. Few other fixes and tests added

This commit is contained in:
mposolda 2017-03-27 22:41:36 +02:00
parent b55b089355
commit e7272dc05a
98 changed files with 3628 additions and 1703 deletions

View file

@ -24,6 +24,7 @@ import java.util.Map;
import java.util.Set;
import org.infinispan.Cache;
import org.keycloak.common.util.Time;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@ -142,35 +143,41 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
}
@Override
public String getNote(String name) {
return entity.getNotes() != null ? entity.getNotes().get(name) : null;
public String getClientNote(String name) {
return entity.getClientNotes() != null ? entity.getClientNotes().get(name) : null;
}
@Override
public void setNote(String name, String value) {
if (entity.getNotes() == null) {
entity.setNotes(new HashMap<String, String>());
public void setClientNote(String name, String value) {
if (entity.getClientNotes() == null) {
entity.setClientNotes(new HashMap<>());
}
entity.getNotes().put(name, value);
entity.getClientNotes().put(name, value);
update();
}
@Override
public void removeNote(String name) {
if (entity.getNotes() != null) {
entity.getNotes().remove(name);
public void removeClientNote(String name) {
if (entity.getClientNotes() != null) {
entity.getClientNotes().remove(name);
}
update();
}
@Override
public Map<String, String> getNotes() {
if (entity.getNotes() == null || entity.getNotes().isEmpty()) return Collections.emptyMap();
public Map<String, String> getClientNotes() {
if (entity.getClientNotes() == null || entity.getClientNotes().isEmpty()) return Collections.emptyMap();
Map<String, String> copy = new HashMap<>();
copy.putAll(entity.getNotes());
copy.putAll(entity.getClientNotes());
return copy;
}
@Override
public void clearClientNotes() {
entity.setClientNotes(new HashMap<>());
update();
}
@Override
public String getAuthNote(String name) {
return entity.getAuthNotes() != null ? entity.getAuthNotes().get(name) : null;
@ -286,4 +293,21 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
else entity.setAuthUserId(user.getId());
update();
}
@Override
public void updateClient(ClientModel client) {
entity.setClientUuid(client.getId());
update();
}
@Override
public void restartSession(RealmModel realm, ClientModel client) {
String id = entity.getId();
entity = new AuthenticationSessionEntity();
entity.setId(id);
entity.setRealm(realm.getId());
entity.setClientUuid(client.getId());
entity.setTimestamp(Time.currentTime());
update();
}
}

View file

@ -31,7 +31,6 @@ import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEnt
import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.RealmInfoUtil;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.AuthenticationSessionProvider;
@ -46,8 +45,6 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
private final Cache<String, AuthenticationSessionEntity> cache;
protected final InfinispanKeycloakTransaction tx;
public static final String AUTH_SESSION_ID = "AUTH_SESSION_ID";
public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache<String, AuthenticationSessionEntity> cache) {
this.session = session;
this.cache = cache;
@ -56,11 +53,14 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
session.getTransactionManager().enlistAfterCompletion(tx);
}
@Override
public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client) {
String id = KeycloakModelUtils.generateId();
return createAuthenticationSession(id, realm, client);
}
@Override
public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client, boolean browser) {
String id = KeycloakModelUtils.generateId();
public AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client) {
AuthenticationSessionEntity entity = new AuthenticationSessionEntity();
entity.setId(id);
entity.setRealm(realm.getId());
@ -69,10 +69,6 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
tx.put(cache, id, entity);
if (browser) {
setBrowserCookie(id, realm);
}
AuthenticationSessionAdapter wrap = wrap(realm, entity);
return wrap;
}
@ -81,17 +77,6 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
return entity==null ? null : new AuthenticationSessionAdapter(session, this, cache, realm, entity);
}
@Override
public String getCurrentAuthenticationSessionId(RealmModel realm) {
return getIdFromBrowserCookie();
}
@Override
public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm) {
String authSessionId = getIdFromBrowserCookie();
return authSessionId==null ? null : getAuthenticationSession(realm, authSessionId);
}
@Override
public AuthenticationSessionModel getAuthenticationSession(RealmModel realm, String authenticationSessionId) {
AuthenticationSessionEntity entity = getAuthenticationSessionEntity(realm, authenticationSessionId);
@ -99,11 +84,11 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
}
private AuthenticationSessionEntity getAuthenticationSessionEntity(RealmModel realm, String authSessionId) {
AuthenticationSessionEntity entity = cache.get(authSessionId);
// Chance created in this transaction
AuthenticationSessionEntity entity = tx.get(cache, authSessionId);
// Chance created in this transaction TODO:mposolda should it be opposite and rather look locally first? Check performance...
if (entity == null) {
entity = tx.get(cache, authSessionId);
entity = cache.get(authSessionId);
}
return entity;
@ -156,28 +141,4 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
}
// COOKIE STUFF
protected void setBrowserCookie(String authSessionId, RealmModel realm) {
String cookiePath = CookieHelper.getRealmCookiePath(realm);
boolean sslRequired = realm.getSslRequired().isRequired(session.getContext().getConnection());
CookieHelper.addCookie(AUTH_SESSION_ID, authSessionId, cookiePath, null, null, -1, sslRequired, true);
// TODO trace with isTraceEnabled
log.infof("Set AUTH_SESSION_ID cookie with value %s", authSessionId);
}
protected String getIdFromBrowserCookie() {
String cookieVal = CookieHelper.getCookieValue(AUTH_SESSION_ID);
if (log.isTraceEnabled()) {
if (cookieVal != null) {
log.tracef("Found AUTH_SESSION_ID cookie with value %s", cookieVal);
} else {
log.tracef("Not found AUTH_SESSION_ID cookie");
}
}
return cookieVal;
}
}

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.models.sessions.infinispan;
import org.infinispan.context.Flag;
import org.keycloak.models.KeycloakTransaction;
import java.util.HashMap;
@ -31,7 +32,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
private final static Logger log = Logger.getLogger(InfinispanKeycloakTransaction.class);
public enum CacheOperation {
ADD, REMOVE, REPLACE
ADD, REMOVE, REPLACE, ADD_IF_ABSENT // ADD_IF_ABSENT throws an exception if there is existing value
}
private boolean active;
@ -79,7 +80,18 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
if (tasks.containsKey(taskKey)) {
throw new IllegalStateException("Can't add session: task in progress for session");
} else {
tasks.put(taskKey, new CacheTask(cache, CacheOperation.ADD, key, value));
tasks.put(taskKey, new CacheTask<>(cache, CacheOperation.ADD, key, value));
}
}
public <K, V> void putIfAbsent(Cache<K, V> cache, K key, V value) {
log.tracev("Adding cache operation: {0} on {1}", CacheOperation.ADD_IF_ABSENT, key);
Object taskKey = getTaskKey(cache, key);
if (tasks.containsKey(taskKey)) {
throw new IllegalStateException("Can't add session: task in progress for session");
} else {
tasks.put(taskKey, new CacheTask<>(cache, CacheOperation.ADD_IF_ABSENT, key, value));
}
}
@ -91,6 +103,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
if (current != null) {
switch (current.operation) {
case ADD:
case ADD_IF_ABSENT:
case REPLACE:
current.value = value;
return;
@ -98,7 +111,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
return;
}
} else {
tasks.put(taskKey, new CacheTask(cache, CacheOperation.REPLACE, key, value));
tasks.put(taskKey, new CacheTask<>(cache, CacheOperation.REPLACE, key, value));
}
}
@ -106,7 +119,7 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
log.tracev("Adding cache operation: {0} on {1}", CacheOperation.REMOVE, key);
Object taskKey = getTaskKey(cache, key);
tasks.put(taskKey, new CacheTask(cache, CacheOperation.REMOVE, key, null));
tasks.put(taskKey, new CacheTask<>(cache, CacheOperation.REMOVE, key, null));
}
// This is for possibility to lookup for session by id, which was created in this transaction
@ -116,12 +129,16 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
if (current != null) {
switch (current.operation) {
case ADD:
case ADD_IF_ABSENT:
case REPLACE:
return current.value;
case REMOVE:
return null;
}
}
return null;
// Should we have per-transaction cache for lookups?
return cache.get(key);
}
private static <K, V> Object getTaskKey(Cache<K, V> cache, K key) {
@ -152,15 +169,28 @@ public class InfinispanKeycloakTransaction implements KeycloakTransaction {
switch (operation) {
case ADD:
cache.put(key, value);
decorateCache().put(key, value);
break;
case REMOVE:
cache.remove(key);
decorateCache().remove(key);
break;
case REPLACE:
cache.replace(key, value);
decorateCache().replace(key, value);
break;
case ADD_IF_ABSENT:
V existing = cache.putIfAbsent(key, value);
if (existing != null) {
throw new IllegalStateException("IllegalState. There is already existing value in cache for key " + key);
}
break;
}
}
// Ignore return values. Should have better performance within cluster / cross-dc env
private Cache<K, V> decorateCache() {
return cache.getAdvancedCache()
.withFlags(Flag.IGNORE_RETURN_VALUES, Flag.SKIP_REMOTE_LOOKUP);
}
}
}

View file

@ -120,9 +120,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
}
@Override
public UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
String id = KeycloakModelUtils.generateId();
public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId) {
UserSessionEntity entity = new UserSessionEntity();
entity.setId(id);
entity.setRealm(realm.getId());
@ -139,7 +137,7 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
entity.setStarted(currentTime);
entity.setLastSessionRefresh(currentTime);
tx.put(sessionCache, id, entity);
tx.putIfAbsent(sessionCache, id, entity);
return wrap(realm, entity, false);
}
@ -151,11 +149,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
protected ClientSessionModel getClientSession(RealmModel realm, String id, boolean offline) {
Cache<String, SessionEntity> cache = getCache(offline);
ClientSessionEntity entity = (ClientSessionEntity) cache.get(id);
ClientSessionEntity entity = (ClientSessionEntity) tx.get(cache, id); // Chance created in this transaction
// Chance created in this transaction
if (entity == null) {
entity = (ClientSessionEntity) tx.get(cache, id);
entity = (ClientSessionEntity) cache.get(id);
}
return wrap(realm, entity, offline);
@ -163,11 +160,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public ClientSessionModel getClientSession(String id) {
ClientSessionEntity entity = (ClientSessionEntity) sessionCache.get(id);
// Chance created in this transaction
ClientSessionEntity entity = (ClientSessionEntity) tx.get(sessionCache, id);
if (entity == null) {
entity = (ClientSessionEntity) tx.get(sessionCache, id);
entity = (ClientSessionEntity) sessionCache.get(id);
}
if (entity != null) {
@ -184,11 +181,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) {
Cache<String, SessionEntity> cache = getCache(offline);
UserSessionEntity entity = (UserSessionEntity) cache.get(id);
UserSessionEntity entity = (UserSessionEntity) tx.get(cache, id); // Chance created in this transaction
// Chance created in this transaction
if (entity == null) {
entity = (UserSessionEntity) tx.get(cache, id);
entity = (UserSessionEntity) cache.get(id);
}
return wrap(realm, entity, offline);
@ -742,11 +738,10 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
@Override
public ClientInitialAccessModel getClientInitialAccessModel(RealmModel realm, String id) {
Cache<String, SessionEntity> cache = getCache(false);
ClientInitialAccessEntity entity = (ClientInitialAccessEntity) cache.get(id);
ClientInitialAccessEntity entity = (ClientInitialAccessEntity) tx.get(cache, id); // Chance created in this transaction
// If created in this transaction
if (entity == null) {
entity = (ClientInitialAccessEntity) tx.get(cache, id);
entity = (ClientInitialAccessEntity) cache.get(id);
}
return wrap(realm, entity);

View file

@ -67,9 +67,11 @@ public class UserSessionAdapter implements UserSessionModel {
Map<String, ClientLoginSessionEntity> clientSessionEntities = entity.getClientLoginSessions();
Map<String, AuthenticatedClientSessionModel> result = new HashMap<>();
if (clientSessionEntities != null) {
clientSessionEntities.forEach((String key, ClientLoginSessionEntity value) -> {
result.put(key, new AuthenticatedClientSessionAdapter(value, this, provider, cache));
});
}
return Collections.unmodifiableMap(result);
}
@ -96,6 +98,11 @@ public class UserSessionAdapter implements UserSessionModel {
return session.users().getUserById(entity.getUser(), realm);
}
@Override
public void setUser(UserModel user) {
entity.setUser(user.getId());
}
@Override
public String getLoginUsername() {
return entity.getLoginUsername();

View file

@ -41,7 +41,7 @@ public class AuthenticationSessionEntity extends SessionEntity {
private Map<String, AuthenticationSessionModel.ExecutionStatus> executionStatus = new HashMap<>();;
private String protocol;
private Map<String, String> notes;
private Map<String, String> clientNotes;
private Map<String, String> authNotes;
private Set<String> requiredActions = new HashSet<>();
private Map<String, String> userSessionNotes;
@ -118,12 +118,12 @@ public class AuthenticationSessionEntity extends SessionEntity {
this.protocol = protocol;
}
public Map<String, String> getNotes() {
return notes;
public Map<String, String> getClientNotes() {
return clientNotes;
}
public void setNotes(Map<String, String> notes) {
this.notes = notes;
public void setClientNotes(Map<String, String> clientNotes) {
this.clientNotes = clientNotes;
}
public Set<String> getRequiredActions() {

View file

@ -99,6 +99,12 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
*/
void resetFlow();
/**
* Reset the current flow to the beginning and restarts it. Allows to add additional listener, which is triggered after flow restarted
*
*/
void resetFlow(Runnable afterResetListener);
/**
* Fork the current flow. The client session will be cloned and set to point at the realm's browser login flow. The Response will be the result
* of this fork. The previous flow will still be set at the current execution. This is used by reset password when it sends an email.

View file

@ -23,6 +23,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
@ -75,7 +76,7 @@ public abstract class AbstractIdentityProvider<C extends IdentityProviderModel>
}
@Override
public void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context) {
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
}

View file

@ -20,6 +20,7 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.UriInfo;
@ -34,16 +35,16 @@ public class AuthenticationRequest {
private final HttpRequest httpRequest;
private final RealmModel realm;
private final String redirectUri;
private final ClientSessionModel clientSession;
private final AuthenticationSessionModel authSession;
public AuthenticationRequest(KeycloakSession session, RealmModel realm, ClientSessionModel clientSession, HttpRequest httpRequest, UriInfo uriInfo, String state, String redirectUri) {
public AuthenticationRequest(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession, HttpRequest httpRequest, UriInfo uriInfo, String state, String redirectUri) {
this.session = session;
this.realm = realm;
this.httpRequest = httpRequest;
this.uriInfo = uriInfo;
this.state = state;
this.redirectUri = redirectUri;
this.clientSession = clientSession;
this.authSession = authSession;
}
public KeycloakSession getSession() {
@ -76,7 +77,7 @@ public class AuthenticationRequest {
return this.redirectUri;
}
public ClientSessionModel getClientSession() {
return this.clientSession;
public AuthenticationSessionModel getAuthenticationSession() {
return this.authSession;
}
}

View file

@ -17,7 +17,6 @@
package org.keycloak.broker.provider;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
@ -25,6 +24,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.Provider;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
@ -51,7 +51,7 @@ public interface IdentityProvider<C extends IdentityProviderModel> extends Provi
void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, BrokeredIdentityContext context);
void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context);
void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context);
void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context);
void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, BrokeredIdentityContext context);

View file

@ -48,7 +48,6 @@ public interface Details {
String CLIENT_SESSION_STATE = "client_session_state";
String CLIENT_SESSION_HOST = "client_session_host";
String RESTART_AFTER_TIMEOUT = "restart_after_timeout";
String RESTART_REQUESTED = "restart_requested";
String CONSENT = "consent";
String CONSENT_VALUE_NO_CONSENT_REQUIRED = "no_consent_required"; // No consent is required by client
@ -64,4 +63,6 @@ public interface Details {
String CLIENT_REGISTRATION_POLICY = "client_registration_policy";
String EXISTING_USER = "previous_user";
}

View file

@ -37,6 +37,7 @@ public interface Errors {
String USER_DISABLED = "user_disabled";
String USER_TEMPORARILY_DISABLED = "user_temporarily_disabled";
String INVALID_USER_CREDENTIALS = "invalid_user_credentials";
String DIFFERENT_USER_AUTHENTICATED = "different_user_authenticated";
String USERNAME_MISSING = "username_missing";
String USERNAME_IN_USE = "username_in_use";

View file

@ -79,6 +79,9 @@ public enum EventType {
RESET_PASSWORD(true),
RESET_PASSWORD_ERROR(true),
RESTART_AUTHENTICATION(true),
RESTART_AUTHENTICATION_ERROR(true),
INVALID_SIGNATURE(false),
INVALID_SIGNATURE_ERROR(false),
REGISTER_NODE(false),

View file

@ -115,6 +115,11 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
return user;
}
@Override
public void setUser(UserModel user) {
throw new IllegalStateException("Not supported");
}
@Override
public RealmModel getRealm() {
return realm;

View file

@ -23,7 +23,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.provider.Provider;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.HttpHeaders;
@ -67,7 +66,7 @@ public interface LoginProtocol extends Provider {
LoginProtocol setEventBuilder(EventBuilder event);
Response authenticated(UserSessionModel userSession, ClientSessionCode<AuthenticatedClientSessionModel> accessCode);
Response authenticated(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
Response sendError(AuthenticationSessionModel authSession, Error error);

View file

@ -18,6 +18,8 @@
package org.keycloak.models;
import java.util.Map;
import org.keycloak.sessions.CommonClientSessionModel;
/**
@ -27,4 +29,9 @@ public interface AuthenticatedClientSessionModel extends CommonClientSessionMode
void setUserSession(UserSessionModel userSession);
UserSessionModel getUserSession();
public String getNote(String name);
public void setNote(String name, String value);
public void removeNote(String name);
public Map<String, String> getNotes();
}

View file

@ -72,5 +72,10 @@ public interface ClientSessionModel extends CommonClientSessionModel {
public void clearUserSessionNotes();
public String getNote(String name);
public void setNote(String name, String value);
public void removeNote(String name);
public Map<String, String> getNotes();
}

View file

@ -66,8 +66,10 @@ public interface UserSessionModel {
State getState();
void setState(State state);
void setUser(UserModel user);
public static enum State {
LOGGING_IN, // TODO: Maybe state "LOGGING_IN" is useless now once userSession is attached after requiredActions
LOGGING_IN, // TODO:mposolda Maybe state "LOGGING_IN" is useless now once userSession is attached after requiredActions
LOGGED_IN,
LOGGING_OUT,
LOGGED_OUT

View file

@ -32,7 +32,7 @@ public interface UserSessionProvider extends Provider {
ClientSessionModel getClientSession(RealmModel realm, String id);
ClientSessionModel getClientSession(String id);
UserSessionModel createUserSession(RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId);
UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId);
UserSessionModel getUserSession(RealmModel realm, String id);
List<UserSessionModel> getUserSessions(RealmModel realm, UserModel user);
List<UserSessionModel> getUserSessions(RealmModel realm, ClientModel client);

View file

@ -20,6 +20,8 @@ package org.keycloak.sessions;
import java.util.Map;
import java.util.Set;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
/**
@ -34,11 +36,11 @@ public interface AuthenticationSessionModel extends CommonClientSessionModel {
// public void setUserSession(UserSessionModel userSession);
public Map<String, ExecutionStatus> getExecutionStatus();
public void setExecutionStatus(String authenticator, ExecutionStatus status);
public void clearExecutionStatus();
public UserModel getAuthenticatedUser();
public void setAuthenticatedUser(UserModel user);
Map<String, ExecutionStatus> getExecutionStatus();
void setExecutionStatus(String authenticator, ExecutionStatus status);
void clearExecutionStatus();
UserModel getAuthenticatedUser();
void setAuthenticatedUser(UserModel user);
/**
* Required actions that are attached to this client session.
@ -56,26 +58,26 @@ public interface AuthenticationSessionModel extends CommonClientSessionModel {
void removeRequiredAction(UserModel.RequiredAction action);
/**
* These are notes you want applied to the UserSessionModel when the client session is attached to it.
*
* @param name
* @param value
*/
public void setUserSessionNote(String name, String value);
// These are notes you want applied to the UserSessionModel when the client session is attached to it.
void setUserSessionNote(String name, String value);
Map<String, String> getUserSessionNotes();
void clearUserSessionNotes();
/**
* These are notes you want applied to the UserSessionModel when the client session is attached to it.
*
* @return
*/
public Map<String, String> getUserSessionNotes();
// These are notes used typically by authenticators and authentication flows. They are cleared when authentication session is restarted
String getAuthNote(String name);
void setAuthNote(String name, String value);
void removeAuthNote(String name);
void clearAuthNotes();
public void clearUserSessionNotes();
// These are notes specific to client protocol. They are NOT cleared when authentication session is restarted
String getClientNote(String name);
void setClientNote(String name, String value);
void removeClientNote(String name);
Map<String, String> getClientNotes();
void clearClientNotes();
public String getAuthNote(String name);
public void setAuthNote(String name, String value);
public void removeAuthNote(String name);
public void clearAuthNotes();
void updateClient(ClientModel client);
// Will completely restart whole state of authentication session. It will just keep same ID. It will setup it with provided realm and client.
void restartSession(RealmModel realm, ClientModel client);
}

View file

@ -26,11 +26,10 @@ import org.keycloak.provider.Provider;
*/
public interface AuthenticationSessionProvider extends Provider {
AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client, boolean browser);
// Generates random ID
AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client);
String getCurrentAuthenticationSessionId(RealmModel realm);
AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm);
AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client);
AuthenticationSessionModel getAuthenticationSession(RealmModel realm, String authenticationSessionId);

View file

@ -54,24 +54,11 @@ public interface CommonClientSessionModel {
public Set<String> getProtocolMappers();
public void setProtocolMappers(Set<String> protocolMappers);
public String getNote(String name);
public void setNote(String name, String value);
public void removeNote(String name);
public Map<String, String> getNotes();
public static enum Action {
OAUTH_GRANT,
CODE_TO_TOKEN,
VERIFY_EMAIL,
UPDATE_PROFILE,
CONFIGURE_TOTP,
UPDATE_PASSWORD,
RECOVER_PASSWORD, // deprecated
AUTHENTICATE,
SOCIAL_CALLBACK,
LOGGED_OUT,
RESET_CREDENTIALS,
EXECUTE_ACTIONS,
REQUIRED_ACTIONS
}

View file

@ -44,16 +44,20 @@ import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.ErrorPageException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.PageExpiredRedirect;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.HashMap;
@ -69,6 +73,10 @@ public class AuthenticationProcessor {
public static final String LAST_PROCESSED_EXECUTION = "last.processed.execution";
public static final String CURRENT_FLOW_PATH = "current.flow.path";
public static final String FORKED_FROM = "forked.from";
public static final String FORWARDED_ERROR_MESSAGE_NOTE = "forwardedErrorMessage";
public static final String BROKER_SESSION_ID = "broker.session.id";
public static final String BROKER_USER_ID = "broker.user.id";
protected static final Logger logger = Logger.getLogger(AuthenticationProcessor.class);
protected RealmModel realm;
@ -83,7 +91,7 @@ public class AuthenticationProcessor {
protected String flowPath;
protected boolean browserFlow;
protected BruteForceProtector protector;
protected boolean oneActionWasSuccessful;
protected Runnable afterResetListener;
/**
* This could be an error message forwarded from another authenticator
*/
@ -508,6 +516,12 @@ public class AuthenticationProcessor {
this.status = FlowStatus.FLOW_RESET;
}
@Override
public void resetFlow(Runnable afterResetListener) {
this.status = FlowStatus.FLOW_RESET;
AuthenticationProcessor.this.afterResetListener = afterResetListener;
}
@Override
public void fork() {
this.status = FlowStatus.FORK;
@ -558,7 +572,7 @@ public class AuthenticationProcessor {
protected void logSuccess() {
if (realm.isBruteForceProtected()) {
String username = clientSession.getNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
String username = authenticationSession.getAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME);
// TODO: as above, need to handle non form success
if(username == null) {
@ -608,6 +622,8 @@ public class AuthenticationProcessor {
ForkFlowException reset = (ForkFlowException)e;
AuthenticationSessionModel clone = clone(session, authenticationSession);
clone.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
setAuthenticationSession(clone);
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setAuthenticationSession(clone)
.setFlowPath(LoginActionsService.AUTHENTICATE_PATH)
@ -701,46 +717,40 @@ public class AuthenticationProcessor {
}
public Response redirectToFlow(String execution) {
logger.info("Redirecting to flow with execution: " + execution);
authenticationSession.setAuthNote(LAST_PROCESSED_EXECUTION, execution);
public Response redirectToFlow() {
URI redirect = new PageExpiredRedirect(session, realm, uriInfo).getLastExecutionUrl(authenticationSession);
logger.info("Redirecting to URL: " + redirect.toString());
URI redirect = LoginActionsService.loginActionsBaseUrl(getUriInfo())
.path(flowPath)
.queryParam("execution", execution).build(getRealm().getName());
return Response.status(302).location(redirect).build();
}
public static Response redirectToRequiredActions(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession, UriInfo uriInfo) {
// redirect to non-action url so browser refresh button works without reposting past data
ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authSession);
accessCode.setAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name());
authSession.setTimestamp(Time.currentTime());
URI redirect = LoginActionsService.loginActionsBaseUrl(uriInfo)
.path(LoginActionsService.REQUIRED_ACTION)
.queryParam(OAuth2Constants.CODE, accessCode.getCode()).build(realm.getName());
return Response.status(302).location(redirect).build();
public void resetFlow() {
resetFlow(authenticationSession, flowPath);
if (afterResetListener != null) {
afterResetListener.run();
}
}
public static void resetFlow(AuthenticationSessionModel authSession) {
public static void resetFlow(AuthenticationSessionModel authSession, String flowPath) {
logger.debug("RESET FLOW");
authSession.setTimestamp(Time.currentTime());
authSession.setAuthenticatedUser(null);
authSession.clearExecutionStatus();
authSession.clearUserSessionNotes();
authSession.clearAuthNotes();
authSession.setAuthNote(CURRENT_FLOW_PATH, flowPath);
}
public static AuthenticationSessionModel clone(KeycloakSession session, AuthenticationSessionModel authSession) {
AuthenticationSessionModel clone = session.authenticationSessions().createAuthenticationSession(authSession.getRealm(), authSession.getClient(), true);
AuthenticationSessionModel clone = new AuthenticationSessionManager(session).createAuthenticationSession(authSession.getRealm(), authSession.getClient(), true);
// Transfer just the client "notes", but not "authNotes"
for (Map.Entry<String, String> entry : authSession.getNotes().entrySet()) {
clone.setNote(entry.getKey(), entry.getValue());
for (Map.Entry<String, String> entry : authSession.getClientNotes().entrySet()) {
clone.setClientNote(entry.getKey(), entry.getValue());
}
clone.setRedirectUri(authSession.getRedirectUri());
@ -762,7 +772,7 @@ public class AuthenticationProcessor {
if (!execution.equals(current)) {
// TODO:mposolda debug
logger.info("Current execution does not equal executed execution. Might be a page refresh");
return redirectToFlow(current);
return new PageExpiredRedirect(session, realm, uriInfo).showPageExpired(authenticationSession);
}
UserModel authUser = authenticationSession.getAuthenticatedUser();
validateUser(authUser);
@ -770,7 +780,7 @@ public class AuthenticationProcessor {
if (model == null) {
logger.debug("Cannot find execution, reseting flow");
logFailure();
resetFlow(authenticationSession);
resetFlow();
return authenticate();
}
event.client(authenticationSession.getClient().getClientId())
@ -826,28 +836,6 @@ public class AuthenticationProcessor {
return challenge;
}
/**
* Marks that at least one action was successful
*
*/
public void setActionSuccessful() {
// oneActionWasSuccessful = true;
}
public Response checkWasSuccessfulBrowserAction() {
if (oneActionWasSuccessful && isBrowserFlow()) {
// redirect to non-action url so browser refresh button works without reposting past data
String code = generateCode();
URI redirect = LoginActionsService.loginActionsBaseUrl(getUriInfo())
.path(flowPath)
.queryParam(OAuth2Constants.CODE, code).build(getRealm().getName());
return Response.status(302).location(redirect).build();
} else {
return null;
}
}
// May create userSession too
public AuthenticatedClientSessionModel attachSession() {
AuthenticatedClientSessionModel clientSession = attachSession(authenticationSession, userSession, session, realm, connection, event);
@ -866,10 +854,28 @@ public class AuthenticationProcessor {
if (attemptedUsername != null) username = attemptedUsername;
String rememberMe = authSession.getAuthNote(Details.REMEMBER_ME);
boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("true");
if (userSession == null) { // if no authenticator attached a usersession
userSession = session.sessions().createUserSession(realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol(), remember, null, null);
userSession = session.sessions().getUserSession(realm, authSession.getId());
if (userSession == null) {
String brokerSessionId = authSession.getAuthNote(BROKER_SESSION_ID);
String brokerUserId = authSession.getAuthNote(BROKER_USER_ID);
userSession = session.sessions().createUserSession(authSession.getId(), realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol()
, remember, brokerSessionId, brokerUserId);
} else {
// We have existing userSession even if it wasn't attached to authenticator. Could happen if SSO authentication was ignored (eg. prompt=login) and in some other cases.
// We need to handle case when different user was used and update that (TODO:mposolda evaluate this again and corner cases like token refresh etc. AND ROLES!!! LIKELY ERROR SHOULD BE SHOWN IF ATTEMPT TO AUTHENTICATE AS DIFFERENT USER)
logger.info("No SSO login, but found existing userSession with ID '%s' after finished authentication.");
if (!authSession.getAuthenticatedUser().equals(userSession.getUser())) {
event.detail(Details.EXISTING_USER, userSession.getUser().getId());
event.error(Errors.DIFFERENT_USER_AUTHENTICATED);
throw new ErrorPageException(session, Messages.DIFFERENT_USER_AUTHENTICATED, userSession.getUser().getUsername());
}
}
userSession.setState(UserSessionModel.State.LOGGING_IN);
}
if (remember) {
event.detail(Details.REMEMBER_ME, "true");
}
@ -909,24 +915,19 @@ public class AuthenticationProcessor {
// attachSession(); // Session will be attached after requiredActions + consents are finished.
AuthenticationManager.setRolesAndMappersInSession(authenticationSession);
if (isActionRequired()) {
// TODO:mposolda This was changed to avoid additional redirect. Doublecheck consequences...
//return redirectToRequiredActions(session, realm, authenticationSession, uriInfo);
ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authenticationSession);
accessCode.setAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name());
authenticationSession.setAuthNote(CURRENT_FLOW_PATH, LoginActionsService.REQUIRED_ACTION);
return AuthenticationManager.nextActionAfterAuthentication(session, authenticationSession, connection, request, uriInfo, event);
String nextRequiredAction = nextRequiredAction();
if (nextRequiredAction != null) {
return AuthenticationManager.redirectToRequiredActions(session, realm, authenticationSession, uriInfo, nextRequiredAction);
} else {
event.detail(Details.CODE_ID, authenticationSession.getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set
// the user has successfully logged in and we can clear his/her previous login failure attempts.
logSuccess();
return AuthenticationManager.finishedRequiredActions(session, authenticationSession, connection, request, uriInfo, event);
return AuthenticationManager.finishedRequiredActions(session, authenticationSession, userSession, connection, request, uriInfo, event);
}
}
public boolean isActionRequired() {
return AuthenticationManager.isActionRequired(session, authenticationSession, connection, request, uriInfo, event);
public String nextRequiredAction() {
return AuthenticationManager.nextRequiredAction(session, authenticationSession, connection, request, uriInfo, event);
}
public AuthenticationProcessor.Result createAuthenticatorContext(AuthenticationExecutionModel model, Authenticator authenticator, List<AuthenticationExecutionModel> executions) {

View file

@ -93,10 +93,6 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
Response response = processResult(result, true);
if (response == null) {
processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
if (result.status == FlowStatus.SUCCESS) {
// we do this so that flow can redirect to a non-action URL
processor.setActionSuccessful();
}
return processFlow();
} else return response;
}
@ -183,8 +179,8 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
}
}
// skip if action as successful already
Response redirect = processor.checkWasSuccessfulBrowserAction();
if (redirect != null) return redirect;
// Response redirect = processor.checkWasSuccessfulBrowserAction();
// if (redirect != null) return redirect;
AuthenticationProcessor.Result context = processor.createAuthenticatorContext(model, authenticator, executions);
logger.debug("invoke authenticator.authenticate");
@ -203,13 +199,6 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
case SUCCESS:
logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator());
processor.getAuthenticationSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS);
// We just do another GET to ensure that page refresh will work
if (isAction) {
processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
return processor.redirectToFlow(execution.getId());
}
if (execution.isAlternative()) alternativeSuccessful = true;
return null;
case FAILED:
@ -258,7 +247,7 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
processor.getAuthenticationSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.ATTEMPTED);
return null;
case FLOW_RESET:
AuthenticationProcessor.resetFlow(processor.getAuthenticationSession());
processor.resetFlow();
return processor.authenticate();
default:
logger.debugv("authenticator INTERNAL_ERROR: {0}", execution.getAuthenticator());

View file

@ -244,7 +244,6 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
}
processor.getAuthenticationSession().setExecutionStatus(actionExecution, ClientSessionModel.ExecutionStatus.SUCCESS);
processor.getAuthenticationSession().removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
processor.setActionSuccessful();
return null;
}

View file

@ -48,7 +48,7 @@ public abstract class AbstractIdpAuthenticator implements Authenticator {
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";
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";
@ -56,12 +56,15 @@ public abstract class AbstractIdpAuthenticator implements Authenticator {
// clientSession.note flag specifies if we imported new user to keycloak (true) or we just linked to an existing keycloak user (false)
public static final String BROKER_REGISTERED_NEW_USER = "BROKER_REGISTERED_NEW_USER";
// Set after firstBrokerLogin is successfully finished and contains the providerId of the provider, whose 'first-broker-login' flow was just finished
public static final String FIRST_BROKER_LOGIN_SUCCESS = "FIRST_BROKER_LOGIN_SUCCESS";
@Override
public void authenticate(AuthenticationFlowContext context) {
AuthenticationSessionModel authSession = context.getAuthenticationSession();
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromLoginSession(authSession, BROKERED_CONTEXT_NOTE);
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, BROKERED_CONTEXT_NOTE);
if (serializedCtx == null) {
throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR);
}
@ -78,7 +81,7 @@ public abstract class AbstractIdpAuthenticator implements Authenticator {
public void action(AuthenticationFlowContext context) {
AuthenticationSessionModel clientSession = context.getAuthenticationSession();
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromLoginSession(clientSession, BROKERED_CONTEXT_NOTE);
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(clientSession, BROKERED_CONTEXT_NOTE);
if (serializedCtx == null) {
throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR);
}

View file

@ -20,6 +20,7 @@ package org.keycloak.authentication.authenticators.broker;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.AuthenticationFlowException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.authenticators.broker.util.ExistingUserInfo;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.broker.provider.BrokeredIdentityContext;
@ -65,9 +66,12 @@ public class IdpConfirmLinkAuthenticator extends AbstractIdpAuthenticator {
String action = formData.getFirst("submitAction");
if (action != null && action.equals("updateProfile")) {
context.getAuthenticationSession().setAuthNote(ENFORCE_UPDATE_PROFILE, "true");
context.getAuthenticationSession().removeAuthNote(EXISTING_USER_INFO);
context.resetFlow();
context.resetFlow(() -> {
AuthenticationSessionModel authSession = context.getAuthenticationSession();
serializedCtx.saveToAuthenticationSession(authSession, BROKERED_CONTEXT_NOTE);
authSession.setAuthNote(ENFORCE_UPDATE_PROFILE, "true");
});
} else if (action != null && action.equals("linkAccount")) {
context.success();
} else {

View file

@ -38,6 +38,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
@ -53,16 +54,17 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
@Override
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
/*KeycloakSession session = context.getSession();
KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
ClientSessionModel clientSession = context.getClientSession();
AuthenticationSessionModel authSession = context.getAuthenticationSession();
if (realm.getSmtpConfig().size() == 0) {
// TODO:mposolda (or hmlnarik :) - uncomment and have this working and have AbstractFirstBrokerLoginTest.testLinkAccountByEmailVerification tp PASS
// if (realm.getSmtpConfig().size() == 0) {
ServicesLogger.LOGGER.smtpNotConfigured();
context.attempted();
return;
}
// }
/*
VerifyEmail.setupKey(clientSession);
UserModel existingUser = getExistingUser(session, realm, clientSession);

View file

@ -127,7 +127,7 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator {
AttributeFormDataProcessor.process(formData, realm, userCtx);
userCtx.saveToLoginSession(context.getAuthenticationSession(), BROKERED_CONTEXT_NOTE);
userCtx.saveToAuthenticationSession(context.getAuthenticationSession(), BROKERED_CONTEXT_NOTE);
logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername());

View file

@ -58,7 +58,7 @@ public class IdpUsernamePasswordForm extends UsernamePasswordForm {
}
protected LoginFormsProvider setupForm(AuthenticationFlowContext context, MultivaluedMap<String, String> formData, UserModel existingUser) {
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromLoginSession(context.getAuthenticationSession(), AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(context.getAuthenticationSession(), AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
if (serializedCtx == null) {
throw new AuthenticationFlowException("Not found serialized context in clientSession", AuthenticationFlowError.IDENTITY_PROVIDER_ERROR);
}

View file

@ -313,8 +313,8 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
return ctx;
}
// Save this context as note to clientSession
public void saveToLoginSession(AuthenticationSessionModel authSession, String noteKey) {
// Save this context as note to authSession
public void saveToAuthenticationSession(AuthenticationSessionModel authSession, String noteKey) {
try {
String asString = JsonSerialization.writeValueAsString(this);
authSession.setAuthNote(noteKey, asString);
@ -323,7 +323,7 @@ public class SerializedBrokeredIdentityContext implements UpdateProfileContext {
}
}
public static SerializedBrokeredIdentityContext readFromLoginSession(AuthenticationSessionModel authSession, String noteKey) {
public static SerializedBrokeredIdentityContext readFromAuthenticationSession(AuthenticationSessionModel authSession, String noteKey) {
String asString = authSession.getAuthNote(noteKey);
if (asString == null) {
return null;

View file

@ -51,7 +51,7 @@ public class CookieAuthenticator implements Authenticator {
if (protocol.requireReauthentication(authResult.getSession(), clientSession)) {
context.attempted();
} else {
clientSession.setNote(AuthenticationManager.SSO_AUTH, "true");
clientSession.setClientNote(AuthenticationManager.SSO_AUTH, "true");
context.setUser(authResult.getUser());
context.attachUserSession(authResult.getSession());

View file

@ -59,7 +59,7 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl
@Override
public void authenticate(AuthenticationFlowContext context) {
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<>();
String loginHint = context.getAuthenticationSession().getNote(OIDCLoginProtocol.LOGIN_HINT_PARAM);
String loginHint = context.getAuthenticationSession().getClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM);
String rememberMeUsername = AuthenticationManager.getRememberMeUsername(context.getRealm(), context.getHttpRequest().getHttpHeaders());
@ -72,7 +72,6 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl
}
}
Response challengeResponse = challenge(context, formData);
context.getAuthenticationSession().setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, context.getExecution().getId());
context.challenge(challengeResponse);
}

View file

@ -92,7 +92,7 @@ public class ValidateX509CertificateUsername extends AbstractX509ClientCertifica
UserModel user;
try {
context.getEvent().detail(Details.USERNAME, userIdentity.toString());
context.getAuthenticationSession().setNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, userIdentity.toString());
context.getAuthenticationSession().setAuthNote(AbstractUsernameFormAuthenticator.ATTEMPTED_USERNAME, userIdentity.toString());
user = getUserIdentityToModelMapper(config).find(context, userIdentity);
}
catch(ModelDuplicateException e) {

View file

@ -166,7 +166,6 @@ public class X509ClientCertificateAuthenticator extends AbstractX509ClientCertif
// to call the method "challenge" results in a wrong/unexpected behavior.
// The question is whether calling "forceChallenge" here is ok from
// the design viewpoint?
context.getAuthenticationSession().setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, context.getExecution().getId());
context.forceChallenge(createSuccessResponse(context, certs[0].getSubjectDN().getName()));
// Do not set the flow status yet, we want to display a form to let users
// choose whether to accept the identity from certificate or to specify username/password explicitly

View file

@ -134,7 +134,7 @@ public class RegistrationUserCreation implements FormAction, FormActionFactory {
user.setEnabled(true);
user.setEmail(email);
context.getAuthenticationSession().setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
AttributeFormDataProcessor.process(formData, context.getRealm(), user);
context.setUser(user);
context.getEvent().user(user);

View file

@ -62,10 +62,14 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.resources.admin.RealmAuth;
import org.keycloak.sessions.AuthenticationSessionModel;
@ -229,11 +233,14 @@ public class PolicyEvaluationService {
if (clientId != null) {
ClientModel clientModel = realm.getClientById(clientId);
AuthenticationSessionModel authSession = keycloakSession.authenticationSessions().createAuthenticationSession(realm, clientModel, false);
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
userSession = keycloakSession.sessions().createUserSession(realm, userModel, userModel.getUsername(), "127.0.0.1", "passwd", false, null, null);
String id = KeycloakModelUtils.generateId();
clientSession = new TokenManager().attachAuthenticationSession(keycloakSession, userSession, authSession);
AuthenticationSessionModel authSession = keycloakSession.authenticationSessions().createAuthenticationSession(id, realm, clientModel);
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
userSession = keycloakSession.sessions().createUserSession(id, realm, userModel, userModel.getUsername(), "127.0.0.1", "passwd", false, null, null);
AuthenticationManager.setRolesAndMappersInSession(authSession);
clientSession = TokenManager.attachAuthenticationSession(keycloakSession, userSession, authSession);
Set<RoleModel> requestedRoles = new HashSet<>();
for (String roleId : clientSession.getRoles()) {

View file

@ -37,6 +37,7 @@ import org.keycloak.services.messages.Messages;
import javax.ws.rs.GET;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
@ -240,6 +241,8 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
return callback.authenticated(federatedIdentity);
}
} catch (WebApplicationException e) {
return e.getResponse();
} catch (Exception e) {
logger.error("Failed to make identity provider oauth callback", e);
}

View file

@ -44,6 +44,7 @@ import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.IdentityBrokerService;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.JsonSerialization;
import javax.ws.rs.GET;
@ -350,14 +351,14 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
@Override
public void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context) {
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
AccessTokenResponse tokenResponse = (AccessTokenResponse)context.getContextData().get(FEDERATED_ACCESS_TOKEN_RESPONSE);
int currentTime = Time.currentTime();
long expiration = tokenResponse.getExpiresIn() > 0 ? tokenResponse.getExpiresIn() + currentTime : 0;
userSession.setNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(expiration));
userSession.setNote(FEDERATED_REFRESH_TOKEN, tokenResponse.getRefreshToken());
userSession.setNote(FEDERATED_ACCESS_TOKEN, tokenResponse.getToken());
userSession.setNote(FEDERATED_ID_TOKEN, tokenResponse.getIdToken());
authSession.setUserSessionNote(FEDERATED_TOKEN_EXPIRATION, Long.toString(expiration));
authSession.setUserSessionNote(FEDERATED_REFRESH_TOKEN, tokenResponse.getRefreshToken());
authSession.setUserSessionNote(FEDERATED_ACCESS_TOKEN, tokenResponse.getToken());
authSession.setUserSessionNote(FEDERATED_ID_TOKEN, tokenResponse.getIdToken());
}
@Override

View file

@ -71,6 +71,7 @@ import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.PathParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
@ -436,7 +437,8 @@ public class SAMLEndpoint {
return callback.authenticated(identity);
} catch (WebApplicationException e) {
return e.getResponse();
} catch (Exception e) {
throw new IdentityBrokerException("Could not process response from SAML identity provider.", e);
}

View file

@ -56,6 +56,7 @@ import java.util.TreeSet;
import org.keycloak.dom.saml.v2.metadata.KeyTypes;
import org.keycloak.keys.KeyMetadata;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.sessions.AuthenticationSessionModel;
/**
* @author Pedro Igor
@ -132,17 +133,17 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
}
@Override
public void attachUserSession(UserSessionModel userSession, ClientSessionModel clientSession, BrokeredIdentityContext context) {
public void authenticationFinished(AuthenticationSessionModel authSession, BrokeredIdentityContext context) {
ResponseType responseType = (ResponseType)context.getContextData().get(SAMLEndpoint.SAML_LOGIN_RESPONSE);
AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
SubjectType subject = assertion.getSubject();
SubjectType.STSubType subType = subject.getSubType();
NameIDType subjectNameID = (NameIDType) subType.getBaseID();
userSession.setNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT, subjectNameID.getValue());
if (subjectNameID.getFormat() != null) userSession.setNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT_NAMEFORMAT, subjectNameID.getFormat().toString());
authSession.setUserSessionNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT, subjectNameID.getValue());
if (subjectNameID.getFormat() != null) authSession.setUserSessionNote(SAMLEndpoint.SAML_FEDERATED_SUBJECT_NAMEFORMAT, subjectNameID.getFormat().toString());
AuthnStatementType authn = (AuthnStatementType)context.getContextData().get(SAMLEndpoint.SAML_AUTHN_STATEMENT);
if (authn != null && authn.getSessionIndex() != null) {
userSession.setNote(SAMLEndpoint.SAML_FEDERATED_SESSION_INDEX, authn.getSessionIndex());
authSession.setUserSessionNote(SAMLEndpoint.SAML_FEDERATED_SESSION_INDEX, authn.getSessionIndex());
}
}

View file

@ -179,6 +179,17 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
String requestURI = uriInfo.getBaseUri().getPath();
UriBuilder uriBuilder = UriBuilder.fromUri(requestURI);
if (page == LoginFormsPages.OAUTH_GRANT) {
// for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param
uriBuilder.replaceQuery(null);
}
URI baseUri = uriBuilder.build();
if (accessCode != null) {
uriBuilder.queryParam(OAuth2Constants.CODE, accessCode);
}
URI baseUriWithCode = uriBuilder.build();
for (String k : queryParameterMap.keySet()) {
@ -228,11 +239,6 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
attributes.put("messagesPerField", messagesPerField);
if (page == LoginFormsPages.OAUTH_GRANT) {
// for some reason Resteasy 2.3.7 doesn't like query params and form params with the same name and will null out the code form param
uriBuilder.replaceQuery(null);
}
URI baseUri = uriBuilder.build();
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));
if (realm != null && user != null && session != null) {
attributes.put("authenticatorConfigured", new AuthenticatorConfiguredMethod(realm, user, session));
@ -243,7 +249,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo));
attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUriWithCode));
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
@ -340,6 +346,11 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
}
URI baseUri = uriBuilder.build();
if (accessCode != null) {
uriBuilder.queryParam(OAuth2Constants.CODE, accessCode);
}
URI baseUriWithCode = uriBuilder.build();
ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending");
Theme theme;
try {
@ -391,7 +402,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
identityProviders = LoginFormsUtil.filterIdentityProviders(identityProviders, session, realm, attributes, formData);
attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUri, uriInfo));
attributes.put("social", new IdentityProviderBean(realm, session, identityProviders, baseUriWithCode));
attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri));
attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri));

View file

@ -42,7 +42,7 @@ public class IdentityProviderBean {
private RealmModel realm;
private final KeycloakSession session;
public IdentityProviderBean(RealmModel realm, KeycloakSession session, List<IdentityProviderModel> identityProviders, URI baseURI, UriInfo uriInfo) {
public IdentityProviderBean(RealmModel realm, KeycloakSession session, List<IdentityProviderModel> identityProviders, URI baseURI) {
this.realm = realm;
this.session = session;

View file

@ -17,17 +17,25 @@
package org.keycloak.protocol;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.PageExpiredRedirect;
import org.keycloak.sessions.AuthenticationSessionModel;
import javax.ws.rs.core.Context;
@ -42,6 +50,10 @@ import javax.ws.rs.core.UriInfo;
*/
public abstract class AuthorizationEndpointBase {
private static final Logger logger = Logger.getLogger(AuthorizationEndpointBase.class);
public static final String APP_INITIATED_FLOW = "APP_INITIATED_FLOW";
protected RealmModel realm;
protected EventBuilder event;
protected AuthenticationManager authManager;
@ -103,15 +115,13 @@ public abstract class AuthorizationEndpointBase {
// processor.attachSession();
} else {
Response response = protocol.sendError(authSession, Error.PASSIVE_LOGIN_REQUIRED);
session.authenticationSessions().removeAuthenticationSession(realm, authSession);
return response;
}
AuthenticationManager.setRolesAndMappersInSession(authSession);
if (processor.isActionRequired()) {
if (processor.nextRequiredAction() != null) {
Response response = protocol.sendError(authSession, Error.PASSIVE_INTERACTION_REQUIRED);
session.authenticationSessions().removeAuthenticationSession(realm, authSession);
return response;
}
@ -125,7 +135,7 @@ public abstract class AuthorizationEndpointBase {
try {
RestartLoginCookie.setRestartCookie(session, realm, clientConnection, uriInfo, authSession);
if (redirectToAuthentication) {
return processor.redirectToFlow(null);
return processor.redirectToFlow();
}
return processor.authenticate();
} catch (Exception e) {
@ -138,4 +148,115 @@ public abstract class AuthorizationEndpointBase {
return realm.getBrowserFlow();
}
protected AuthorizationEndpointChecks getOrCreateAuthenticationSession(ClientModel client, String requestState) {
AuthenticationSessionManager manager = new AuthenticationSessionManager(session);
String authSessionId = manager.getCurrentAuthenticationSessionId(realm);
AuthenticationSessionModel authSession = authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
if (authSession != null) {
ClientSessionCode<AuthenticationSessionModel> check = new ClientSessionCode<>(session, realm, authSession);
if (!check.isActionActive(ClientSessionCode.ActionType.LOGIN)) {
logger.infof("Authentication session '%s' exists, but is expired. Restart existing authentication session", authSession.getId());
authSession.restartSession(realm, client);
return new AuthorizationEndpointChecks(authSession);
} else if (isNewRequest(authSession, client, requestState)) {
// Check if we have lastProcessedExecution and restart the session just if yes. Otherwise update just client information from the AuthorizationEndpoint request.
// This difference is needed, because of logout from JS applications in multiple browser tabs.
if (hasProcessedExecution(authSession)) {
logger.info("New request from application received, but authentication session already exists. Restart existing authentication session");
authSession.restartSession(realm, client);
} else {
logger.info("New request from application received, but authentication session already exists. Update client information in existing authentication session");
authSession.clearClientNotes(); // update client data
authSession.updateClient(client);
}
return new AuthorizationEndpointChecks(authSession);
} else {
logger.info("Re-sent some previous request to Authorization endpoint. Likely browser 'back' or 'refresh' button.");
// See if we have lastProcessedExecution note. If yes, we are expired. Also if we are in different flow than initial one. Otherwise it is browser refresh of initial username/password form
if (!shouldShowExpirePage(authSession)) {
return new AuthorizationEndpointChecks(authSession);
} else {
CacheControlUtil.noBackButtonCacheControlHeader();
Response response = new PageExpiredRedirect(session, realm, uriInfo)
.showPageExpired(authSession);
return new AuthorizationEndpointChecks(response);
}
}
}
UserSessionModel userSession = authSessionId==null ? null : session.sessions().getUserSession(realm, authSessionId);
if (userSession != null) {
logger.infof("Sent request to authz endpoint. We don't have authentication session with ID '%s' but we have userSession. Will re-create authentication session with same ID", authSessionId);
authSession = session.authenticationSessions().createAuthenticationSession(authSessionId, realm, client);
} else {
authSession = manager.createAuthenticationSession(realm, client, true);
logger.infof("Sent request to authz endpoint. Created new authentication session with ID '%s'", authSession.getId());
}
return new AuthorizationEndpointChecks(authSession);
}
private boolean hasProcessedExecution(AuthenticationSessionModel authSession) {
String lastProcessedExecution = authSession.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION);
return (lastProcessedExecution != null);
}
// See if we have lastProcessedExecution note. If yes, we are expired. Also if we are in different flow than initial one. Otherwise it is browser refresh of initial username/password form
private boolean shouldShowExpirePage(AuthenticationSessionModel authSession) {
if (hasProcessedExecution(authSession)) {
return true;
}
String initialFlow = authSession.getClientNote(APP_INITIATED_FLOW);
if (initialFlow == null) {
initialFlow = LoginActionsService.AUTHENTICATE_PATH;
}
String lastFlow = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
// Check if we transitted between flows (eg. clicking "register" on login screen)
if (!initialFlow.equals(lastFlow)) {
logger.infof("Transition between flows! Current flow: %s, Previous flow: %s", initialFlow, lastFlow);
if (lastFlow == null || LoginActionsService.isFlowTransitionAllowed(initialFlow, lastFlow)) {
authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, initialFlow);
authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
return false;
} else {
return true;
}
}
return false;
}
// Try to see if it is new request from the application, or refresh of some previous request
protected abstract boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String requestState);
protected static class AuthorizationEndpointChecks {
public final AuthenticationSessionModel authSession;
public final Response response;
private AuthorizationEndpointChecks(Response response) {
this.authSession = null;
this.response = response;
}
private AuthorizationEndpointChecks(AuthenticationSessionModel authSession) {
this.authSession = authSession;
this.response = null;
}
}
}

View file

@ -28,6 +28,7 @@ import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.sessions.AuthenticationSessionModel;
@ -38,7 +39,7 @@ import java.util.HashMap;
import java.util.Map;
/**
* This is an an encoded token that is stored as a cookie so that if there is a client timeout, then the client session
* This is an an encoded token that is stored as a cookie so that if there is a client timeout, then the authentication session
* can be restarted.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -47,8 +48,6 @@ import java.util.Map;
public class RestartLoginCookie {
private static final Logger logger = Logger.getLogger(RestartLoginCookie.class);
public static final String KC_RESTART = "KC_RESTART";
@JsonProperty("cs")
protected String authenticationSession;
@JsonProperty("cid")
protected String clientId;
@ -65,14 +64,6 @@ public class RestartLoginCookie {
@JsonProperty("notes")
protected Map<String, String> notes = new HashMap<>();
public String getAuthenticationSession() {
return authenticationSession;
}
public void setAuthenticationSession(String authenticationSession) {
this.authenticationSession = authenticationSession;
}
public Map<String, String> getNotes() {
return notes;
}
@ -128,8 +119,7 @@ public class RestartLoginCookie {
this.clientId = clientSession.getClient().getClientId();
this.authMethod = clientSession.getProtocol();
this.redirectUri = clientSession.getRedirectUri();
this.authenticationSession = clientSession.getId();
for (Map.Entry<String, String> entry : clientSession.getNotes().entrySet()) {
for (Map.Entry<String, String> entry : clientSession.getClientNotes().entrySet()) {
notes.put(entry.getKey(), entry.getValue());
}
}
@ -150,10 +140,6 @@ public class RestartLoginCookie {
public static AuthenticationSessionModel restartSession(KeycloakSession session, RealmModel realm) throws Exception {
return restartSessionByClientSession(session, realm);
}
private static AuthenticationSessionModel restartSessionByClientSession(KeycloakSession session, RealmModel realm) throws Exception {
Cookie cook = session.getContext().getRequestHeaders().getCookies().get(KC_RESTART);
if (cook == null) {
logger.debug("KC_RESTART cookie doesn't exist");
@ -171,12 +157,12 @@ public class RestartLoginCookie {
ClientModel client = realm.getClientByClientId(cookie.getClientId());
if (client == null) return null;
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client, true);
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
authSession.setProtocol(cookie.getAuthMethod());
authSession.setRedirectUri(cookie.getRedirectUri());
authSession.setAction(cookie.getAction());
for (Map.Entry<String, String> entry : cookie.getNotes().entrySet()) {
authSession.setNote(entry.getKey(), entry.getValue());
authSession.setClientNote(entry.getKey(), entry.getValue());
}
return authSession;

View file

@ -36,6 +36,7 @@ import org.keycloak.protocol.oidc.utils.OIDCResponseType;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.sessions.CommonClientSessionModel;
@ -130,9 +131,7 @@ public class OIDCLoginProtocol implements LoginProtocol {
}
private void setupResponseTypeAndMode(CommonClientSessionModel clientSession) {
String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
String responseMode = clientSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
private void setupResponseTypeAndMode(String responseType, String responseMode) {
this.responseType = OIDCResponseType.parse(responseType);
this.responseMode = OIDCResponseMode.parse(responseMode, this.responseType);
this.event.detail(Details.RESPONSE_TYPE, responseType);
@ -171,9 +170,12 @@ public class OIDCLoginProtocol implements LoginProtocol {
@Override
public Response authenticated(UserSessionModel userSession, ClientSessionCode<AuthenticatedClientSessionModel> accessCode) {
AuthenticatedClientSessionModel clientSession = accessCode.getClientSession();
setupResponseTypeAndMode(clientSession);
public Response authenticated(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
ClientSessionCode<AuthenticatedClientSessionModel> accessCode = new ClientSessionCode<>(session, realm, clientSession);
String responseTypeParam = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
String responseModeParam = clientSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
setupResponseTypeAndMode(responseTypeParam, responseModeParam);
String redirect = clientSession.getRedirectUri();
OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode);
@ -230,15 +232,16 @@ public class OIDCLoginProtocol implements LoginProtocol {
@Override
public Response sendError(AuthenticationSessionModel authSession, Error error) {
setupResponseTypeAndMode(authSession);
String responseTypeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
String responseModeParam = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
setupResponseTypeAndMode(responseTypeParam, responseModeParam);
String redirect = authSession.getRedirectUri();
String state = authSession.getNote(OIDCLoginProtocol.STATE_PARAM);
String state = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
OIDCRedirectUriBuilder redirectUri = OIDCRedirectUriBuilder.fromUri(redirect, responseMode).addParam(OAuth2Constants.ERROR, translateError(error));
if (state != null)
redirectUri.addParam(OAuth2Constants.STATE, state);
session.authenticationSessions().removeAuthenticationSession(realm, authSession);
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
return redirectUri.build();
}
@ -296,13 +299,13 @@ public class OIDCLoginProtocol implements LoginProtocol {
}
protected boolean isPromptLogin(AuthenticationSessionModel authSession) {
String prompt = authSession.getNote(OIDCLoginProtocol.PROMPT_PARAM);
String prompt = authSession.getClientNote(OIDCLoginProtocol.PROMPT_PARAM);
return TokenUtil.hasPrompt(prompt, OIDCLoginProtocol.PROMPT_VALUE_LOGIN);
}
protected boolean isAuthTimeExpired(UserSessionModel userSession, AuthenticationSessionModel authSession) {
String authTime = userSession.getNote(AuthenticationManager.AUTH_TIME);
String maxAge = authSession.getNote(OIDCLoginProtocol.MAX_AGE_PARAM);
String maxAge = authSession.getClientNote(OIDCLoginProtocol.MAX_AGE_PARAM);
if (maxAge == null) {
return false;
}

View file

@ -57,6 +57,7 @@ import org.keycloak.representations.IDToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.sessions.AuthenticationSessionModel;
@ -358,14 +359,18 @@ public class TokenManager {
public static AuthenticatedClientSessionModel attachAuthenticationSession(KeycloakSession session, UserSessionModel userSession, AuthenticationSessionModel authSession) {
ClientModel client = authSession.getClient();
AuthenticatedClientSessionModel clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession);
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessions().get(client.getId());
if (clientSession == null) {
clientSession = session.sessions().createClientSession(userSession.getRealm(), client, userSession);
}
clientSession.setRedirectUri(authSession.getRedirectUri());
clientSession.setProtocol(authSession.getProtocol());
clientSession.setRoles(authSession.getRoles());
clientSession.setProtocolMappers(authSession.getProtocolMappers());
Map<String, String> transferredNotes = authSession.getNotes();
Map<String, String> transferredNotes = authSession.getClientNotes();
for (Map.Entry<String, String> entry : transferredNotes.entrySet()) {
clientSession.setNote(entry.getKey(), entry.getValue());
}
@ -378,7 +383,7 @@ public class TokenManager {
clientSession.setTimestamp(Time.currentTime());
// Remove authentication session now
session.authenticationSessions().removeAuthenticationSession(userSession.getRealm(), authSession);
new AuthenticationSessionManager(session).removeAuthenticationSession(userSession.getRealm(), authSession, true);
return clientSession;
}

View file

@ -41,6 +41,7 @@ import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.services.ErrorPageException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.util.CacheControlUtil;
@ -67,7 +68,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
* Prefix used to store additional HTTP GET params from original client request into {@link AuthenticationSessionModel} note to be available later in Authenticators, RequiredActions etc. Prefix is used to
* prevent collisions with internally used notes.
*
* @see AuthenticationSessionModel#getNote(String)
* @see AuthenticationSessionModel#getClientNote(String)
*/
public static final String LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX = "client_request_param_";
@ -126,7 +127,13 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
return errorResponse;
}
createLoginSession();
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, request.getState());
if (checks.response != null) {
return checks.response;
}
authenticationSession = checks.authSession;
updateAuthenticationSession();
// So back button doesn't work
CacheControlUtil.noBackButtonCacheControlHeader();
@ -164,6 +171,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
return this;
}
private void checkSsl() {
if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
event.error(Errors.SSL_REQUIRED);
@ -358,39 +366,57 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
}
}
private void createLoginSession() {
authenticationSession = session.authenticationSessions().createAuthenticationSession(realm, client, true);
@Override
protected boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String stateFromRequest) {
if (stateFromRequest==null) {
return true;
}
// Check if it's different client
if (!clientFromRequest.equals(authSession.getClient())) {
return true;
}
// If state is same, we likely have the refresh of some previous request
String stateFromSession = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
return !stateFromRequest.equals(stateFromSession);
}
private void updateAuthenticationSession() {
authenticationSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
authenticationSession.setRedirectUri(redirectUri);
authenticationSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
authenticationSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, request.getResponseType());
authenticationSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, request.getRedirectUriParam());
authenticationSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, request.getResponseType());
authenticationSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, request.getRedirectUriParam());
authenticationSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
if (request.getState() != null) authenticationSession.setNote(OIDCLoginProtocol.STATE_PARAM, request.getState());
if (request.getNonce() != null) authenticationSession.setNote(OIDCLoginProtocol.NONCE_PARAM, request.getNonce());
if (request.getMaxAge() != null) authenticationSession.setNote(OIDCLoginProtocol.MAX_AGE_PARAM, String.valueOf(request.getMaxAge()));
if (request.getScope() != null) authenticationSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, request.getScope());
if (request.getLoginHint() != null) authenticationSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint());
if (request.getPrompt() != null) authenticationSession.setNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt());
if (request.getIdpHint() != null) authenticationSession.setNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint());
if (request.getResponseMode() != null) authenticationSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
if (request.getState() != null) authenticationSession.setClientNote(OIDCLoginProtocol.STATE_PARAM, request.getState());
if (request.getNonce() != null) authenticationSession.setClientNote(OIDCLoginProtocol.NONCE_PARAM, request.getNonce());
if (request.getMaxAge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.MAX_AGE_PARAM, String.valueOf(request.getMaxAge()));
if (request.getScope() != null) authenticationSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, request.getScope());
if (request.getLoginHint() != null) authenticationSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint());
if (request.getPrompt() != null) authenticationSession.setClientNote(OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt());
if (request.getIdpHint() != null) authenticationSession.setClientNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint());
if (request.getResponseMode() != null) authenticationSession.setClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
// https://tools.ietf.org/html/rfc7636#section-4
if (request.getCodeChallenge() != null) loginSession.setNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
if (request.getCodeChallenge() != null) authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
if (request.getCodeChallengeMethod() != null) {
loginSession.setNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, request.getCodeChallengeMethod());
authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, request.getCodeChallengeMethod());
} else {
loginSession.setNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, OIDCLoginProtocol.PKCE_METHOD_PLAIN);
authenticationSession.setClientNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, OIDCLoginProtocol.PKCE_METHOD_PLAIN);
}
if (request.getAdditionalReqParams() != null) {
for (String paramName : request.getAdditionalReqParams().keySet()) {
authenticationSession.setNote(LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName));
authenticationSession.setClientNote(LOGIN_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName));
}
}
}
private Response buildAuthorizationCodeAuthorizationResponse() {
this.event.event(EventType.LOGIN);
authenticationSession.setAuthNote(Details.AUTH_TYPE, CODE_AUTH_TYPE);
@ -405,6 +431,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
String flowId = flow.getId();
AuthenticationProcessor processor = createProcessor(authenticationSession, flowId, LoginActionsService.REGISTRATION_PATH);
authenticationSession.setClientNote(APP_INITIATED_FLOW, LoginActionsService.REGISTRATION_PATH);
return processor.authenticate();
}
@ -416,6 +443,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
String flowId = flow.getId();
AuthenticationProcessor processor = createProcessor(authenticationSession, flowId, LoginActionsService.RESET_CREDENTIALS_PATH);
authenticationSession.setClientNote(APP_INITIATED_FLOW, LoginActionsService.REGISTRATION_PATH);
return processor.authenticate();
}

View file

@ -47,6 +47,7 @@ import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
@ -416,11 +417,11 @@ public class TokenEndpoint {
}
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client, false);
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, false);
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
authSession.setAction(AuthenticatedClientSessionModel.Action.AUTHENTICATE.name());
authSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
authSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
AuthenticationFlowModel flow = realm.getDirectGrantFlow();
String flowId = flow.getId();
@ -442,6 +443,9 @@ public class TokenEndpoint {
throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Account is not fully set up", Response.Status.BAD_REQUEST);
}
AuthenticationManager.setRolesAndMappersInSession(authSession);
AuthenticatedClientSessionModel clientSession = processor.attachSession();
UserSessionModel userSession = processor.getUserSession();
updateUserSessionFromClientAuth(userSession);
@ -492,14 +496,16 @@ public class TokenEndpoint {
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client, false);
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, false);
authSession.setAuthenticatedUser(clientUser);
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
authSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
authSession.setNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
UserSessionModel userSession = session.sessions().createUserSession(realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
UserSessionModel userSession = session.sessions().createUserSession(authSession.getId(), realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
event.session(userSession);
AuthenticationManager.setRolesAndMappersInSession(authSession);
AuthenticatedClientSessionModel clientSession = TokenManager.attachAuthenticationSession(session, userSession, authSession);
// Notes about client details

View file

@ -57,6 +57,7 @@ import org.keycloak.saml.common.exceptions.ProcessingException;
import org.keycloak.saml.common.util.XmlKeyInfoKeyNameTransformer;
import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.messages.Messages;
@ -177,7 +178,7 @@ public class SamlProtocol implements LoginProtocol {
} else {
SAML2ErrorResponseBuilder builder = new SAML2ErrorResponseBuilder().destination(authSession.getRedirectUri()).issuer(getResponseIssuer(realm)).status(translateErrorToSAMLStatus(error).get());
try {
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(authSession.getNote(GeneralConstants.RELAY_STATE));
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder().relayState(authSession.getClientNote(GeneralConstants.RELAY_STATE));
SamlClient samlClient = new SamlClient(client);
KeyManager keyManager = session.keys();
if (samlClient.requiresRealmSignature()) {
@ -206,8 +207,7 @@ public class SamlProtocol implements LoginProtocol {
}
}
} finally {
RestartLoginCookie.expireRestartCookie(realm, session.getContext().getConnection(), uriInfo);
session.authenticationSessions().removeAuthenticationSession(realm, authSession);
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, true);
}
}
@ -250,10 +250,16 @@ public class SamlProtocol implements LoginProtocol {
return RealmsResource.realmBaseUrl(uriInfo).build(realm.getName()).toString();
}
protected boolean isPostBinding(CommonClientSessionModel authSession) {
protected boolean isPostBinding(AuthenticationSessionModel authSession) {
ClientModel client = authSession.getClient();
SamlClient samlClient = new SamlClient(client);
return SamlProtocol.SAML_POST_BINDING.equals(authSession.getNote(SamlProtocol.SAML_BINDING)) || samlClient.forcePostBinding();
return SamlProtocol.SAML_POST_BINDING.equals(authSession.getClientNote(SamlProtocol.SAML_BINDING)) || samlClient.forcePostBinding();
}
protected boolean isPostBinding(AuthenticatedClientSessionModel clientSession) {
ClientModel client = clientSession.getClient();
SamlClient samlClient = new SamlClient(client);
return SamlProtocol.SAML_POST_BINDING.equals(clientSession.getNote(SamlProtocol.SAML_BINDING)) || samlClient.forcePostBinding();
}
public static boolean isLogoutPostBindingForInitiator(UserSessionModel session) {
@ -286,7 +292,7 @@ public class SamlProtocol implements LoginProtocol {
return (logoutRedirectUrl == null || logoutRedirectUrl.trim().isEmpty());
}
protected String getNameIdFormat(SamlClient samlClient, CommonClientSessionModel clientSession) {
protected String getNameIdFormat(SamlClient samlClient, AuthenticatedClientSessionModel clientSession) {
String nameIdFormat = clientSession.getNote(GeneralConstants.NAMEID_FORMAT);
boolean forceFormat = samlClient.forceNameIDFormat();
@ -353,8 +359,8 @@ public class SamlProtocol implements LoginProtocol {
}
@Override
public Response authenticated(UserSessionModel userSession, ClientSessionCode<AuthenticatedClientSessionModel> accessCode) {
AuthenticatedClientSessionModel clientSession = accessCode.getClientSession();
public Response authenticated(UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
ClientSessionCode<AuthenticatedClientSessionModel> accessCode = new ClientSessionCode<>(session, realm, clientSession);
ClientModel client = clientSession.getClient();
SamlClient samlClient = new SamlClient(client);
String requestID = clientSession.getNote(SAML_REQUEST_ID);

View file

@ -55,6 +55,7 @@ import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.CacheControlUtil;
@ -89,7 +90,7 @@ import org.keycloak.saml.processing.core.util.KeycloakKeySamlExtensionGenerator;
import org.keycloak.sessions.AuthenticationSessionModel;
/**
* Resource class for the oauth/openid connect token service
* Resource class for the saml connect token service
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
@ -101,6 +102,8 @@ public class SamlService extends AuthorizationEndpointBase {
@Context
protected KeycloakSession session;
private String requestRelayState;
public SamlService(RealmModel realm, EventBuilder event) {
super(realm, event);
}
@ -271,13 +274,19 @@ public class SamlService extends AuthorizationEndpointBase {
return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI);
}
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client, true);
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, relayState);
if (checks.response != null) {
return checks.response;
}
AuthenticationSessionModel authSession = checks.authSession;
authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
authSession.setRedirectUri(redirect);
authSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
authSession.setNote(SamlProtocol.SAML_BINDING, bindingType);
authSession.setNote(GeneralConstants.RELAY_STATE, relayState);
authSession.setNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID());
authSession.setClientNote(SamlProtocol.SAML_BINDING, bindingType);
authSession.setClientNote(GeneralConstants.RELAY_STATE, relayState);
authSession.setClientNote(SamlProtocol.SAML_REQUEST_ID, requestAbstractType.getID());
// Handle NameIDPolicy from SP
NameIDPolicyType nameIdPolicy = requestAbstractType.getNameIDPolicy();
@ -286,7 +295,7 @@ public class SamlService extends AuthorizationEndpointBase {
String nameIdFormat = nameIdFormatUri.toString();
// TODO: Handle AllowCreate too, relevant for persistent NameID.
if (isSupportedNameIdFormat(nameIdFormat)) {
authSession.setNote(GeneralConstants.NAMEID_FORMAT, nameIdFormat);
authSession.setClientNote(GeneralConstants.NAMEID_FORMAT, nameIdFormat);
} else {
event.detail(Details.REASON, "unsupported_nameid_format");
event.error(Errors.INVALID_SAML_AUTHN_REQUEST);
@ -302,7 +311,7 @@ public class SamlService extends AuthorizationEndpointBase {
BaseIDAbstractType baseID = subject.getSubType().getBaseID();
if (baseID != null && baseID instanceof NameIDType) {
NameIDType nameID = (NameIDType) baseID;
authSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, nameID.getValue());
authSession.setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, nameID.getValue());
}
}
@ -443,6 +452,16 @@ public class SamlService extends AuthorizationEndpointBase {
return !realm.getSslRequired().isRequired(clientConnection);
}
}
public Response execute(String samlRequest, String samlResponse, String relayState) {
Response response = basicChecks(samlRequest, samlResponse);
if (response != null)
return response;
if (samlRequest != null)
return handleSamlRequest(samlRequest, relayState);
else
return handleSamlResponse(samlResponse, relayState);
}
}
protected class PostBindingProtocol extends BindingProtocol {
@ -467,16 +486,6 @@ public class SamlService extends AuthorizationEndpointBase {
return SamlProtocol.SAML_POST_BINDING;
}
public Response execute(String samlRequest, String samlResponse, String relayState) {
Response response = basicChecks(samlRequest, samlResponse);
if (response != null)
return response;
if (samlRequest != null)
return handleSamlRequest(samlRequest, relayState);
else
return handleSamlResponse(samlResponse, relayState);
}
}
protected class RedirectBindingProtocol extends BindingProtocol {
@ -507,16 +516,6 @@ public class SamlService extends AuthorizationEndpointBase {
return SamlProtocol.SAML_REDIRECT_BINDING;
}
public Response execute(String samlRequest, String samlResponse, String relayState) {
Response response = basicChecks(samlRequest, samlResponse);
if (response != null)
return response;
if (samlRequest != null)
return handleSamlRequest(samlRequest, relayState);
else
return handleSamlResponse(samlResponse, relayState);
}
}
protected Response newBrowserAuthentication(AuthenticationSessionModel authSession, boolean isPassive, boolean redirectToAuthentication) {
@ -616,7 +615,7 @@ public class SamlService extends AuthorizationEndpointBase {
return ErrorPage.error(session, Messages.INVALID_REDIRECT_URI);
}
AuthenticationSessionModel authSession = createLoginSessionForIdpInitiatedSso(this.session, this.realm, client, relayState);
AuthenticationSessionModel authSession = getOrCreateLoginSessionForIdpInitiatedSso(this.session, this.realm, client, relayState);
return newBrowserAuthentication(authSession, false, false);
}
@ -632,7 +631,7 @@ public class SamlService extends AuthorizationEndpointBase {
* @param relayState Optional relay state - free field as per SAML specification
* @return
*/
public static AuthenticationSessionModel createLoginSessionForIdpInitiatedSso(KeycloakSession session, RealmModel realm, ClientModel client, String relayState) {
public AuthenticationSessionModel getOrCreateLoginSessionForIdpInitiatedSso(KeycloakSession session, RealmModel realm, ClientModel client, String relayState) {
String bindingType = SamlProtocol.SAML_POST_BINDING;
if (client.getManagementUrl() == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_POST_ATTRIBUTE) == null && client.getAttribute(SamlProtocol.SAML_ASSERTION_CONSUMER_URL_REDIRECT_ATTRIBUTE) != null) {
bindingType = SamlProtocol.SAML_REDIRECT_BINDING;
@ -648,23 +647,49 @@ public class SamlService extends AuthorizationEndpointBase {
redirect = client.getManagementUrl();
}
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client, true);
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, null);
if (checks.response != null) {
throw new IllegalStateException("Not expected to detect re-sent request for IDP initiated SSO");
}
AuthenticationSessionModel authSession = checks.authSession;
authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
authSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
authSession.setNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING);
authSession.setNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN, "true");
authSession.setClientNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING);
authSession.setClientNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN, "true");
authSession.setRedirectUri(redirect);
if (relayState == null) {
relayState = client.getAttribute(SamlProtocol.SAML_IDP_INITIATED_SSO_RELAY_STATE);
}
if (relayState != null && !relayState.trim().equals("")) {
authSession.setNote(GeneralConstants.RELAY_STATE, relayState);
authSession.setClientNote(GeneralConstants.RELAY_STATE, relayState);
}
return authSession;
}
@Override
protected boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String requestRelayState) {
// No support of browser "refresh" or "back" buttons for SAML IDP initiated SSO. So always treat as new request
String idpInitiated = authSession.getClientNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN);
if (Boolean.parseBoolean(idpInitiated)) {
return true;
}
if (requestRelayState == null) {
return true;
}
// Check if it's different client
if (!clientFromRequest.equals(authSession.getClient())) {
return true;
}
return !requestRelayState.equals(authSession.getClientNote(GeneralConstants.RELAY_STATE));
}
@POST
@NoCache
@Consumes({"application/soap+xml",MediaType.TEXT_XML})

View file

@ -207,8 +207,7 @@ public class Urls {
}
public static URI realmLoginRestartPage(URI baseUri, String realmId) {
return loginActionsBase(baseUri).path(LoginActionsService.class, "authenticate")
.queryParam("restart", "true")
return loginActionsBase(baseUri).path(LoginActionsService.class, "restartSession")
.build(realmId);
}

View file

@ -39,6 +39,7 @@ import org.keycloak.jose.jws.AlgorithmType;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
@ -52,16 +53,19 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.AccessToken;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.IdentityBrokerService;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.services.util.P3PHelper;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
import javax.crypto.SecretKey;
import javax.ws.rs.core.Cookie;
@ -69,6 +73,7 @@ import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.security.PublicKey;
@ -88,6 +93,9 @@ public class AuthenticationManager {
public static final String SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS= "SET_REDIRECT_URI_AFTER_REQUIRED_ACTIONS";
public static final String END_AFTER_REQUIRED_ACTIONS = "END_AFTER_REQUIRED_ACTIONS";
// Last authenticated client in userSession.
public static final String LAST_AUTHENTICATED_CLIENT = "LAST_AUTHENTICATED_CLIENT";
// 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
@ -470,7 +478,9 @@ public class AuthenticationManager {
userSession.setNote(AUTH_TIME, String.valueOf(authTime));
}
return protocol.authenticated(userSession, new ClientSessionCode<>(session, realm, clientSession));
userSession.setNote(LAST_AUTHENTICATED_CLIENT, clientSession.getClient().getId());
return protocol.authenticated(userSession, clientSession);
}
@ -485,11 +495,32 @@ public class AuthenticationManager {
HttpRequest request, UriInfo uriInfo, EventBuilder event) {
Response requiredAction = actionRequired(session, authSession, clientConnection, request, uriInfo, event);
if (requiredAction != null) return requiredAction;
return finishedRequiredActions(session, authSession, clientConnection, request, uriInfo, event);
return finishedRequiredActions(session, authSession, null, clientConnection, request, uriInfo, event);
}
public static Response finishedRequiredActions(KeycloakSession session, AuthenticationSessionModel authSession,
public static Response redirectToRequiredActions(KeycloakSession session, RealmModel realm, AuthenticationSessionModel authSession, UriInfo uriInfo, String requiredAction) {
// redirect to non-action url so browser refresh button works without reposting past data
ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authSession);
accessCode.setAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name());
authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, LoginActionsService.REQUIRED_ACTION);
authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, requiredAction);
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo)
.path(LoginActionsService.REQUIRED_ACTION);
if (requiredAction != null) {
uriBuilder.queryParam("execution", requiredAction);
}
URI redirect = uriBuilder.build(realm.getName());
return Response.status(302).location(redirect).build();
}
public static Response finishedRequiredActions(KeycloakSession session, AuthenticationSessionModel authSession, UserSessionModel userSession,
ClientConnection clientConnection, HttpRequest request, UriInfo uriInfo, EventBuilder event) {
if (authSession.getAuthNote(END_AFTER_REQUIRED_ACTIONS) != null) {
LoginFormsProvider infoPage = session.getProvider(LoginFormsProvider.class)
@ -506,10 +537,12 @@ public class AuthenticationManager {
.createInfoPage();
return response;
// TODO:mposolda doublecheck if restart-cookie and authentication session are cleared in this flow
}
RealmModel realm = authSession.getRealm();
AuthenticatedClientSessionModel clientSession = AuthenticationProcessor.attachSession(authSession, null, session, realm, clientConnection, event);
AuthenticatedClientSessionModel clientSession = AuthenticationProcessor.attachSession(authSession, userSession, session, realm, clientConnection, event);
event.event(EventType.LOGIN);
event.session(clientSession.getUserSession());
@ -517,7 +550,8 @@ public class AuthenticationManager {
return redirectAfterSuccessfulFlow(session, realm, clientSession.getUserSession(), clientSession, request, uriInfo, clientConnection, event, authSession.getProtocol());
}
public static boolean isActionRequired(final KeycloakSession session, final AuthenticationSessionModel authSession,
// Return null if action is not required. Or the name of the requiredAction in case it is required.
public static String nextRequiredAction(final KeycloakSession session, final AuthenticationSessionModel authSession,
final ClientConnection clientConnection,
final HttpRequest request, final UriInfo uriInfo, final EventBuilder event) {
final RealmModel realm = authSession.getRealm();
@ -526,7 +560,12 @@ public class AuthenticationManager {
evaluateRequiredActionTriggers(session, authSession, clientConnection, request, uriInfo, event, realm, user);
if (!user.getRequiredActions().isEmpty() || !authSession.getRequiredActions().isEmpty()) return true;
if (!user.getRequiredActions().isEmpty()) {
return user.getRequiredActions().iterator().next();
}
if (!authSession.getRequiredActions().isEmpty()) {
return authSession.getRequiredActions().iterator().next();
}
if (client.isConsentRequired()) {
@ -539,13 +578,13 @@ public class AuthenticationManager {
if (grantedConsent != null && grantedConsent.isRoleGranted(r)) {
continue;
}
return true;
return CommonClientSessionModel.Action.OAUTH_GRANT.name();
}
for (ProtocolMapperModel protocolMapper : accessCode.getRequestedProtocolMappers()) {
if (protocolMapper.isConsentRequired() && protocolMapper.getConsentText() != null) {
if (grantedConsent == null || !grantedConsent.isProtocolMapperGranted(protocolMapper)) {
return true;
return CommonClientSessionModel.Action.OAUTH_GRANT.name();
}
}
}
@ -554,7 +593,7 @@ public class AuthenticationManager {
} else {
event.detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED);
}
return false;
return null;
}
@ -617,7 +656,7 @@ public class AuthenticationManager {
accessCode.
setAction(AuthenticatedClientSessionModel.Action.REQUIRED_ACTIONS.name());
authSession.setAuthNote(CURRENT_REQUIRED_ACTION, AuthenticatedClientSessionModel.Action.OAUTH_GRANT.name());
authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, AuthenticatedClientSessionModel.Action.OAUTH_GRANT.name());
return session.getProvider(LoginFormsProvider.class)
.setClientSessionCode(accessCode.getCode())
@ -641,7 +680,7 @@ public class AuthenticationManager {
Set<String> requestedRoles = new HashSet<String>();
// todo scope param protocol independent
String scopeParam = authSession.getNote(OAuth2Constants.SCOPE);
String scopeParam = authSession.getClientNote(OAuth2Constants.SCOPE);
for (RoleModel r : TokenManager.getAccess(scopeParam, true, client, user)) {
requestedRoles.add(r.getId());
}
@ -697,7 +736,7 @@ public class AuthenticationManager {
return response;
}
else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
authSession.setAuthNote(CURRENT_REQUIRED_ACTION, model.getProviderId());
authSession.setAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, model.getProviderId());
return context.getChallenge();
}
else if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {

View file

@ -0,0 +1,115 @@
/*
* 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.services.managers;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.keycloak.common.ClientConnection;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.RestartLoginCookie;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.sessions.AuthenticationSessionModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class AuthenticationSessionManager {
private static final String AUTH_SESSION_ID = "AUTH_SESSION_ID";
private static final Logger log = Logger.getLogger(AuthenticationSessionManager.class);
private final KeycloakSession session;
public AuthenticationSessionManager(KeycloakSession session) {
this.session = session;
}
public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client, boolean browserCookie) {
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client);
if (browserCookie) {
setAuthSessionCookie(authSession.getId(), realm);
}
return authSession;
}
public String getCurrentAuthenticationSessionId(RealmModel realm) {
return getAuthSessionCookie();
}
public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm) {
String authSessionId = getAuthSessionCookie();
return authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
}
public void setAuthSessionCookie(String authSessionId, RealmModel realm) {
UriInfo uriInfo = session.getContext().getUri();
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);
// TODO trace with isTraceEnabled
log.infof("Set AUTH_SESSION_ID cookie with value %s", authSessionId);
}
public String getAuthSessionCookie() {
String cookieVal = CookieHelper.getCookieValue(AUTH_SESSION_ID);
if (log.isTraceEnabled()) {
if (cookieVal != null) {
log.tracef("Found AUTH_SESSION_ID cookie with value %s", cookieVal);
} else {
log.tracef("Not found AUTH_SESSION_ID cookie");
}
}
return cookieVal;
}
public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authSession, boolean expireRestartCookie) {
log.infof("Removing authSession '%s'. Expire restart cookie: %b", authSession.getId(), expireRestartCookie);
session.authenticationSessions().removeAuthenticationSession(realm, authSession);
// expire restart cookie
if (expireRestartCookie) {
ClientConnection clientConnection = session.getContext().getConnection();
UriInfo uriInfo = session.getContext().getUri();
RestartLoginCookie.expireRestartCookie(realm, clientConnection, uriInfo);
}
}
// Check to see if we already have authenticationSession with same ID
public UserSessionModel getUserSession(AuthenticationSessionModel authSession) {
return session.sessions().getUserSession(authSession.getRealm(), authSession.getId());
}
}

View file

@ -27,7 +27,6 @@ import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.OAuth2Constants;
import org.keycloak.sessions.CommonClientSessionModel;
import java.security.MessageDigest;
@ -44,8 +43,6 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
private static final Logger logger = Logger.getLogger(ClientSessionCode.class);
private static final String NEXT_CODE = ClientSessionCode.class.getName() + ".nextCode";
private KeycloakSession session;
private final RealmModel realm;
private final CLIENT_SESSION commonLoginSession;
@ -112,7 +109,8 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
}
public static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, Class<CLIENT_SESSION> sessionClass) {
CLIENT_SESSION clientSession = CodeGenerateUtil.parseSession(code, session, realm, sessionClass);
CommonClientSessionModel clientSessionn = CodeGenerateUtil.getParser(sessionClass).parseSession(code, session, realm);;
CLIENT_SESSION clientSession = sessionClass.cast(clientSessionn);
// TODO:mposolda Move this to somewhere else? Maybe LoginActionsService.sessionCodeChecks should be somehow even for non-action URLs...
if (clientSession != null) {
@ -164,7 +162,8 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
}
public void removeExpiredClientSession() {
CodeGenerateUtil.removeExpiredSession(session, commonLoginSession);
CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(commonLoginSession.getClass());
parser.removeExpiredSession(session, commonLoginSession);
}
@ -206,12 +205,12 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
}
public String getCode() {
String nextCode = (String) session.getAttribute(NEXT_CODE + "." + commonLoginSession.getId());
CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(commonLoginSession.getClass());
String nextCode = parser.getNote(commonLoginSession, ACTIVE_CODE);
if (nextCode == null) {
nextCode = generateCode(commonLoginSession);
session.setAttribute(NEXT_CODE + "." + commonLoginSession.getId(), nextCode);
} else {
logger.debug("Code already generated for session, using code from session attributes");
logger.debug("Code already generated for session, using same code");
}
return nextCode;
}
@ -225,22 +224,10 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
sb.append('.');
sb.append(authSession.getId());
// https://tools.ietf.org/html/rfc7636#section-4
String codeChallenge = authSession.getNote(OAuth2Constants.CODE_CHALLENGE);
String codeChallengeMethod = authSession.getNote(OAuth2Constants.CODE_CHALLENGE_METHOD);
if (codeChallenge != null) {
logger.debugf("PKCE received codeChallenge = %s", codeChallenge);
if (codeChallengeMethod == null) {
logger.debug("PKCE not received codeChallengeMethod, treating plain");
codeChallengeMethod = OAuth2Constants.PKCE_METHOD_PLAIN;
} else {
logger.debugf("PKCE received codeChallengeMethod = %s", codeChallengeMethod);
}
}
CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(authSession.getClass());
String code = CodeGenerateUtil.generateCode(authSession, actionId);
authSession.setNote(ACTIVE_CODE, code);
String code = parser.generateCode(authSession, actionId);
parser.setNote(authSession, ACTIVE_CODE, code);
return code;
} catch (Exception e) {
@ -250,13 +237,15 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
public static boolean verifyCode(String code, CommonClientSessionModel authSession) {
try {
String activeCode = authSession.getNote(ACTIVE_CODE);
CodeGenerateUtil.ClientSessionParser parser = CodeGenerateUtil.getParser(authSession.getClass());
String activeCode = parser.getNote(authSession, ACTIVE_CODE);
if (activeCode == null) {
logger.debug("Active code not found in client session");
return false;
}
authSession.removeNote(ACTIVE_CODE);
parser.removeNote(authSession, ACTIVE_CODE);
return MessageDigest.isEqual(code.getBytes(), activeCode.getBytes());
} catch (Exception e) {

View file

@ -20,6 +20,8 @@ package org.keycloak.services.managers;
import java.util.HashMap;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.OAuth2Constants;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
@ -34,7 +36,9 @@ import org.keycloak.sessions.AuthenticationSessionModel;
*/
class CodeGenerateUtil {
private static final Map<Class<? extends CommonClientSessionModel>, ClientSessionParser<?>> PARSERS = new HashMap<>();
private static final Logger logger = Logger.getLogger(CodeGenerateUtil.class);
private static final Map<Class<? extends CommonClientSessionModel>, ClientSessionParser> PARSERS = new HashMap<>();
static {
PARSERS.put(ClientSessionModel.class, new ClientSessionModelParser());
@ -43,26 +47,10 @@ class CodeGenerateUtil {
}
public static <CS extends CommonClientSessionModel> CS parseSession(String code, KeycloakSession session, RealmModel realm, Class<CS> expectedClazz) {
ClientSessionParser<?> parser = PARSERS.get(expectedClazz);
CommonClientSessionModel result = parser.parseSession(code, session, realm);
return expectedClazz.cast(result);
}
public static String generateCode(CommonClientSessionModel clientSession, String actionId) {
ClientSessionParser parser = getParser(clientSession);
return parser.generateCode(clientSession, actionId);
}
public static void removeExpiredSession(KeycloakSession session, CommonClientSessionModel clientSession) {
ClientSessionParser parser = getParser(clientSession);
parser.removeExpiredSession(session, clientSession);
}
private static ClientSessionParser<?> getParser(CommonClientSessionModel clientSession) {
static <CS extends CommonClientSessionModel> ClientSessionParser<CS> getParser(Class<CS> clientSessionClass) {
for (Class<?> c : PARSERS.keySet()) {
if (c.isAssignableFrom(clientSession.getClass())) {
if (c.isAssignableFrom(clientSessionClass)) {
return PARSERS.get(c);
}
}
@ -70,7 +58,7 @@ class CodeGenerateUtil {
}
private interface ClientSessionParser<CS extends CommonClientSessionModel> {
interface ClientSessionParser<CS extends CommonClientSessionModel> {
CS parseSession(String code, KeycloakSession session, RealmModel realm);
@ -78,6 +66,12 @@ class CodeGenerateUtil {
void removeExpiredSession(KeycloakSession session, CS clientSession);
String getNote(CS clientSession, String name);
void removeNote(CS clientSession, String name);
void setNote(CS clientSession, String name, String value);
}
@ -114,6 +108,21 @@ class CodeGenerateUtil {
public void removeExpiredSession(KeycloakSession session, ClientSessionModel clientSession) {
session.sessions().removeClientSession(clientSession.getRealm(), clientSession);
}
@Override
public String getNote(ClientSessionModel clientSession, String name) {
return clientSession.getNote(name);
}
@Override
public void removeNote(ClientSessionModel clientSession, String name) {
clientSession.removeNote(name);
}
@Override
public void setNote(ClientSessionModel clientSession, String name, String value) {
clientSession.setNote(name, value);
}
}
@ -122,7 +131,7 @@ class CodeGenerateUtil {
@Override
public AuthenticationSessionModel parseSession(String code, KeycloakSession session, RealmModel realm) {
// Read authSessionID from cookie. Code is ignored for now
return session.authenticationSessions().getCurrentAuthenticationSession(realm);
return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm);
}
@Override
@ -132,7 +141,22 @@ class CodeGenerateUtil {
@Override
public void removeExpiredSession(KeycloakSession session, AuthenticationSessionModel clientSession) {
session.authenticationSessions().removeAuthenticationSession(clientSession.getRealm(), clientSession);
new AuthenticationSessionManager(session).removeAuthenticationSession(clientSession.getRealm(), clientSession, true);
}
@Override
public String getNote(AuthenticationSessionModel clientSession, String name) {
return clientSession.getAuthNote(name);
}
@Override
public void removeNote(AuthenticationSessionModel clientSession, String name) {
clientSession.removeAuthNote(name);
}
@Override
public void setNote(AuthenticationSessionModel clientSession, String name, String value) {
clientSession.setAuthNote(name, value);
}
}
@ -168,6 +192,21 @@ class CodeGenerateUtil {
sb.append(userSessionId);
sb.append('.');
sb.append(clientUUID);
// TODO:mposolda codeChallengeMethod is not used anywhere. Not sure if it's bug of PKCE contribution. Doublecheck the PKCE specification what should be done regarding code
// https://tools.ietf.org/html/rfc7636#section-4
String codeChallenge = clientSession.getNote(OAuth2Constants.CODE_CHALLENGE);
String codeChallengeMethod = clientSession.getNote(OAuth2Constants.CODE_CHALLENGE_METHOD);
if (codeChallenge != null) {
logger.debugf("PKCE received codeChallenge = %s", codeChallenge);
if (codeChallengeMethod == null) {
logger.debug("PKCE not received codeChallengeMethod, treating plain");
codeChallengeMethod = OAuth2Constants.PKCE_METHOD_PLAIN;
} else {
logger.debugf("PKCE received codeChallengeMethod = %s", codeChallengeMethod);
}
}
return sb.toString();
}
@ -176,6 +215,20 @@ class CodeGenerateUtil {
throw new IllegalStateException("Not yet implemented");
}
@Override
public String getNote(AuthenticatedClientSessionModel clientSession, String name) {
return clientSession.getNote(name);
}
@Override
public void removeNote(AuthenticatedClientSessionModel clientSession, String name) {
clientSession.removeNote(name);
}
@Override
public void setNote(AuthenticatedClientSessionModel clientSession, String name, String value) {
clientSession.setNote(name, value);
}
}

View file

@ -33,6 +33,8 @@ public class Messages {
public static final String EXPIRED_CODE = "expiredCodeMessage";
public static final String EXPIRED_ACTION = "expiredActionMessage";
public static final String MISSING_FIRST_NAME = "missingFirstNameMessage";
public static final String MISSING_LAST_NAME = "missingLastNameMessage";
@ -197,5 +199,10 @@ public class Messages {
public static final String FAILED_LOGOUT = "failedLogout";
public static final String CONSENT_DENIED="consentDenied";
public static final String ALREADY_LOGGED_IN="alreadyLoggedIn";
public static final String DIFFERENT_USER_AUTHENTICATED = "differentUserAuthenticated";
public static final String BROKER_LINKING_SESSION_EXPIRED = "brokerLinkingSessionExpired";
}

View file

@ -56,6 +56,7 @@ import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.services.validation.Validation;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.util.JsonSerialization;
@ -205,16 +206,20 @@ public class AccountService extends AbstractSecuredLocalService {
setReferrerOnPage();
String forwardedError = auth.getClientSession().getNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
UserSessionModel userSession = auth.getClientSession().getUserSession();
AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, userSession.getId());
if (authSession != null) {
String forwardedError = authSession.getAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
if (forwardedError != null) {
try {
FormMessage errorMessage = JsonSerialization.readValue(forwardedError, FormMessage.class);
account.setError(errorMessage.getMessage(), errorMessage.getParameters());
auth.getClientSession().removeNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
authSession.removeAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
} catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
}
return account.createResponse(page);
} else {
@ -766,7 +771,7 @@ public class AccountService extends AbstractSecuredLocalService {
try {
String nonce = UUID.randomUUID().toString();
MessageDigest md = MessageDigest.getInstance("SHA-256");
String input = nonce + auth.getSession().getId() + auth.getClientSession().getId() + providerId;
String input = nonce + auth.getSession().getId() + client.getClientId() + providerId;
byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
String hash = Base64Url.encode(check);
URI linkUrl = Urls.identityProviderLinkRequest(this.uriInfo.getBaseUri(), providerId, realm.getName());

View file

@ -29,10 +29,14 @@ import org.keycloak.TokenVerifier.Predicate;
import org.keycloak.TokenVerifier.TokenTypeCheck;
import org.keycloak.authentication.*;
import org.keycloak.authentication.authenticators.broker.AbstractIdpAuthenticator;
import org.keycloak.authentication.authenticators.broker.util.PostBrokerLoginConstants;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.ObjectUtil;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
@ -50,7 +54,9 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.protocol.RestartLoginCookie;
@ -62,11 +68,14 @@ import org.keycloak.services.ErrorPage;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.services.util.PageExpiredRedirect;
import org.keycloak.services.util.BrowserHistoryHelper;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.sessions.CommonClientSessionModel.Action;
import javax.ws.rs.Consumes;
@ -76,7 +85,6 @@ import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
@ -99,7 +107,6 @@ public class LoginActionsService {
private static final Logger logger = Logger.getLogger(LoginActionsService.class);
public static final String ACTION_COOKIE = "KEYCLOAK_ACTION";
public static final String AUTHENTICATE_PATH = "authenticate";
public static final String REGISTRATION_PATH = "registration";
public static final String RESET_CREDENTIALS_PATH = "reset-credentials";
@ -107,6 +114,10 @@ public class LoginActionsService {
public static final String FIRST_BROKER_LOGIN_PATH = "first-broker-login";
public static final String POST_BROKER_LOGIN_PATH = "post-broker-login";
public static final String RESTART_PATH = "restart";
public static final String FORWARDED_ERROR_MESSAGE_NOTE = "forwardedErrorMessage";
private RealmModel realm;
@Context
@ -172,9 +183,9 @@ public class LoginActionsService {
}
}
private SessionCodeChecks checksForCode(String code, String execution, String flowPath, boolean wantsRestartSession) {
SessionCodeChecks res = new SessionCodeChecks(code, execution, flowPath, wantsRestartSession);
res.initialVerifyCode();
private SessionCodeChecks checksForCode(String code, String execution, String flowPath) {
SessionCodeChecks res = new SessionCodeChecks(code, execution, flowPath);
res.initialVerify();
return res;
}
@ -189,13 +200,11 @@ public class LoginActionsService {
private final String code;
private final String execution;
private final String flowPath;
private final boolean wantsRestartSession;
public SessionCodeChecks(String code, String execution, String flowPath, boolean wantsRestartSession) {
public SessionCodeChecks(String code, String execution, String flowPath) {
this.code = code;
this.execution = execution;
this.flowPath = flowPath;
this.wantsRestartSession = wantsRestartSession;
}
public AuthenticationSessionModel getAuthenticationSession() {
@ -211,81 +220,109 @@ public class LoginActionsService {
}
boolean verifyCode(String requiredAction, ClientSessionCode.ActionType actionType) {
boolean verifyCode(String expectedAction, ClientSessionCode.ActionType actionType) {
if (failed()) {
return false;
}
if (!clientCode.isValidAction(requiredAction)) {
if (!isActionActive(actionType)) {
return false;
}
if (!clientCode.isValidAction(expectedAction)) {
AuthenticationSessionModel authSession = getAuthenticationSession();
if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(authSession.getAction())) {
// TODO:mposolda debug or trace
logger.info("Incorrect flow '%s' . User authenticated already. Trying requiredActions now.");
response = AuthenticationManager.nextActionAfterAuthentication(session, authSession, clientConnection, request, uriInfo, event);
logger.info("Incorrect flow '%s' . User authenticated already. Redirecting to requiredActions now.");
response = redirectToRequiredActions(null);
return false;
} // TODO:mposolda
/*else if (clientSession.getUserSession() != null && clientSession.getUserSession().getState() == UserSessionModel.State.LOGGED_IN) {
response = session.getProvider(LoginFormsProvider.class)
.setSuccess(Messages.ALREADY_LOGGED_IN)
.createInfoPage();
} else {
// TODO:mposolda could this happen? Doublecheck if we use other AuthenticationSession.Action besides AUTHENTICATE and REQUIRED_ACTIONS
logger.errorf("Bad action. Expected action '%s', current action '%s'", expectedAction, authSession.getAction());
response = ErrorPage.error(session, Messages.EXPIRED_CODE);
return false;
}*/
}
}
return isActionActive(actionType);
}
private boolean isValidAction(String requiredAction) {
if (!clientCode.isValidAction(requiredAction)) {
invalidAction();
return false;
}
return true;
}
private void invalidAction() {
event.client(getAuthenticationSession().getClient());
event.error(Errors.INVALID_CODE);
response = ErrorPage.error(session, Messages.INVALID_CODE);
}
private boolean isActionActive(ClientSessionCode.ActionType actionType) {
if (!clientCode.isActionActive(actionType)) {
event.client(getAuthenticationSession().getClient());
event.clone().error(Errors.EXPIRED_CODE);
if (getAuthenticationSession().getAction().equals(ClientSessionModel.Action.AUTHENTICATE.name())) {
AuthenticationSessionModel authSession = getAuthenticationSession();
AuthenticationProcessor.resetFlow(authSession);
AuthenticationProcessor.resetFlow(authSession, AUTHENTICATE_PATH);
response = processAuthentication(false, null, authSession, Messages.LOGIN_TIMEOUT);
return false;
}
response = ErrorPage.error(session, Messages.EXPIRED_CODE);
return false;
}
return true;
}
private boolean initialVerifyCode() {
private AuthenticationSessionModel initialVerifyAuthSession() {
// Basic realm checks
if (!checkSsl()) {
event.error(Errors.SSL_REQUIRED);
response = ErrorPage.error(session, Messages.HTTPS_REQUIRED);
return false;
return null;
}
if (!realm.isEnabled()) {
event.error(Errors.REALM_DISABLED);
response = ErrorPage.error(session, Messages.REALM_NOT_ENABLED);
return null;
}
// authenticationSession retrieve
AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(code, session, realm, AuthenticationSessionModel.class);
if (authSession != null) {
return authSession;
}
// See if we are already authenticated and userSession with same ID exists.
String sessionId = new AuthenticationSessionManager(session).getCurrentAuthenticationSessionId(realm);
if (sessionId != null) {
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
if (userSession != null) {
LoginFormsProvider loginForm = session.getProvider(LoginFormsProvider.class)
.setSuccess(Messages.ALREADY_LOGGED_IN);
ClientModel client = null;
String lastClientUuid = userSession.getNote(AuthenticationManager.LAST_AUTHENTICATED_CLIENT);
if (lastClientUuid != null) {
client = realm.getClientById(lastClientUuid);
}
if (client != null) {
session.getContext().setClient(client);
} else {
loginForm.setAttribute("skipLink", true);
}
response = loginForm.createInfoPage();
return null;
}
}
// Otherwise just try to restart from the cookie
response = restartAuthenticationSessionFromCookie();
return null;
}
private boolean initialVerify() {
// Basic realm checks and authenticationSession retrieve
AuthenticationSessionModel authSession = initialVerifyAuthSession();
if (authSession == null) {
return false;
}
// authenticationSession retrieve and check if we need session restart
AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(code, session, realm, AuthenticationSessionModel.class);
if (authSession == null) {
response = restartAuthenticationSession(false);
return false;
}
if (wantsRestartSession) {
response = restartAuthenticationSession(true);
// Check cached response from previous action request
response = BrowserHistoryHelper.getInstance().loadSavedResponse(session, authSession);
if (response != null) {
return false;
}
@ -309,16 +346,16 @@ public class LoginActionsService {
// Check if it's action or not
if (code == null) {
String lastExecFromSession = authSession.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION);
String lastExecFromSession = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
String lastFlow = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
// Check if we transitted between flows (eg. clicking "register" on login screen)
if (execution==null && !flowPath.equals(lastFlow)) {
logger.infof("Transition between flows! Current flow: %s, Previous flow: %s", flowPath, lastFlow);
if (lastFlow == null || isFlowTransitionAllowed(lastFlow)) {
if (lastFlow == null || isFlowTransitionAllowed(flowPath, lastFlow)) {
authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, flowPath);
authSession.removeAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION);
authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
lastExecFromSession = null;
}
}
@ -329,8 +366,7 @@ public class LoginActionsService {
actionRequest = false;
return true;
} else {
logger.info("Redirecting to page expired page.");
response = showPageExpired(flowPath, authSession);
response = showPageExpired(authSession);
return false;
}
} else {
@ -339,13 +375,15 @@ public class LoginActionsService {
if (clientCode == null) {
// In case that is replayed action, but sent to the same FORM like actual FORM, we just re-render the page
if (ObjectUtil.isEqualOrBothNull(execution, authSession.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION))) {
if (ObjectUtil.isEqualOrBothNull(execution, authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION))) {
String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
URI redirectUri = getLastExecutionUrl(latestFlowPath, execution);
logger.infof("Invalid action code, but execution matches. So just redirecting to %s", redirectUri);
authSession.setAuthNote(FORWARDED_ERROR_MESSAGE_NOTE, Messages.EXPIRED_ACTION);
response = Response.status(Response.Status.FOUND).location(redirectUri).build();
} else {
response = showPageExpired(flowPath, authSession);
response = showPageExpired(authSession);
}
return false;
}
@ -357,38 +395,32 @@ public class LoginActionsService {
}
}
private boolean isFlowTransitionAllowed(String lastFlow) {
if (flowPath.equals(AUTHENTICATE_PATH) && (lastFlow.equals(REGISTRATION_PATH) || lastFlow.equals(RESET_CREDENTIALS_PATH))) {
return true;
}
if (flowPath.equals(REGISTRATION_PATH) && (lastFlow.equals(AUTHENTICATE_PATH))) {
return true;
}
if (flowPath.equals(RESET_CREDENTIALS_PATH) && (lastFlow.equals(AUTHENTICATE_PATH))) {
return true;
}
return false;
}
public boolean verifyRequiredAction(String executedAction) {
if (failed()) {
return false;
}
if (!isValidAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name())) return false;
if (!clientCode.isValidAction(ClientSessionModel.Action.REQUIRED_ACTIONS.name())) {
// TODO:mposolda debug or trace
logger.infof("Expected required action, but session action is '%s' . Showing expired page now.", getAuthenticationSession().getAction());
event.client(getAuthenticationSession().getClient());
event.error(Errors.INVALID_CODE);
response = showPageExpired(getAuthenticationSession());
return false;
}
if (!isActionActive(ClientSessionCode.ActionType.USER)) return false;
final AuthenticationSessionModel authSession = getAuthenticationSession();
if (actionRequest) {
String currentRequiredAction = authSession.getAuthNote(AuthenticationManager.CURRENT_REQUIRED_ACTION);
String currentRequiredAction = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
if (executedAction == null || !executedAction.equals(currentRequiredAction)) {
logger.debug("required action doesn't match current required action");
authSession.removeAuthNote(AuthenticationManager.CURRENT_REQUIRED_ACTION);
response = redirectToRequiredActions(currentRequiredAction, authSession);
response = redirectToRequiredActions(currentRequiredAction);
return false;
}
}
@ -397,8 +429,33 @@ public class LoginActionsService {
}
protected Response restartAuthenticationSession(boolean managedRestart) {
logger.infof("Login restart requested or authentication session not found. Trying to restart from cookie. Managed restart: %s", managedRestart);
public static boolean isFlowTransitionAllowed(String currentFlow, String previousFlow) {
if (currentFlow.equals(AUTHENTICATE_PATH) && (previousFlow.equals(REGISTRATION_PATH) || previousFlow.equals(RESET_CREDENTIALS_PATH))) {
return true;
}
if (currentFlow.equals(REGISTRATION_PATH) && (previousFlow.equals(AUTHENTICATE_PATH))) {
return true;
}
if (currentFlow.equals(RESET_CREDENTIALS_PATH) && (previousFlow.equals(AUTHENTICATE_PATH) || previousFlow.equals(FIRST_BROKER_LOGIN_PATH))) {
return true;
}
if (currentFlow.equals(FIRST_BROKER_LOGIN_PATH) && (previousFlow.equals(AUTHENTICATE_PATH) || previousFlow.equals(POST_BROKER_LOGIN_PATH))) {
return true;
}
if (currentFlow.equals(POST_BROKER_LOGIN_PATH) && (previousFlow.equals(AUTHENTICATE_PATH) || previousFlow.equals(FIRST_BROKER_LOGIN_PATH))) {
return true;
}
return false;
}
protected Response restartAuthenticationSessionFromCookie() {
logger.info("Authentication session not found. Trying to restart from cookie.");
AuthenticationSessionModel authSession = null;
try {
authSession = RestartLoginCookie.restartSession(session, realm);
@ -409,17 +466,20 @@ public class LoginActionsService {
if (authSession != null) {
event.clone();
String warningMessage = null;
if (managedRestart) {
event.detail(Details.RESTART_REQUESTED, "true");
} else {
event.detail(Details.RESTART_AFTER_TIMEOUT, "true");
warningMessage = Messages.LOGIN_TIMEOUT;
event.error(Errors.EXPIRED_CODE);
String warningMessage = Messages.LOGIN_TIMEOUT;
authSession.setAuthNote(FORWARDED_ERROR_MESSAGE_NOTE, warningMessage);
String flowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW);
if (flowPath == null) {
flowPath = AUTHENTICATE_PATH;
}
event.error(Errors.EXPIRED_CODE);
return processFlow(false, null, authSession, AUTHENTICATE_PATH, realm.getBrowserFlow(), warningMessage, new AuthenticationProcessor());
URI redirectUri = getLastExecutionUrl(flowPath, null);
logger.infof("Authentication session restart from cookie succeeded. Redirecting to %s", redirectUri);
return Response.status(Response.Status.FOUND).location(redirectUri).build();
} else {
event.error(Errors.INVALID_CODE);
return ErrorPage.error(session, Messages.INVALID_CODE);
@ -427,29 +487,50 @@ public class LoginActionsService {
}
protected Response showPageExpired(String flowPath, AuthenticationSessionModel authSession) {
String executionId = authSession==null ? null : authSession.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION);
String latestFlowPath = authSession==null ? flowPath : authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
URI lastStepUrl = getLastExecutionUrl(latestFlowPath, executionId);
logger.infof("Redirecting to 'page expired' now. Will use URL: %s", lastStepUrl);
return session.getProvider(LoginFormsProvider.class)
.setActionUri(lastStepUrl)
.createLoginExpiredPage();
protected Response showPageExpired(AuthenticationSessionModel authSession) {
return new PageExpiredRedirect(session, realm, uriInfo)
.showPageExpired(authSession);
}
protected URI getLastExecutionUrl(String flowPath, String executionId) {
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo)
.path(flowPath);
return new PageExpiredRedirect(session, realm, uriInfo)
.getLastExecutionUrl(flowPath, executionId);
}
if (executionId != null) {
uriBuilder.queryParam("execution", executionId);
/**
* protocol independent page for restart of the flow
*
* @return
*/
@Path(RESTART_PATH)
@GET
public Response restartSession() {
event.event(EventType.RESTART_AUTHENTICATION);
SessionCodeChecks checks = new SessionCodeChecks(null, null, null);
AuthenticationSessionModel authSession = checks.initialVerifyAuthSession();
if (authSession == null) {
return checks.response;
}
return uriBuilder.build(realm.getName());
String flowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW);
if (flowPath == null) {
flowPath = AUTHENTICATE_PATH;
}
AuthenticationProcessor.resetFlow(authSession, flowPath);
// TODO:mposolda Check it's better to put it to AuthenticationProcessor.resetFlow (with consider of brokering)
authSession.setAction(CommonClientSessionModel.Action.AUTHENTICATE.name());
URI redirectUri = getLastExecutionUrl(flowPath, null);
logger.infof("Flow restart requested. Redirecting to %s", redirectUri);
return Response.status(Response.Status.FOUND).location(redirectUri).build();
}
/**
* protocol independent login page entry point
*
@ -459,13 +540,10 @@ public class LoginActionsService {
@Path(AUTHENTICATE_PATH)
@GET
public Response authenticate(@QueryParam("code") String code,
@QueryParam("execution") String execution,
@QueryParam("restart") String restart) {
@QueryParam("execution") String execution) {
event.event(EventType.LOGIN);
boolean wantsSessionRestart = Boolean.parseBoolean(restart);
SessionCodeChecks checks = checksForCode(code, execution, AUTHENTICATE_PATH, wantsSessionRestart);
SessionCodeChecks checks = checksForCode(code, execution, AUTHENTICATE_PATH);
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
@ -491,17 +569,31 @@ public class LoginActionsService {
.setSession(session)
.setUriInfo(uriInfo)
.setRequest(request);
if (errorMessage != null) processor.setForwardedErrorMessage(new FormMessage(null, errorMessage));
if (errorMessage != null) {
processor.setForwardedErrorMessage(new FormMessage(null, errorMessage));
}
// Check the forwarded error message, which was set by previous HTTP request
String forwardedErrorMessage = authSession.getAuthNote(FORWARDED_ERROR_MESSAGE_NOTE);
if (forwardedErrorMessage != null) {
authSession.removeAuthNote(FORWARDED_ERROR_MESSAGE_NOTE);
processor.setForwardedErrorMessage(new FormMessage(null, forwardedErrorMessage));
}
Response response;
try {
if (action) {
return processor.authenticationAction(execution);
response = processor.authenticationAction(execution);
} else {
return processor.authenticate();
response = processor.authenticate();
}
} catch (Exception e) {
return processor.handleBrowserException(e);
response = processor.handleBrowserException(e);
authSession = processor.getAuthenticationSession(); // Could be changed (eg. Forked flow)
}
return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, action);
}
/**
@ -514,7 +606,7 @@ public class LoginActionsService {
@POST
public Response authenticateForm(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return authenticate(code, execution, null);
return authenticate(code, execution);
}
@Path(RESET_CREDENTIALS_PATH)
@ -538,7 +630,6 @@ public class LoginActionsService {
/**
* Verifies that the authentication session has not yet been converted to user session, in other words
* that the user has not yet completed authentication and logged in.
* @param t token
*/
private class IsAuthenticationSessionNotConvertedToUserSession<T extends JsonWebToken> implements Predicate<T> {
@ -587,13 +678,13 @@ public class LoginActionsService {
if (client == null) {
event.error(Errors.CLIENT_NOT_FOUND);
session.authenticationSessions().removeAuthenticationSession(realm, authenticationSession);
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authenticationSession, true);
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER));
}
if (! client.isEnabled()) {
event.error(Errors.CLIENT_NOT_FOUND);
session.authenticationSessions().removeAuthenticationSession(realm, authenticationSession);
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authenticationSession, true);
throw new LoginActionsServiceException(ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED));
}
@ -637,7 +728,7 @@ public class LoginActionsService {
}
if (authSession == null) { // timeout or logged-already (NOPE - this is handled by IsAuthenticationSessionNotConvertedToUserSession)
throw new LoginActionsServiceException(restartAuthenticationSession(false));
throw new LoginActionsServiceException(restartAuthenticationSessionFromCookie());
}
event
@ -653,7 +744,6 @@ public class LoginActionsService {
/**
* This check verifies that if the token has not authentication session set, a new authentication session is introduced
* for the given client and reset-credentials flow is started with this new session.
* @param <T>
*/
private class ResetCredsIntroduceAuthenticationSessionIfNotSet implements Predicate<ResetCredentialsActionToken> {
@ -702,7 +792,7 @@ public class LoginActionsService {
if (authSession != null && ! Objects.equals(authSession.getAction(), this.expectedAction.name())) {
if (ClientSessionModel.Action.REQUIRED_ACTIONS.name().equals(authSession.getAction())) {
throw new LoginActionsServiceException(redirectToRequiredActions(null, authSession));
throw new LoginActionsServiceException(redirectToRequiredActions(null));
}
}
@ -723,12 +813,14 @@ public class LoginActionsService {
public Response resetCredentialsGET(@QueryParam("code") String code,
@QueryParam("execution") String execution,
@QueryParam(Constants.KEY) String key) {
event.event(EventType.RESET_PASSWORD);
if (code != null && key != null) {
// TODO:mposolda better handling of error
throw new IllegalStateException("Illegal state");
}
AuthenticationSessionModel authSession = session.authenticationSessions().getCurrentAuthenticationSession(realm);
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm);
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
if (authSession == null && key == null && code == null) {
@ -755,15 +847,15 @@ public class LoginActionsService {
// set up the account service as the endpoint to call.
ClientModel client = realm.getClientByClientId(clientId);
authSession = session.authenticationSessions().createAuthenticationSession(realm, client, true);
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();
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?
authSession.setRedirectUri(redirectUri);
authSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
authSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
authSession.setNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
authSession.setClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE);
authSession.setClientNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUri);
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
return authSession;
}
@ -776,7 +868,7 @@ public class LoginActionsService {
*/
protected Response resetCredentials(String code, String execution) {
event.event(EventType.RESET_PASSWORD);
SessionCodeChecks checks = checksForCode(code, execution, RESET_CREDENTIALS_PATH, false);
SessionCodeChecks checks = checksForCode(code, execution, RESET_CREDENTIALS_PATH);
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
return checks.response;
}
@ -835,7 +927,7 @@ public class LoginActionsService {
.client(token.getAuthenticationSession().getClient())
.error(Errors.EXPIRED_CODE);
AuthenticationSessionModel authSession = token.getAuthenticationSession();
AuthenticationProcessor.resetFlow(authSession);
AuthenticationProcessor.resetFlow(authSession, AUTHENTICATE_PATH);
return processAuthentication(false, null, authSession, Messages.LOGIN_TIMEOUT);
}
@ -866,8 +958,7 @@ public class LoginActionsService {
if (!isSameBrowser(authSession)) {
logger.debug("Action request processed in different browser.");
// TODO:mposolda improve this. The code should be merged with the InfinispanLoginSessionProvider code and rather extracted from the infinispan provider
setAuthSessionCookie(authSession.getId());
new AuthenticationSessionManager(session).setAuthSessionCookie(authSession.getId(), realm);
authSession.setAuthNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
}
@ -878,7 +969,7 @@ public class LoginActionsService {
// Verify if action is processed in same browser.
private boolean isSameBrowser(AuthenticationSessionModel actionTokenSession) {
String cookieSessionId = session.authenticationSessions().getCurrentAuthenticationSessionId(realm);
String cookieSessionId = new AuthenticationSessionManager(session).getCurrentAuthenticationSessionId(realm);
if (cookieSessionId == null) {
return false;
@ -901,11 +992,12 @@ public class LoginActionsService {
if (actionTokenSession.getId().equals(parentSessionId)) {
// It's the correct browser. Let's remove forked session as we won't continue from the login form (browser flow) but from the resetCredentialsByToken flow
session.authenticationSessions().removeAuthenticationSession(realm, forkedSession);
// Don't expire KC_RESTART cookie at this point
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, forkedSession, false);
logger.infof("Removed forked session: %s", forkedSession.getId());
// Refresh browser cookie
setAuthSessionCookie(parentSessionId);
new AuthenticationSessionManager(session).setAuthSessionCookie(parentSessionId, realm);
return true;
} else {
@ -913,16 +1005,6 @@ public class LoginActionsService {
}
}
// TODO:mposolda improve this. The code should be merged with the InfinispanLoginSessionProvider code and rather extracted from the infinispan provider
private void setAuthSessionCookie(String authSessionId) {
logger.infof("Set browser cookie to %s", authSessionId);
String cookiePath = CookieHelper.getRealmCookiePath(realm);
boolean sslRequired = realm.getSslRequired().isRequired(session.getContext().getConnection());
CookieHelper.addCookie("AUTH_SESSION_ID", authSessionId, cookiePath, null, null, -1, sslRequired, true);
}
protected Response processResetCredentials(boolean actionRequest, String execution, AuthenticationSessionModel authSession, String errorMessage) {
AuthenticationProcessor authProcessor = new AuthenticationProcessor() {
@ -937,11 +1019,13 @@ public class LoginActionsService {
return ErrorPage.error(session, Messages.IDENTITY_PROVIDER_DIFFERENT_USER_MESSAGE, authenticationSession.getAuthenticatedUser().getUsername(), linkingUser.getUsername());
}
logger.debugf("Forget-password flow finished when authenticated user '%s' after first broker login.", linkingUser.getUsername());
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
authSession.setAuthNote(AbstractIdpAuthenticator.FIRST_BROKER_LOGIN_SUCCESS, serializedCtx.getIdentityProviderId());
// TODO:mposolda Isn't this a bug that we redirect to 'afterBrokerLoginEndpoint' without rather continue with firstBrokerLogin and other authenticators like OTP?
//return redirectToAfterBrokerLoginEndpoint(authSession, true);
return null;
logger.debugf("Forget-password flow finished when authenticated user '%s' after first broker login with identity provider '%s'.",
linkingUser.getUsername(), serializedCtx.getIdentityProviderId());
return redirectToAfterBrokerLoginEndpoint(authSession, true);
} else {
return super.authenticationComplete();
}
@ -992,7 +1076,7 @@ public class LoginActionsService {
return ErrorPage.error(session, Messages.REGISTRATION_NOT_ALLOWED);
}
SessionCodeChecks checks = checksForCode(code, execution, REGISTRATION_PATH, false);
SessionCodeChecks checks = checksForCode(code, execution, REGISTRATION_PATH);
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
@ -1008,55 +1092,56 @@ public class LoginActionsService {
return processRegistration(checks.actionRequest, execution, clientSession, null);
}
// TODO:mposolda broker login
/*
@Path(FIRST_BROKER_LOGIN_PATH)
@GET
public Response firstBrokerLoginGet(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return brokerLoginFlow(code, execution, true);
return brokerLoginFlow(code, execution, FIRST_BROKER_LOGIN_PATH);
}
@Path(FIRST_BROKER_LOGIN_PATH)
@POST
public Response firstBrokerLoginPost(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return brokerLoginFlow(code, execution, true);
return brokerLoginFlow(code, execution, FIRST_BROKER_LOGIN_PATH);
}
@Path(POST_BROKER_LOGIN_PATH)
@GET
public Response postBrokerLoginGet(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return brokerLoginFlow(code, execution, false);
return brokerLoginFlow(code, execution, POST_BROKER_LOGIN_PATH);
}
@Path(POST_BROKER_LOGIN_PATH)
@POST
public Response postBrokerLoginPost(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
return brokerLoginFlow(code, execution, false);
return brokerLoginFlow(code, execution, POST_BROKER_LOGIN_PATH);
}
protected Response brokerLoginFlow(String code, String execution, final boolean firstBrokerLogin) {
protected Response brokerLoginFlow(String code, String execution, String flowPath) {
boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH);
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
event.event(eventType);
SessionCodeChecks checks = checksForCode(code);
SessionCodeChecks checks = checksForCode(code, execution, flowPath);
if (!checks.verifyCode(ClientSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
return checks.response;
}
event.detail(Details.CODE_ID, code);
final ClientSessionModel clientSessionn = checks.getClientSession();
final AuthenticationSessionModel authSession = checks.getAuthenticationSession();
String noteKey = firstBrokerLogin ? AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE : PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT;
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromClientSession(clientSessionn, noteKey);
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, noteKey);
if (serializedCtx == null) {
ServicesLogger.LOGGER.notFoundSerializedCtxInClientSession(noteKey);
throw new WebApplicationException(ErrorPage.error(session, "Not found serialized context in clientSession."));
}
BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, clientSessionn);
BrokeredIdentityContext brokerContext = serializedCtx.deserialize(session, authSession);
final String identityProviderAlias = brokerContext.getIdpConfig().getAlias();
String flowId = firstBrokerLogin ? brokerContext.getIdpConfig().getFirstBrokerLoginFlowId() : brokerContext.getIdpConfig().getPostBrokerLoginFlowId();
@ -1078,23 +1163,24 @@ public class LoginActionsService {
@Override
protected Response authenticationComplete() {
if (!firstBrokerLogin) {
if (firstBrokerLogin) {
authSession.setAuthNote(AbstractIdpAuthenticator.FIRST_BROKER_LOGIN_SUCCESS, identityProviderAlias);
} else {
String authStateNoteKey = PostBrokerLoginConstants.PBL_AUTH_STATE_PREFIX + identityProviderAlias;
clientSessionn.setNote(authStateNoteKey, "true");
authSession.setAuthNote(authStateNoteKey, "true");
}
return redirectToAfterBrokerLoginEndpoint(clientSession, firstBrokerLogin);
return redirectToAfterBrokerLoginEndpoint(authSession, firstBrokerLogin);
}
};
String flowPath = firstBrokerLogin ? FIRST_BROKER_LOGIN_PATH : POST_BROKER_LOGIN_PATH;
return processFlow(execution, clientSessionn, flowPath, brokerLoginFlow, null, processor);
return processFlow(checks.actionRequest, execution, authSession, flowPath, brokerLoginFlow, null, processor);
}
private Response redirectToAfterBrokerLoginEndpoint(ClientSessionModel clientSession, boolean firstBrokerLogin) {
ClientSessionCode accessCode = new ClientSessionCode(session, realm, clientSession);
clientSession.setTimestamp(Time.currentTime());
private Response redirectToAfterBrokerLoginEndpoint(AuthenticationSessionModel authSession, boolean firstBrokerLogin) {
ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authSession);
authSession.setTimestamp(Time.currentTime());
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) :
Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getCode()) ;
@ -1102,7 +1188,7 @@ public class LoginActionsService {
return Response.status(302).location(redirect).build();
}
*/
/**
* OAuth grant page. You should not invoked this directly!
@ -1116,7 +1202,7 @@ public class LoginActionsService {
public Response processConsent(final MultivaluedMap<String, String> formData) {
event.event(EventType.LOGIN);
String code = formData.getFirst("code");
SessionCodeChecks checks = checksForCode(code, null, REQUIRED_ACTION, false);
SessionCodeChecks checks = checksForCode(code, null, REQUIRED_ACTION);
if (!checks.verifyRequiredAction(ClientSessionModel.Action.OAUTH_GRANT.name())) {
return checks.response;
}
@ -1159,7 +1245,6 @@ public class LoginActionsService {
event.detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED);
event.success();
// TODO:mposolda So assume that requiredActions were already done in this stage. Doublecheck...
AuthenticatedClientSessionModel clientSession = AuthenticationProcessor.attachSession(authSession, null, session, realm, clientConnection, event);
return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, clientSession.getUserSession(), clientSession, request, uriInfo, clientConnection, event, authSession.getProtocol());
}
@ -1276,27 +1361,12 @@ public class LoginActionsService {
return null;
}
private String getActionCookie() {
return getActionCookie(headers, realm, uriInfo, clientConnection);
}
public static String getActionCookie(HttpHeaders headers, RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection) {
Cookie cookie = headers.getCookies().get(ACTION_COOKIE);
AuthenticationManager.expireCookie(realm, ACTION_COOKIE, AuthenticationManager.getRealmCookiePath(realm, uriInfo), realm.getSslRequired().isRequired(clientConnection), clientConnection);
return cookie != null ? cookie.getValue() : null;
}
// TODO: Remove this method. We will be able to use login-session-cookie
public static void createActionCookie(RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, String sessionId) {
CookieHelper.addCookie(ACTION_COOKIE, sessionId, AuthenticationManager.getRealmCookiePath(realm, uriInfo), null, null, -1, realm.getSslRequired().isRequired(clientConnection), true);
}
private void initLoginEvent(AuthenticationSessionModel authSession) {
String responseType = authSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
String responseType = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
if (responseType == null) {
responseType = "code";
}
String respMode = authSession.getNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
String respMode = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM);
OIDCResponseMode responseMode = OIDCResponseMode.parse(respMode, OIDCResponseType.parse(responseType));
event.event(EventType.LOGIN).client(authSession.getClient())
@ -1345,7 +1415,9 @@ public class LoginActionsService {
}
private Response processRequireAction(final String code, String action) {
SessionCodeChecks checks = checksForCode(code, action, REQUIRED_ACTION, false);
event.event(EventType.CUSTOM_REQUIRED_ACTION);
SessionCodeChecks checks = checksForCode(code, action, REQUIRED_ACTION);
if (!checks.verifyRequiredAction(action)) {
return checks.response;
}
@ -1375,21 +1447,24 @@ public class LoginActionsService {
throw new RuntimeException("Cannot call ignore within processAction()");
}
};
Response response;
provider.processAction(context);
authSession.setAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION, action);
if (context.getStatus() == RequiredActionContext.Status.SUCCESS) {
event.clone().success();
initLoginEvent(authSession);
event.event(EventType.LOGIN);
authSession.removeRequiredAction(factory.getId());
authSession.getAuthenticatedUser().removeRequiredAction(factory.getId());
authSession.removeAuthNote(AuthenticationManager.CURRENT_REQUIRED_ACTION);
authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
return redirectToRequiredActions(action, authSession);
}
if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
return context.getChallenge();
}
if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
response = AuthenticationManager.nextActionAfterAuthentication(session, authSession, clientConnection, request, uriInfo, event);
} else if (context.getStatus() == RequiredActionContext.Status.CHALLENGE) {
response = context.getChallenge();
} else if (context.getStatus() == RequiredActionContext.Status.FAILURE) {
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, authSession.getProtocol());
protocol.setRealm(context.getRealm())
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
@ -1397,18 +1472,16 @@ public class LoginActionsService {
.setEventBuilder(event);
event.detail(Details.CUSTOM_REQUIRED_ACTION, action);
Response response = protocol.sendError(authSession, Error.CONSENT_DENIED);
response = protocol.sendError(authSession, Error.CONSENT_DENIED);
event.error(Errors.REJECTED_BY_USER);
return response;
}
} else {
throw new RuntimeException("Unreachable");
}
private Response redirectToRequiredActions(String action, AuthenticationSessionModel authSession) {
authSession.setAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION, action);
return BrowserHistoryHelper.getInstance().saveResponseAndRedirect(session, authSession, response, true);
}
private Response redirectToRequiredActions(String action) {
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo)
.path(LoginActionsService.REQUIRED_ACTION);

View file

@ -234,16 +234,12 @@ public class RealmsResource {
public IdentityBrokerService getBrokerService(final @PathParam("realm") String name) {
RealmModel realm = init(name);
// TODO:mposolda
/*
IdentityBrokerService brokerService = new IdentityBrokerService(realm);
ResteasyProviderFactory.getInstance().injectProperties(brokerService);
brokerService.init();
return brokerService;
*/
return null;
}
@OPTIONS

View file

@ -45,6 +45,7 @@ import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderFactory;
@ -332,7 +333,8 @@ public class UsersResource {
}
EventBuilder event = new EventBuilder(realm, session, clientConnection);
UserSessionModel userSession = session.sessions().createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null);
String sessionId = KeycloakModelUtils.generateId();
UserSessionModel userSession = session.sessions().createUserSession(sessionId, realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null);
AuthenticationManager.createLoginCookie(session, realm, userSession.getUser(), userSession, uriInfo, clientConnection);
URI redirect = AccountService.accountServiceApplicationPage(uriInfo).build(realm.getName());
Map<String, Object> result = new HashMap<>();

View file

@ -0,0 +1,191 @@
/*
* 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.services.util;
import java.net.URI;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.core.Response;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.theme.BrowserSecurityHeaderSetup;
import org.keycloak.utils.MediaType;
/**
* The point of this is to improve experience of browser history (back/forward/refresh buttons), but ensure there is no more redirects then necessary.
*
* Ideally we want to:
* - Remove all POST requests from browser history, because browsers don't automatically re-send them when click "back" button. POSTS in history causes unfriendly dialogs and browser "Page is expired" pages.
*
* - Keep the browser URL to match the flow and execution from authentication session. This means that browser refresh works fine and show us the correct form.
*
* - Avoid redirects. This is possible with javascript based approach (JavascriptHistoryReplace). The RedirectAfterPostHelper requires one redirect after POST, but works even on browser without javascript and
* on old browsers where "history.replaceState" is unsupported.
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public abstract class BrowserHistoryHelper {
protected static final Logger logger = Logger.getLogger(BrowserHistoryHelper.class);
public abstract Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest);
public abstract Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession);
// Always rely on javascript for now
public static BrowserHistoryHelper getInstance() {
return new JavascriptHistoryReplace();
//return new RedirectAfterPostHelper();
//return new NoOpHelper();
}
// IMPL
private static class JavascriptHistoryReplace extends BrowserHistoryHelper {
private static final Pattern HEAD_END_PATTERN = Pattern.compile("</[hH][eE][aA][dD]>");
@Override
public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) {
if (!actionRequest) {
return response;
}
// 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) {
String responseString = (String) entity;
URI lastExecutionURL = new PageExpiredRedirect(session, session.getContext().getRealm(), session.getContext().getUri()).getLastExecutionUrl(authSession);
// Inject javascript for history "replaceState"
String responseWithJavascript = responseWithJavascript(responseString, lastExecutionURL.toString());
return Response.fromResponse(response).entity(responseWithJavascript).build();
}
}
return response;
}
@Override
public Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession) {
return null;
}
private String responseWithJavascript(String origHtml, String lastExecutionUrl) {
Matcher m = HEAD_END_PATTERN.matcher(origHtml);
if (m.find()) {
int start = m.start();
String javascript = getJavascriptText(lastExecutionUrl);
return new StringBuilder(origHtml.substring(0, start))
.append(javascript )
.append(origHtml.substring(start))
.toString();
} else {
return origHtml;
}
}
private String getJavascriptText(String lastExecutionUrl) {
return new StringBuilder("<SCRIPT>")
.append(" if (typeof history.replaceState === 'function') {")
.append(" history.replaceState({}, \"some title\", \"" + lastExecutionUrl + "\");")
.append(" }")
.append("</SCRIPT>")
.toString();
}
}
private static class RedirectAfterPostHelper extends BrowserHistoryHelper {
private static final String CACHED_RESPONSE = "cached.response";
@Override
public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) {
if (!actionRequest) {
return response;
}
// 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) {
String responseString = (String) entity;
authSession.setAuthNote(CACHED_RESPONSE, responseString);
URI lastExecutionURL = new PageExpiredRedirect(session, session.getContext().getRealm(), session.getContext().getUri()).getLastExecutionUrl(authSession);
// TODO:mposolda trace
logger.infof("Saved response challenge and redirect to %s", lastExecutionURL);
return Response.status(302).location(lastExecutionURL).build();
}
}
return response;
}
@Override
public Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession) {
String savedResponse = authSession.getAuthNote(CACHED_RESPONSE);
if (savedResponse != null) {
authSession.removeAuthNote(CACHED_RESPONSE);
// TODO:mposolda trace
logger.infof("Restored previously saved request");
Response.ResponseBuilder builder = Response.status(200).type(MediaType.TEXT_HTML_UTF_8).entity(savedResponse);
BrowserSecurityHeaderSetup.headers(builder, session.getContext().getRealm()); // TODO:mposolda cRather all the headers from the saved response should be added here.
return builder.build();
}
return null;
}
}
private static class NoOpHelper extends BrowserHistoryHelper {
@Override
public Response saveResponseAndRedirect(KeycloakSession session, AuthenticationSessionModel authSession, Response response, boolean actionRequest) {
return response;
}
@Override
public Response loadSavedResponse(KeycloakSession session, AuthenticationSessionModel authSession) {
return null;
}
}
}

View file

@ -24,6 +24,7 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.util.ServerCookie;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.AuthenticationManager;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.HttpHeaders;
@ -63,13 +64,4 @@ public class CookieHelper {
return cookie != null ? cookie.getValue() : null;
}
public static String getRealmCookiePath(RealmModel realm) {
UriInfo uriInfo = ResteasyProviderFactory.getContextData(UriInfo.class);
UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
URI uri = baseUriBuilder.path("/realms/{realm}").build(realm.getName());
return uri.getRawPath();
}
}

View file

@ -0,0 +1,90 @@
/*
* 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.services.util;
import java.net.URI;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.sessions.AuthenticationSessionModel;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class PageExpiredRedirect {
protected static final Logger logger = Logger.getLogger(PageExpiredRedirect.class);
private final KeycloakSession session;
private final RealmModel realm;
private final UriInfo uriInfo;
public PageExpiredRedirect(KeycloakSession session, RealmModel realm, UriInfo uriInfo) {
this.session = session;
this.realm = realm;
this.uriInfo = uriInfo;
}
public Response showPageExpired(AuthenticationSessionModel authSession) {
URI lastStepUrl = getLastExecutionUrl(authSession);
logger.infof("Redirecting to 'page expired' now. Will use last step URL: %s", lastStepUrl);
return session.getProvider(LoginFormsProvider.class)
.setActionUri(lastStepUrl)
.createLoginExpiredPage();
}
public URI getLastExecutionUrl(String flowPath, String executionId) {
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo)
.path(flowPath);
if (executionId != null) {
uriBuilder.queryParam("execution", executionId);
}
return uriBuilder.build(realm.getName());
}
public URI getLastExecutionUrl(AuthenticationSessionModel authSession) {
String executionId = authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
if (latestFlowPath == null) {
latestFlowPath = authSession.getClientNote(AuthorizationEndpointBase.APP_INITIATED_FLOW);
}
if (latestFlowPath == null) {
latestFlowPath = LoginActionsService.AUTHENTICATE_PATH;
}
return getLastExecutionUrl(latestFlowPath, executionId);
}
}

View file

@ -26,13 +26,13 @@ import org.keycloak.broker.social.SocialIdentityProvider;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.services.ErrorPage;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.sessions.AuthenticationSessionModel;
import twitter4j.Twitter;
import twitter4j.TwitterFactory;
import twitter4j.auth.AccessToken;
@ -40,6 +40,7 @@ import twitter4j.auth.RequestToken;
import javax.ws.rs.GET;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
@ -54,6 +55,10 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
SocialIdentityProvider<OAuth2IdentityProviderConfig> {
protected static final Logger logger = Logger.getLogger(TwitterIdentityProvider.class);
private static final String TWITTER_TOKEN = "twitter_token";
private static final String TWITTER_TOKENSECRET = "twitter_tokenSecret";
public TwitterIdentityProvider(KeycloakSession session, OAuth2IdentityProviderConfig config) {
super(session, config);
}
@ -72,10 +77,10 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
URI uri = new URI(request.getRedirectUri() + "?state=" + request.getState());
RequestToken requestToken = twitter.getOAuthRequestToken(uri.toString());
ClientSessionModel clientSession = request.getClientSession();
AuthenticationSessionModel authSession = request.getAuthenticationSession();
clientSession.setNote("twitter_token", requestToken.getToken());
clientSession.setNote("twitter_tokenSecret", requestToken.getTokenSecret());
authSession.setAuthNote(TWITTER_TOKEN, requestToken.getToken());
authSession.setAuthNote(TWITTER_TOKENSECRET, requestToken.getTokenSecret());
URI authenticationUrl = URI.create(requestToken.getAuthenticationURL());
@ -115,16 +120,14 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
}
try {
// TODO:mposolda
/*
Twitter twitter = new TwitterFactory().getInstance();
twitter.setOAuthConsumer(getConfig().getClientId(), getConfig().getClientSecret());
ClientSessionModel clientSession = ClientSessionCode.getClientSession(state, session, realm);
AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(state, session, realm, AuthenticationSessionModel.class);
String twitterToken = clientSession.getNote("twitter_token");
String twitterSecret = clientSession.getNote("twitter_tokenSecret");
String twitterToken = authSession.getAuthNote(TWITTER_TOKEN);
String twitterSecret = authSession.getAuthNote(TWITTER_TOKENSECRET);
RequestToken requestToken = new RequestToken(twitterToken, twitterSecret);
@ -151,8 +154,8 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
identity.setCode(state);
return callback.authenticated(identity);
*/
return null;
} catch (WebApplicationException e) {
return e.getResponse();
} catch (Exception e) {
logger.error("Could get user profile from twitter.", e);
}

View file

@ -52,7 +52,7 @@ public class PassThroughRegistration implements Authenticator, AuthenticatorFact
user.setEnabled(true);
user.setEmail(email);
context.getAuthenticationSession().setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
context.getAuthenticationSession().setClientNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username);
context.setUser(user);
context.getEvent().user(user);
context.getEvent().success();

View file

@ -163,6 +163,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
}
session.sessions().removeExpired(realm);
session.authenticationSessions().removeExpired(realm);
return Response.ok().build();
}

View file

@ -47,7 +47,7 @@ public class ClientInitiatedAccountLinkServlet extends HttpServlet {
String realm = request.getParameter("realm");
KeycloakSecurityContext session = (KeycloakSecurityContext) request.getAttribute(KeycloakSecurityContext.class.getName());
AccessToken token = session.getToken();
String clientSessionId = token.getClientSession();
String clientId = token.getAudience()[0];
String nonce = UUID.randomUUID().toString();
MessageDigest md = null;
try {
@ -55,7 +55,7 @@ public class ClientInitiatedAccountLinkServlet extends HttpServlet {
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
String input = nonce + token.getSessionState() + clientSessionId + provider;
String input = nonce + token.getSessionState() + clientId + provider;
byte[] check = md.digest(input.getBytes(StandardCharsets.UTF_8));
String hash = Base64Url.encode(check);
request.getSession().setAttribute("hash", hash);

View file

@ -33,6 +33,9 @@ public class InfoPage extends AbstractPage {
@FindBy(className = "instruction")
private WebElement infoMessage;
@FindBy(linkText = "« Back to Application")
private WebElement backToApplicationLink;
public String getInfo() {
return infoMessage.getText();
}
@ -46,4 +49,8 @@ public class InfoPage extends AbstractPage {
throw new UnsupportedOperationException();
}
public void clickBackToApplicationLink() {
backToApplicationLink.click();
}
}

View file

@ -0,0 +1,51 @@
/*
* 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.pages;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LoginExpiredPage extends AbstractPage {
@FindBy(id = "loginRestartLink")
private WebElement loginRestartLink;
@FindBy(id = "loginContinueLink")
private WebElement loginContinueLink;
public void clickLoginRestartLink() {
loginRestartLink.click();
}
public void clickLoginContinueLink() {
loginContinueLink.click();
}
public boolean isCurrent() {
return driver.getTitle().equals("Page has expired");
}
public void open() {
throw new UnsupportedOperationException();
}
}

View file

@ -54,6 +54,9 @@ public class RegisterPage extends AbstractPage {
@FindBy(className = "instruction")
private WebElement loginInstructionMessage;
@FindBy(linkText = "« Back to Login")
private WebElement backToLoginLink;
public void register(String firstName, String lastName, String email, String username, String password, String passwordConfirm) {
firstNameInput.clear();
@ -125,6 +128,10 @@ public class RegisterPage extends AbstractPage {
submitButton.click();
}
public void clickBackToLogin() {
backToLoginLink.click();
}
public String getError() {
return loginErrorMessage != null ? loginErrorMessage.getText() : null;
}

View file

@ -41,6 +41,7 @@ import org.keycloak.constants.AdapterConstants;
import org.keycloak.jose.jwk.JSONWebKeySet;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
@ -73,7 +74,7 @@ import java.util.*;
*/
public class OAuthClient {
public static final String SERVER_ROOT = AuthServerTestEnricher.getAuthServerContextRoot();
public static final String AUTH_SERVER_ROOT = SERVER_ROOT + "/auth";
public static String AUTH_SERVER_ROOT = SERVER_ROOT + "/auth";
public static final String APP_ROOT = AUTH_SERVER_ROOT + "/realms/master/app";
private static final boolean sslRequired = Boolean.parseBoolean(System.getProperty("auth.server.ssl.required"));
@ -89,7 +90,7 @@ public class OAuthClient {
private String redirectUri;
private String state;
private StateParamProvider state;
private String scope;
@ -162,7 +163,7 @@ public class OAuthClient {
realm = "test";
clientId = "test-app";
redirectUri = APP_ROOT + "/auth";
state = "mystate";
state = KeycloakModelUtils::generateId;
scope = null;
uiLocales = null;
clientSessionState = null;
@ -607,6 +608,7 @@ public class OAuthClient {
if (redirectUri != null) {
b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri);
}
String state = this.state.getState();
if (state != null) {
b.queryParam(OAuth2Constants.STATE, state);
}
@ -692,8 +694,17 @@ public class OAuthClient {
return this;
}
public OAuthClient state(String state) {
this.state = state;
public OAuthClient stateParamHardcoded(String value) {
this.state = () -> {
return value;
};
return this;
}
public OAuthClient stateParamRandom() {
this.state = () -> {
return KeycloakModelUtils.generateId();
};
return this;
}
@ -927,4 +938,12 @@ public class OAuthClient {
return publicKeys.get(realm);
}
private interface StateParamProvider {
String getState();
}
}

View file

@ -158,7 +158,6 @@ public class AccountTest extends AbstractTestRealmKeycloakTest {
@Before
public void before() {
oauth.state("mystate"); // keycloak enforces that a state param has been sent by client
userId = findUser("test-user@localhost").getId();
// Revert any password policy and user password changes
@ -854,7 +853,6 @@ public class AccountTest extends AbstractTestRealmKeycloakTest {
try {
OAuthClient oauth2 = new OAuthClient();
oauth2.init(adminClient, driver2);
oauth2.state("mystate");
oauth2.doLogin("view-sessions", "password");
EventRepresentation login2Event = events.expectLogin().user(userId).detail(Details.USERNAME, "view-sessions").assertEvent();

View file

@ -87,9 +87,6 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
@Before
public void before() {
oauth.state("mystate"); // have to set this as keycloak validates that state is sent
ApiUtil.removeUserByUsername(testRealm(), "test-user@localhost");
UserRepresentation user = UserBuilder.create().enabled(true)
.username("test-user@localhost")

View file

@ -59,11 +59,6 @@ public class RequiredActionResetPasswordTest extends AbstractTestRealmKeycloakTe
@Page
protected LoginPasswordUpdatePage changePasswordPage;
@Before
public void before() {
oauth.state("mystate"); // have to set this as keycloak validates that state is sent
}
@Test
public void tempPassword() throws Exception {

View file

@ -38,10 +38,14 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.broker.BrokerTestTools;
import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.pages.UpdateAccountInformationPage;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.WaitUtils;
@ -72,11 +76,17 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer
public static final String PARENT_USERNAME = "parent";
@Page
protected UpdateAccountInformationPage profilePage;
protected LoginUpdateProfilePage loginUpdateProfilePage;
@Page
protected AccountUpdateProfilePage profilePage;
@Page
private LoginPage loginPage;
@Page
protected ErrorPage errorPage;
public static class ClientApp extends AbstractPageWithInjectedUrl {
public static final String DEPLOYMENT_NAME = "client-linking";
@ -532,6 +542,92 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer
}
@Test
public void testAccountNotLinkedAutomatically() throws Exception {
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
Assert.assertTrue(links.isEmpty());
// Login to account mgmt first
profilePage.open(CHILD_IDP);
WaitUtils.waitForPageToLoad(driver);
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
loginPage.login("child", "password");
profilePage.assertCurrent();
// Now in another tab, open login screen with "prompt=login" . Login screen will be displayed even if I have SSO cookie
UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
.path("nosuch");
String linkUrl = linkBuilder.clone()
.queryParam(OIDCLoginProtocol.PROMPT_PARAM, OIDCLoginProtocol.PROMPT_VALUE_LOGIN)
.build().toString();
navigateTo(linkUrl);
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
loginPage.clickSocial(PARENT_IDP);
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
loginPage.login(PARENT_USERNAME, "password");
// Test I was not automatically linked.
links = realm.users().get(childUserId).getFederatedIdentity();
Assert.assertTrue(links.isEmpty());
loginUpdateProfilePage.assertCurrent();
loginUpdateProfilePage.update("Joe", "Doe", "joe@parent.com");
errorPage.assertCurrent();
Assert.assertEquals("You are already authenticated as different user 'child' in this session. Please logout first.", errorPage.getError());
logoutAll();
// Remove newly created user
String newUserId = ApiUtil.findUserByUsername(realm, "parent").getId();
getCleanup("child").addUserId(newUserId);
}
@Test
public void testAccountLinkingExpired() throws Exception {
RealmResource realm = adminClient.realms().realm(CHILD_IDP);
List<FederatedIdentityRepresentation> links = realm.users().get(childUserId).getFederatedIdentity();
Assert.assertTrue(links.isEmpty());
// Login to account mgmt first
profilePage.open(CHILD_IDP);
WaitUtils.waitForPageToLoad(driver);
Assert.assertTrue(loginPage.isCurrent(CHILD_IDP));
loginPage.login("child", "password");
profilePage.assertCurrent();
// Now in another tab, request account linking
UriBuilder linkBuilder = UriBuilder.fromUri(appPage.getInjectedUrl().toString())
.path("link");
String linkUrl = linkBuilder.clone()
.queryParam("realm", CHILD_IDP)
.queryParam("provider", PARENT_IDP).build().toString();
navigateTo(linkUrl);
Assert.assertTrue(loginPage.isCurrent(PARENT_IDP));
// Logout "child" userSession in the meantime (for example through admin request)
realm.logoutAll();
// Finish login on parent.
loginPage.login(PARENT_USERNAME, "password");
// Test I was not automatically linked
links = realm.users().get(childUserId).getFederatedIdentity();
Assert.assertTrue(links.isEmpty());
errorPage.assertCurrent();
Assert.assertEquals("Requested broker account linking, but current session is no longer valid.", errorPage.getError());
logoutAll();
}
private void navigateTo(String uri) {
driver.navigate().to(uri);
WaitUtils.waitForPageToLoad(driver);

View file

@ -0,0 +1,376 @@
/*
* 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.forms;
import java.io.IOException;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.pages.LoginExpiredPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordResetPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.UserBuilder;
import static org.junit.Assert.assertEquals;
/**
* Test for browser back/forward/refresh buttons
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest {
private String userId;
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Before
public void setup() {
UserRepresentation user = UserBuilder.create()
.username("login-test")
.email("login@test.com")
.enabled(true)
.requiredAction(UserModel.RequiredAction.UPDATE_PROFILE.toString())
.requiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString())
.build();
userId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
expectedMessagesCount = 0;
getCleanup().addUserId(userId);
oauth.clientId("test-app");
}
@Rule
public GreenMailRule greenMail = new GreenMailRule();
@Page
protected AppPage appPage;
@Page
protected LoginPage loginPage;
@Page
protected ErrorPage errorPage;
@Page
protected InfoPage infoPage;
@Page
protected VerifyEmailPage verifyEmailPage;
@Page
protected LoginPasswordResetPage resetPasswordPage;
@Page
protected LoginPasswordUpdatePage updatePasswordPage;
@Page
protected LoginUpdateProfilePage updateProfilePage;
@Page
protected LoginExpiredPage loginExpiredPage;
@Page
protected RegisterPage registerPage;
@Page
protected OAuthGrantPage grantPage;
@Rule
public AssertEvents events = new AssertEvents(this);
private int expectedMessagesCount;
// KEYCLOAK-4670 - Flow 1
@Test
public void invalidLoginAndBackButton() throws IOException, MessagingException {
loginPage.open();
loginPage.login("login-test2", "invalid");
loginPage.assertCurrent();
loginPage.login("login-test3", "invalid");
loginPage.assertCurrent();
// Click browser back. Should be still on login page (TODO: Retest with real browsers like FF or Chrome. Maybe they need some additional actions to confirm re-sending POST request )
driver.navigate().back();
loginPage.assertCurrent();
// Click browser refresh. Should be still on login page
driver.navigate().refresh();
loginPage.assertCurrent();
}
// KEYCLOAK-4670 - Flow 2
@Test
public void requiredActionsBackForwardTest() throws IOException, MessagingException {
loginPage.open();
// Login and assert on "updatePassword" page
loginPage.login("login-test", "password");
updatePasswordPage.assertCurrent();
// Update password and assert on "updateProfile" page
updatePasswordPage.changePassword("password", "password");
updateProfilePage.assertCurrent();
// Click browser back. Assert on "Page expired" page
driver.navigate().back();
loginExpiredPage.assertCurrent();
// Click browser forward. Assert on "updateProfile" page again
driver.navigate().forward();
updateProfilePage.assertCurrent();
// Successfully update profile and assert user logged
updateProfilePage.update("John", "Doe3", "john@doe3.com");
appPage.assertCurrent();
}
// KEYCLOAK-4670 - Flow 3 extended
@Test
public void requiredActionsBackAndRefreshTest() throws IOException, MessagingException {
loginPage.open();
// Login and assert on "updatePassword" page
loginPage.login("login-test", "password");
updatePasswordPage.assertCurrent();
// Click browser refresh. Assert still on updatePassword page
driver.navigate().refresh();
updatePasswordPage.assertCurrent();
// Update password and assert on "updateProfile" page
updatePasswordPage.changePassword("password", "password");
updateProfilePage.assertCurrent();
// Click browser back. Assert on "Page expired" page
driver.navigate().back();
loginExpiredPage.assertCurrent();
// Click browser refresh. Assert still on "Page expired" page
driver.navigate().refresh();
loginExpiredPage.assertCurrent();
// Click "login restart" and assert on loginPage
loginExpiredPage.clickLoginRestartLink();
loginPage.assertCurrent();
// Login again and assert on "updateProfile" page
loginPage.login("login-test", "password");
updateProfilePage.assertCurrent();
// Click browser back. Assert on "Page expired" page
driver.navigate().back();
loginExpiredPage.assertCurrent();
// Click "login continue" and assert on updateProfile page
loginExpiredPage.clickLoginContinueLink();
updateProfilePage.assertCurrent();
// Successfully update profile and assert user logged
updateProfilePage.update("John", "Doe3", "john@doe3.com");
appPage.assertCurrent();
}
// KEYCLOAK-4670 - Flow 4
@Test
public void consentRefresh() {
oauth.clientId("third-party");
// Login and go through required actions
loginPage.open();
loginPage.login("login-test", "password");
updatePasswordPage.changePassword("password", "password");
updateProfilePage.update("John", "Doe3", "john@doe3.com");
// Assert on consent screen
grantPage.assertCurrent();
// Click browser back. Assert on "page expired"
driver.navigate().back();
loginExpiredPage.assertCurrent();
// Click continue login. Assert on consent screen again
loginExpiredPage.clickLoginContinueLink();
grantPage.assertCurrent();
// Click refresh. Assert still on consent screen
driver.navigate().refresh();
grantPage.assertCurrent();
// Confirm consent. Assert authenticated
grantPage.accept();
appPage.assertCurrent();
}
// KEYCLOAK-4670 - Flow 5
@Test
public void clickBackButtonAfterReturnFromRegister() {
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
// Click "Back to login" link on registerPage
registerPage.clickBackToLogin();
loginPage.assertCurrent();
// Click browser "back" button. Should be back on register page
driver.navigate().back();
registerPage.assertCurrent();
}
@Test
public void clickBackButtonFromRegisterPage() {
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
// Click browser "back" button. Should be back on login page
driver.navigate().back();
loginPage.assertCurrent();
}
@Test
public void backButtonToAuthorizationEndpoint() {
loginPage.open();
// Login and assert on "updatePassword" page
loginPage.login("login-test", "password");
updatePasswordPage.assertCurrent();
// Click browser back. I should be on 'page expired' . URL corresponds to OIDC AuthorizationEndpoint
driver.navigate().back();
loginExpiredPage.assertCurrent();
// Click 'restart' link. I should be on login page
loginExpiredPage.clickLoginRestartLink();
loginPage.assertCurrent();
}
@Test
public void backButtonInResetPasswordFlow() throws Exception {
// Click on "forgot password" and type username
loginPage.open();
loginPage.resetPassword();
resetPasswordPage.assertCurrent();
resetPasswordPage.changePassword("login-test");
loginPage.assertCurrent();
assertEquals("You should receive an email shortly with further instructions.", loginPage.getSuccessMessage());
// Receive email
MimeMessage message = greenMail.getReceivedMessages()[greenMail.getReceivedMessages().length - 1];
String changePasswordUrl = ResetPasswordTest.getPasswordResetEmailLink(message);
driver.navigate().to(changePasswordUrl.trim());
updatePasswordPage.assertCurrent();
// Click browser back. Should be on 'page expired'
driver.navigate().back();
loginExpiredPage.assertCurrent();
// Click 'continue' should be on updatePasswordPage
loginExpiredPage.clickLoginContinueLink();
updatePasswordPage.assertCurrent();
// Click browser back. Should be on 'page expired'
driver.navigate().back();
loginExpiredPage.assertCurrent();
// Click 'restart' . Should be on login page
loginExpiredPage.clickLoginRestartLink();
loginPage.assertCurrent();
}
@Test
public void appInitiatedRegistrationWithBackButton() throws Exception {
// Send request from the application directly to 'registrations'
String appInitiatedRegisterUrl = oauth.getLoginFormUrl();
appInitiatedRegisterUrl = appInitiatedRegisterUrl.replace("openid-connect/auth", "openid-connect/registrations"); // Should be done better way...
driver.navigate().to(appInitiatedRegisterUrl);
registerPage.assertCurrent();
// Click 'back to login'
registerPage.clickBackToLogin();
loginPage.assertCurrent();
// Login
loginPage.login("login-test", "password");
updatePasswordPage.assertCurrent();
// Click browser back. Should be on 'page expired'
driver.navigate().back();
loginExpiredPage.assertCurrent();
// Click 'continue' should be on updatePasswordPage
loginExpiredPage.clickLoginContinueLink();
updatePasswordPage.assertCurrent();
// Click browser back. Should be on 'page expired'
driver.navigate().back();
loginExpiredPage.assertCurrent();
// Click 'restart' . Check that I was put to the registration page as flow was initiated as registration flow
loginExpiredPage.clickLoginRestartLink();
registerPage.assertCurrent();
}
}

View file

@ -23,21 +23,26 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.Constants;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder;
import org.openqa.selenium.NoSuchElementException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
@ -47,6 +52,7 @@ import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
@ -395,18 +401,7 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
events.expectLogin().user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
}
@Test
public void loginTimeout() {
loginPage.open();
setTimeOffset(1850);
loginPage.login("login-test", "password");
setTimeOffset(0);
events.expectLogin().clearDetails().detail(Details.CODE_ID, AssertEvents.isCodeId()).user((String) null).session((String) null).error("expired_code").assertEvent().getSessionId();
}
@Test
public void loginLoginHint() {
@ -555,11 +550,33 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
}
}
// Login timeout scenarios
// KEYCLOAK-1037
@Test
public void loginExpiredCode() {
loginPage.open();
setTimeOffset(5000);
// No explicitly call "removeExpired". Hence authSession will still exists, but will be expired
//testingClient.testing().removeExpired("test");
loginPage.login("login@test.com", "password");
loginPage.assertCurrent();
Assert.assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError());
setTimeOffset(0);
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
.assertEvent();
}
// KEYCLOAK-1037
@Test
public void loginExpiredCodeWithExplicitRemoveExpired() {
loginPage.open();
setTimeOffset(5000);
// Explicitly call "removeExpired". Hence authSession won't exist, but will be restarted from the KC_RESTART
testingClient.testing().removeExpired("test");
loginPage.login("login@test.com", "password");
@ -567,13 +584,68 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
//loginPage.assertCurrent();
loginPage.assertCurrent();
//Assert.assertEquals("Login timeout. Please login again.", loginPage.getError());
Assert.assertEquals("You took too long to login. Login process starting from beginning.", loginPage.getError());
setTimeOffset(0);
events.expectLogin().user((String) null).session((String) null).error("expired_code").clearDetails()
events.expectLogin().user((String) null).session((String) null).error(Errors.EXPIRED_CODE).clearDetails()
.detail(Details.RESTART_AFTER_TIMEOUT, "true")
.client((String) null)
.assertEvent();
}
@Test
public void loginExpiredCodeAndExpiredCookies() {
loginPage.open();
driver.manage().deleteAllCookies();
// Cookies are expired including KC_RESTART. No way to continue login. Error page must be shown
loginPage.login("login@test.com", "password");
errorPage.assertCurrent();
}
@Test
public void openLoginFormWithDifferentApplication() throws Exception {
// Login form shown after redirect from admin console
oauth.clientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
oauth.redirectUri(AuthServerTestEnricher.getAuthServerContextRoot() + "/auth/admin/test/console");
oauth.openLoginForm();
// Login form shown after redirect from app
oauth.clientId("test-app");
oauth.redirectUri(OAuthClient.APP_ROOT + "/auth");
oauth.openLoginForm();
assertTrue(loginPage.isCurrent());
loginPage.login("test-user@localhost", "password");
appPage.assertCurrent();
events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
}
@Test
public void openLoginFormAfterExpiredCode() throws Exception {
oauth.openLoginForm();
setTimeOffset(5000);
oauth.openLoginForm();
loginPage.assertCurrent();
try {
String loginError = loginPage.getError();
Assert.fail("Not expected to have error on loginForm. Error is: " + loginError);
} catch (NoSuchElementException nsee) {
// Expected
}
loginPage.login("test-user@localhost", "password");
appPage.assertCurrent();
events.expectLogin().detail(Details.USERNAME, "test-user@localhost").assertEvent();
}
}

View file

@ -126,7 +126,7 @@ public class LogoutTest extends AbstractTestRealmKeycloakTest {
// Check session 1 not logged-in
oauth.openLoginForm();
assertEquals(oauth.getLoginFormUrl(), driver.getCurrentUrl());
loginPage.assertCurrent();
// Login session 3
oauth.doLogin("test-user@localhost", "password");

View file

@ -0,0 +1,248 @@
/*
* 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.forms;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.pages.LoginExpiredPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordResetPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.pages.VerifyEmailPage;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.UserBuilder;
/**
* Tries to test multiple browser tabs
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
private String userId;
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Before
public void setup() {
UserRepresentation user = UserBuilder.create()
.username("login-test")
.email("login@test.com")
.enabled(true)
.requiredAction(UserModel.RequiredAction.UPDATE_PROFILE.toString())
.requiredAction(UserModel.RequiredAction.UPDATE_PASSWORD.toString())
.build();
userId = ApiUtil.createUserAndResetPasswordWithAdminClient(testRealm(), user, "password");
getCleanup().addUserId(userId);
oauth.clientId("test-app");
}
@Rule
public GreenMailRule greenMail = new GreenMailRule();
@Page
protected AppPage appPage;
@Page
protected LoginPage loginPage;
@Page
protected ErrorPage errorPage;
@Page
protected InfoPage infoPage;
@Page
protected VerifyEmailPage verifyEmailPage;
@Page
protected LoginPasswordResetPage resetPasswordPage;
@Page
protected LoginPasswordUpdatePage updatePasswordPage;
@Page
protected LoginUpdateProfilePage updateProfilePage;
@Page
protected LoginExpiredPage loginExpiredPage;
@Page
protected RegisterPage registerPage;
@Page
protected OAuthGrantPage grantPage;
@Rule
public AssertEvents events = new AssertEvents(this);
// Test for scenario when user is logged into JS application in 2 browser tabs. Then click "logout" in tab1 and he is logged-out from both tabs (tab2 is logged-out automatically due to session iframe few seconds later)
// Now both browser tabs show the 1st login screen and we need to make sure that actionURL (code with execution) is valid on both tabs, so user won't have error page when he tries to login from tab1
@Test
public void openMultipleTabs() {
oauth.openLoginForm();
loginPage.assertCurrent();
String actionUrl1 = getActionUrl(driver.getPageSource());
oauth.openLoginForm();
loginPage.assertCurrent();
String actionUrl2 = getActionUrl(driver.getPageSource());
Assert.assertEquals(actionUrl1, actionUrl2);
}
private String getActionUrl(String pageSource) {
return pageSource.split("action=\"")[1].split("\"")[0].replaceAll("&amp;", "&");
}
@Test
public void multipleTabsParallelLoginTest() {
oauth.openLoginForm();
loginPage.assertCurrent();
loginPage.login("login-test", "password");
updatePasswordPage.assertCurrent();
String tab1Url = driver.getCurrentUrl();
// Simulate login in different browser tab tab2. I will be on loginPage again.
oauth.openLoginForm();
loginPage.assertCurrent();
// Login in tab2
loginPage.login("login-test", "password");
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword("password", "password");
updateProfilePage.update("John", "Doe3", "john@doe3.com");
appPage.assertCurrent();
// Try to go back to tab 1. We should have ALREADY_LOGGED_IN info page
driver.navigate().to(tab1Url);
infoPage.assertCurrent();
Assert.assertEquals("You are already logged in.", infoPage.getInfo());
infoPage.clickBackToApplicationLink();
appPage.assertCurrent();
}
@Test
public void expiredAuthenticationAction_currentCodeExpiredExecution() {
// Simulate to open login form in 2 tabs
oauth.openLoginForm();
loginPage.assertCurrent();
String actionUrl1 = getActionUrl(driver.getPageSource());
// Click "register" in tab2
loginPage.clickRegister();
registerPage.assertCurrent();
// Simulate going back to tab1 and confirm login form. Page "showExpired" should be shown (NOTE: WebDriver does it with GET, when real browser would do it with POST. Improve test if needed...)
driver.navigate().to(actionUrl1);
loginExpiredPage.assertCurrent();
// Click on continue and assert I am on "register" form
loginExpiredPage.clickLoginContinueLink();
registerPage.assertCurrent();
// Finally click "Back to login" and authenticate
registerPage.clickBackToLogin();
loginPage.assertCurrent();
// Login success now
loginPage.login("login-test", "password");
updatePasswordPage.changePassword("password", "password");
updateProfilePage.update("John", "Doe3", "john@doe3.com");
appPage.assertCurrent();
}
@Test
public void expiredAuthenticationAction_expiredCodeCurrentExecution() {
// Simulate to open login form in 2 tabs
oauth.openLoginForm();
loginPage.assertCurrent();
String actionUrl1 = getActionUrl(driver.getPageSource());
loginPage.login("invalid", "invalid");
loginPage.assertCurrent();
Assert.assertEquals("Invalid username or password.", loginPage.getError());
// Simulate going back to tab1 and confirm login form. Login page with "action expired" message should be shown (NOTE: WebDriver does it with GET, when real browser would do it with POST. Improve test if needed...)
driver.navigate().to(actionUrl1);
loginPage.assertCurrent();
Assert.assertEquals("Action expired. Please continue with login now.", loginPage.getError());
// Login success now
loginPage.login("login-test", "password");
updatePasswordPage.changePassword("password", "password");
updateProfilePage.update("John", "Doe3", "john@doe3.com");
appPage.assertCurrent();
}
@Test
public void expiredAuthenticationAction_expiredCodeExpiredExecution() {
// Open tab1
oauth.openLoginForm();
loginPage.assertCurrent();
String actionUrl1 = getActionUrl(driver.getPageSource());
// Authenticate in tab2
loginPage.login("login-test", "password");
updatePasswordPage.assertCurrent();
// Simulate going back to tab1 and confirm login form. Page "Page expired" should be shown (NOTE: WebDriver does it with GET, when real browser would do it with POST. Improve test if needed...)
driver.navigate().to(actionUrl1);
loginExpiredPage.assertCurrent();
// Finish login
loginExpiredPage.clickLoginContinueLink();
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword("password", "password");
updateProfilePage.update("John", "Doe3", "john@doe3.com");
appPage.assertCurrent();
}
}

View file

@ -172,8 +172,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
String changePasswordUrl = resetPassword("login-test");
events.clear();
// TODO:hmlnarik is this correct??
assertSecondPasswordResetFails(changePasswordUrl, "test-app"); // KC_RESTART exists, hence client-ID is taken from it.
assertSecondPasswordResetFails(changePasswordUrl, null); // KC_RESTART doesn't exists, it was deleted after first successful reset-password flow was finished
}
@Test
@ -195,7 +194,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
assertEquals("An error occurred, please login again through your application.", errorPage.getError());
events.expect(EventType.RESET_PASSWORD)
.client(clientId)
.client((String) null)
.session((String) null)
.user(userId)
.detail(Details.USERNAME, "login-test")
@ -286,6 +285,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
}
private void resetPasswordInvalidPassword(String username, String password, String error) throws IOException, MessagingException {
initiateResetPasswordFromResetPasswordPage(username);
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String) null)
@ -299,6 +299,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
driver.navigate().to(changePasswordUrl.trim());
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword(password, password);
@ -308,7 +309,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
events.expectRequiredAction(EventType.UPDATE_PASSWORD_ERROR).error(Errors.PASSWORD_REJECTED).user(userId).detail(Details.USERNAME, "login-test").assertEvent().getSessionId();
}
public void initiateResetPasswordFromResetPasswordPage(String username) {
private void initiateResetPasswordFromResetPasswordPage(String username) {
loginPage.open();
loginPage.resetPassword();
@ -547,7 +548,7 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
}
@Test
public void resetPasswordWithPasswordHisoryPolicy() throws IOException, MessagingException {
public void resetPasswordWithPasswordHistoryPolicy() throws IOException, MessagingException {
//Block passwords that are equal to previous passwords. Default value is 3.
setPasswordPolicy("passwordHistory");
@ -563,13 +564,14 @@ public class ResetPasswordTest extends AbstractTestRealmKeycloakTest {
resetPasswordInvalidPassword("login-test", "password1", "Invalid password: must not be equal to any of last 3 passwords.");
resetPasswordInvalidPassword("login-test", "password2", "Invalid password: must not be equal to any of last 3 passwords.");
setTimeOffset(8000000);
setTimeOffset(6000000);
resetPassword("login-test", "password3");
resetPasswordInvalidPassword("login-test", "password1", "Invalid password: must not be equal to any of last 3 passwords.");
resetPasswordInvalidPassword("login-test", "password2", "Invalid password: must not be equal to any of last 3 passwords.");
resetPasswordInvalidPassword("login-test", "password3", "Invalid password: must not be equal to any of last 3 passwords.");
setTimeOffset(8000000);
resetPassword("login-test", "password");
} finally {
setTimeOffset(0);

View file

@ -23,9 +23,12 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.models.UserModel;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.drone.Different;
@ -33,6 +36,7 @@ import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LoginPasswordUpdatePage;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.WebDriver;
@ -59,6 +63,9 @@ public class SSOTest extends AbstractTestRealmKeycloakTest {
@Page
protected AccountUpdateProfilePage profilePage;
@Page
protected LoginPasswordUpdatePage updatePasswordPage;
@Rule
public AssertEvents events = new AssertEvents(this);
@ -109,6 +116,7 @@ public class SSOTest extends AbstractTestRealmKeycloakTest {
events.clear();
}
@Test
public void multipleSessions() {
loginPage.open();
@ -124,7 +132,6 @@ public class SSOTest extends AbstractTestRealmKeycloakTest {
OAuthClient oauth2 = new OAuthClient();
oauth2.init(adminClient, driver2);
oauth2.state("mystate");
oauth2.doLogin("test-user@localhost", "password");
EventRepresentation login2 = events.expectLogin().assertEvent();
@ -158,4 +165,38 @@ public class SSOTest extends AbstractTestRealmKeycloakTest {
}
}
@Test
public void loginWithRequiredActionAddedInTheMeantime() {
// SSO login
loginPage.open();
loginPage.login("test-user@localhost", "password");
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
EventRepresentation loginEvent = events.expectLogin().assertEvent();
String sessionId = loginEvent.getSessionId();
// Add update-profile required action to user now
UserRepresentation user = testRealm().users().get(loginEvent.getUserId()).toRepresentation();
user.getRequiredActions().add(UserModel.RequiredAction.UPDATE_PASSWORD.toString());
testRealm().users().get(loginEvent.getUserId()).update(user);
// Attempt SSO login. update-password form is shown
oauth.openLoginForm();
updatePasswordPage.assertCurrent();
updatePasswordPage.changePassword("password", "password");
events.expectRequiredAction(EventType.UPDATE_PASSWORD).assertEvent();
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
loginEvent = events.expectLogin().removeDetail(Details.USERNAME).client("test-app").assertEvent();
String sessionId2 = loginEvent.getSessionId();
assertEquals(sessionId, sessionId2);
}
}

View file

@ -16,8 +16,6 @@
*/
package org.keycloak.testsuite.oauth;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
@ -31,7 +29,6 @@ import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.OAuthClient;
import org.openqa.selenium.By;
@ -61,11 +58,12 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
public void clientConfiguration() {
oauth.responseType(OAuth2Constants.CODE);
oauth.responseMode(null);
oauth.stateParamRandom();
}
@Test
public void authorizationRequest() throws IOException {
oauth.state("OpenIdConnect.AuthenticationProperties=2302984sdlk");
oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk");
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
@ -100,8 +98,6 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
public void authorizationValidRedirectUri() throws IOException {
ClientManager.realm(adminClient.realm("test")).clientId("test-app").addRedirectUris(oauth.getRedirectUri());
oauth.state("mystate");
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertTrue(response.isRedirected());
@ -113,7 +109,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
@Test
public void authorizationRequestNoState() throws IOException {
oauth.state(null);
oauth.stateParamHardcoded(null);
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
@ -143,7 +139,7 @@ public class AuthorizationCodeTest extends AbstractKeycloakTest {
@Test
public void authorizationRequestFormPostResponseMode() throws IOException {
oauth.responseMode(OIDCResponseMode.FORM_POST.toString().toLowerCase());
oauth.state("OpenIdConnect.AuthenticationProperties=2302984sdlk");
oauth.stateParamHardcoded("OpenIdConnect.AuthenticationProperties=2302984sdlk");
oauth.doLoginGrant("test-user@localhost", "password");
String sources = driver.getPageSource();

View file

@ -36,6 +36,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.ClientManager;
@ -204,6 +205,8 @@ public class ResourceOwnerPasswordCredentialsGrantTest extends AbstractKeycloakT
.removeDetail(Details.CONSENT)
.assertEvent();
Assert.assertTrue(login.equals(accessToken.getPreferredUsername()) || login.equals(accessToken.getEmail()));
assertEquals(accessToken.getSessionState(), refreshToken.getSessionState());
OAuthClient.AccessTokenResponse refreshedResponse = oauth.doRefreshTokenRequest(response.getRefreshToken(), "secret");

View file

@ -324,6 +324,8 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
@Test
public void requestParamUnsigned() throws Exception {
oauth.stateParamHardcoded("mystate2");
String validRedirectUri = oauth.getRedirectUri();
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
@ -344,12 +346,14 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
oauth.request(requestStr);
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertNotNull(response.getCode());
Assert.assertEquals("mystate", response.getState());
Assert.assertEquals("mystate2", response.getState());
assertTrue(appPage.isCurrent());
}
@Test
public void requestUriParamUnsigned() throws Exception {
oauth.stateParamHardcoded("mystate1");
String validRedirectUri = oauth.getRedirectUri();
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
@ -367,12 +371,14 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertNotNull(response.getCode());
Assert.assertEquals("mystate", response.getState());
Assert.assertEquals("mystate1", response.getState());
assertTrue(appPage.isCurrent());
}
@Test
public void requestUriParamSigned() throws Exception {
oauth.stateParamHardcoded("mystate3");
String validRedirectUri = oauth.getRedirectUri();
TestOIDCEndpointsApplicationResource oidcClientEndpointsResource = testingClient.testApp().oidcClientEndpoints();
@ -412,7 +418,7 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest
// Check signed request_uri will pass
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
Assert.assertNotNull(response.getCode());
Assert.assertEquals("mystate", response.getState());
Assert.assertEquals("mystate3", response.getState());
assertTrue(appPage.isCurrent());
// Revert requiring signature for client

View file

@ -69,11 +69,13 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent
}
@Test
public void testDisabledUser() {
public void testDisabledUser() throws Exception {
KeycloakSession session = brokerServerRule.startSession();
setUpdateProfileFirstLogin(session.realms().getRealmByName("realm-with-broker"), IdentityProviderRepresentation.UPFLM_OFF);
brokerServerRule.stopSession(session, true);
Thread.sleep(10000000);
driver.navigate().to("http://localhost:8081/test-app");
loginPage.clickSocial(getProviderId());
loginPage.login("test-user", "password");
@ -328,7 +330,7 @@ public abstract class AbstractKeycloakIdentityProviderTest extends AbstractIdent
}
@Test
public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() {
public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() throws Exception {
RealmModel realm = getRealm();
realm.setRegistrationEmailAsUsername(true);
setUpdateProfileFirstLogin(realm, IdentityProviderRepresentation.UPFLM_OFF);

View file

@ -116,7 +116,7 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP
}
@Test
public void testDisabledUser() {
public void testDisabledUser() throws Exception {
super.testDisabledUser();
}
@ -156,7 +156,7 @@ public class OIDCKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP
}
@Test
public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() {
public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() throws Exception {
super.testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername();
}

View file

@ -143,6 +143,7 @@ public class OIDCKeycloakServerBrokerWithConsentTest extends AbstractIdentityPro
this.session = brokerServerRule.startSession();
session.sessions().removeExpired(getRealm());
session.authenticationSessions().removeExpired(getRealm());
brokerServerRule.stopSession(this.session, true);
this.session = brokerServerRule.startSession();

View file

@ -128,7 +128,7 @@ public class SAMLKeyCloakServerBrokerBasicTest extends AbstractKeycloakIdentityP
}
@Test
public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() {
public void testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername() throws Exception {
super.testSuccessfulAuthenticationWithoutUpdateProfile_newUser_emailAsUsername();
}

View file

@ -28,6 +28,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserManager;
import org.keycloak.models.UserModel;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.testsuite.rule.KeycloakRule;
/**
@ -77,7 +78,7 @@ public class AuthenticationSessionProviderTest {
ClientModel client1 = realm.getClientByClientId("test-app");
UserModel user1 = session.users().getUserByUsername("user1", realm);
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client1, false);
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client1);
authSession.setAction("foo");
authSession.setTimestamp(100);
@ -86,7 +87,8 @@ public class AuthenticationSessionProviderTest {
// Ensure session is here
authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
testLoginSession(authSession, client1.getId(), null, "foo", 100);
testLoginSession(authSession, client1.getId(), null, "foo");
Assert.assertEquals(100, authSession.getTimestamp());
// Update and commit
authSession.setAction("foo-updated");
@ -97,7 +99,8 @@ public class AuthenticationSessionProviderTest {
// Ensure session was updated
authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
testLoginSession(authSession, client1.getId(), user1.getId(), "foo-updated", 200);
testLoginSession(authSession, client1.getId(), user1.getId(), "foo-updated");
Assert.assertEquals(200, authSession.getTimestamp());
// Remove and commit
session.authenticationSessions().removeAuthenticationSession(realm, authSession);
@ -109,14 +112,52 @@ public class AuthenticationSessionProviderTest {
}
private void testLoginSession(AuthenticationSessionModel authSession, String expectedClientId, String expectedUserId, String expectedAction, int expectedTimestamp) {
@Test
public void testLoginSessionRestart() {
ClientModel client1 = realm.getClientByClientId("test-app");
UserModel user1 = session.users().getUserByUsername("user1", realm);
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client1);
authSession.setAction("foo");
authSession.setTimestamp(100);
authSession.setAuthenticatedUser(user1);
authSession.setAuthNote("foo", "bar");
authSession.setClientNote("foo2", "bar2");
authSession.setExecutionStatus("123", CommonClientSessionModel.ExecutionStatus.SUCCESS);
resetSession();
client1 = realm.getClientByClientId("test-app");
authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
authSession.restartSession(realm, client1);
resetSession();
authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
testLoginSession(authSession, client1.getId(), null, null);
Assert.assertTrue(authSession.getTimestamp() > 0);
Assert.assertTrue(authSession.getClientNotes().isEmpty());
Assert.assertNull(authSession.getAuthNote("foo2"));
Assert.assertTrue(authSession.getExecutionStatus().isEmpty());
}
private void testLoginSession(AuthenticationSessionModel authSession, String expectedClientId, String expectedUserId, String expectedAction) {
Assert.assertEquals(expectedClientId, authSession.getClient().getId());
if (expectedUserId == null) {
Assert.assertNull(authSession.getAuthenticatedUser());
} else {
Assert.assertEquals(expectedUserId, authSession.getAuthenticatedUser().getId());
}
if (expectedAction == null) {
Assert.assertNull(authSession.getAction());
} else {
Assert.assertEquals(expectedAction, authSession.getAction());
Assert.assertEquals(expectedTimestamp, authSession.getTimestamp());
}
}
}

View file

@ -103,7 +103,7 @@ public class CacheTest {
user.setFirstName("firstName");
user.addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP);
UserSessionModel userSession = session.sessions().createUserSession(realm, user, "testAddUserNotAddedToCache", "127.0.0.1", "auth", false, null, null);
UserSessionModel userSession = session.sessions().createUserSession("123", realm, user, "testAddUserNotAddedToCache", "127.0.0.1", "auth", false, null, null);
UserModel user2 = userSession.getUser();
user.setLastName("lastName");

View file

@ -75,7 +75,7 @@ public class ClusterSessionCleanerTest {
RealmModel realm1 = session1.realms().getRealmByName(REALM_NAME);
UserModel user1 = session1.users().getUserByUsername("test-user@localhost", realm1);
for (int i=0 ; i<15 ; i++) {
session1.sessions().createUserSession(realm1, user1, user1.getUsername(), "127.0.0.1", "form", true, null, null);
session1.sessions().createUserSession("123", realm1, user1, user1.getUsername(), "127.0.0.1", "form", true, null, null);
}
session1 = commit(server1, session1);
@ -87,7 +87,7 @@ public class ClusterSessionCleanerTest {
Assert.assertEquals(user2.getId(), user1.getId());
for (int i=0 ; i<15 ; i++) {
session2.sessions().createUserSession(realm2, user2, user2.getUsername(), "127.0.0.1", "form", true, null, null);
session2.sessions().createUserSession("456", realm2, user2, user2.getUsername(), "127.0.0.1", "form", true, null, null);
}
session2 = commit(server2, session2);

View file

@ -31,6 +31,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.models.UserManager;
import org.keycloak.testsuite.rule.KeycloakRule;
@ -283,11 +284,11 @@ public class UserSessionProviderTest {
Set<String> expiredClientSessions = new HashSet<String>();
Time.setOffset(-(realm.getSsoSessionMaxLifespan() + 1));
expired.add(session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null).getId());
expired.add(session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null).getId());
expiredClientSessions.add(session.sessions().createClientSession(realm, client).getId());
Time.setOffset(0);
UserSessionModel s = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.1", "form", true, null, null);
UserSessionModel s = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.1", "form", true, null, null);
//s.setLastSessionRefresh(Time.currentTime() - (realm.getSsoSessionIdleTimeout() + 1));
s.setLastSessionRefresh(0);
expired.add(s.getId());
@ -299,7 +300,7 @@ public class UserSessionProviderTest {
Set<String> valid = new HashSet<String>();
Set<String> validClientSessions = new HashSet<String>();
valid.add(session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null).getId());
valid.add(session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null).getId());
validClientSessions.add(session.sessions().createClientSession(realm, client).getId());
resetSession();
@ -427,7 +428,7 @@ public class UserSessionProviderTest {
try {
for (int i = 0; i < 25; i++) {
Time.setOffset(i);
UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0." + i, "form", false, null, null);
UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0." + i, "form", false, null, null);
ClientSessionModel clientSession = session.sessions().createClientSession(realm, realm.getClientByClientId("test-app"));
clientSession.setUserSession(userSession);
clientSession.setRedirectUri("http://redirect");
@ -450,7 +451,7 @@ public class UserSessionProviderTest {
@Test
public void testCreateAndGetInSameTransaction() {
UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
ClientSessionModel clientSession = createClientSession(realm.getClientByClientId("test-app"), userSession, "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
Assert.assertNotNull(session.sessions().getUserSession(realm, userSession.getId()));
@ -463,7 +464,7 @@ public class UserSessionProviderTest {
@Test
public void testClientLoginSessions() {
UserSessionModel userSession = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
UserSessionModel userSession = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
ClientModel client1 = realm.getClientByClientId("test-app");
ClientModel client2 = realm.getClientByClientId("third-party");
@ -527,6 +528,27 @@ public class UserSessionProviderTest {
Assert.assertNull(clientSessions.get(client1.getId()));
}
@Test
public void testFailCreateExistingSession() {
UserSessionModel userSession = session.sessions().createUserSession("123", realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
// commit
resetSession();
try {
session.sessions().createUserSession("123", realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
kc.stopSession(session, true);
Assert.fail("Not expected to successfully create duplicated userSession");
} catch (IllegalStateException e) {
// Expected
session = kc.startSession();
}
}
private void testClientLoginSession(AuthenticatedClientSessionModel clientLoginSession, String expectedClientId, String expectedUserSessionId, String expectedAction, int expectedTimestamp) {
Assert.assertEquals(expectedClientId, clientLoginSession.getClient().getClientId());
Assert.assertEquals(expectedUserSessionId, clientLoginSession.getUserSession().getId());
@ -632,7 +654,7 @@ public class UserSessionProviderTest {
private UserSessionModel[] createSessions() {
UserSessionModel[] sessions = new UserSessionModel[3];
sessions[0] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
sessions[0] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.1", "form", true, null, null);
Set<String> roles = new HashSet<String>();
roles.add("one");
@ -645,10 +667,10 @@ public class UserSessionProviderTest {
createClientSession(realm.getClientByClientId("test-app"), sessions[0], "http://redirect", "state", roles, protocolMappers);
createClientSession(realm.getClientByClientId("third-party"), sessions[0], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
sessions[1] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
sessions[1] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user1", realm), "user1", "127.0.0.2", "form", true, null, null);
createClientSession(realm.getClientByClientId("test-app"), sessions[1], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
sessions[2] = session.sessions().createUserSession(realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null);
sessions[2] = session.sessions().createUserSession(KeycloakModelUtils.generateId(), realm, session.users().getUserByUsername("user2", realm), "user2", "127.0.0.3", "form", true, null, null);
createClientSession(realm.getClientByClientId("test-app"), sessions[2], "http://redirect", "state", new HashSet<String>(), new HashSet<String>());
resetSession();

View file

@ -21,7 +21,6 @@ import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.ApplicationServlet;

View file

@ -80,7 +80,6 @@ log4j.logger.org.apache.directory.server.ldap.LdapProtocolHandler=error
#log4j.logger.org.apache.http.impl.conn=debug
# Enable to view details from identity provider authenticator
# log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
# TODO: Remove
log4j.logger.org.keycloak.models.sessions.infinispan.InfinispanUserSessionProvider=debug
log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
log4j.logger.org.keycloak.services.resources.IdentityBrokerService=trace
log4j.logger.org.keycloak.broker=trace

View file

@ -6,7 +6,8 @@
${msg("pageExpiredTitle")}
<#elseif section = "form">
<p id="instruction1" class="instruction">
${msg("pageExpiredMsg1")} <a href="${url.loginRestartFlowUrl}">${msg("doClickHere")}</a> . ${msg("pageExpiredMsg2")} <a href="${url.loginAction}">${msg("doClickHere")}</a> .
${msg("pageExpiredMsg1")} <a id="loginRestartLink" href="${url.loginRestartFlowUrl}">${msg("doClickHere")}</a> .
${msg("pageExpiredMsg2")} <a id="loginContinueLink" href="${url.loginAction}">${msg("doClickHere")}</a> .
</p>
</#if>
</@layout.registrationLayout>

View file

@ -127,6 +127,7 @@ invalidEmailMessage=Invalid email address.
accountDisabledMessage=Account is disabled, contact admin.
accountTemporarilyDisabledMessage=Account is temporarily disabled, contact admin or try again later.
expiredCodeMessage=Login timeout. Please login again.
expiredActionMessage=Action expired. Please continue with login now.
missingFirstNameMessage=Please specify first name.
missingLastNameMessage=Please specify last name.
@ -213,6 +214,7 @@ 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.
emailVerifiedMessage=Your email address has been verified.
staleEmailVerificationLink=The link you clicked is a old stale link and is no longer valid. Maybe you have already verified your email?
identityProviderAlreadyLinkedMessage=Federated identity returned by {0} is already linked to another user.
locale_ca=Catal\u00E0
locale_de=Deutsch
@ -233,5 +235,7 @@ clientNotFoundMessage=Client not found.
clientDisabledMessage=Client disabled.
invalidParameterMessage=Invalid parameter\: {0}
alreadyLoggedIn=You are already logged in.
differentUserAuthenticated=You are already authenticated as different user ''{0}'' in this session. Please logout first.
brokerLinkingSessionExpired=Requested broker account linking, but current session is no longer valid.
p3pPolicy=CP="This is not a P3P policy!"