KEYCLOAK-4626 AuthenticationSessions - brokering works. Few other fixes and tests added
This commit is contained in:
parent
b55b089355
commit
e7272dc05a
98 changed files with 3628 additions and 1703 deletions
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<>();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -163,6 +163,7 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
|||
}
|
||||
|
||||
session.sessions().removeExpired(realm);
|
||||
session.authenticationSessions().removeExpired(realm);
|
||||
return Response.ok().build();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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("&", "&");
|
||||
}
|
||||
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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,9 +285,10 @@ 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)
|
||||
events.expectRequiredAction(EventType.SEND_RESET_PASSWORD).user(userId).session((String) null)
|
||||
.detail(Details.USERNAME, username).detail(Details.EMAIL, "login@test.com").assertEvent();
|
||||
|
||||
assertEquals(expectedMessagesCount, greenMail.getReceivedMessages().length);
|
||||
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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!"
|
||||
|
|
Loading…
Reference in a new issue