KEYCLOAK-5797 Refactoring authenticationSessions to support login in multiple browser tabs with different clients
This commit is contained in:
parent
b466f4d0b6
commit
7b03eed9c8
48 changed files with 844 additions and 550 deletions
|
@ -36,6 +36,8 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
||||||
|
|
||||||
private String authSessionId;
|
private String authSessionId;
|
||||||
|
|
||||||
|
private String clientUUID;
|
||||||
|
|
||||||
private Map<String, String> authNotesFragment;
|
private Map<String, String> authNotesFragment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,9 +46,10 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
||||||
* @param authNotesFragment
|
* @param authNotesFragment
|
||||||
* @return Event. Note that {@code authNotesFragment} property is not thread safe which is fine for now.
|
* @return Event. Note that {@code authNotesFragment} property is not thread safe which is fine for now.
|
||||||
*/
|
*/
|
||||||
public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, Map<String, String> authNotesFragment) {
|
public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, String clientUUID, Map<String, String> authNotesFragment) {
|
||||||
AuthenticationSessionAuthNoteUpdateEvent event = new AuthenticationSessionAuthNoteUpdateEvent();
|
AuthenticationSessionAuthNoteUpdateEvent event = new AuthenticationSessionAuthNoteUpdateEvent();
|
||||||
event.authSessionId = authSessionId;
|
event.authSessionId = authSessionId;
|
||||||
|
event.clientUUID = clientUUID;
|
||||||
event.authNotesFragment = new LinkedHashMap<>(authNotesFragment);
|
event.authNotesFragment = new LinkedHashMap<>(authNotesFragment);
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
@ -55,13 +58,18 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
||||||
return authSessionId;
|
return authSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getClientUUID() {
|
||||||
|
return clientUUID;
|
||||||
|
}
|
||||||
|
|
||||||
public Map<String, String> getAuthNotesFragment() {
|
public Map<String, String> getAuthNotesFragment() {
|
||||||
return authNotesFragment;
|
return authNotesFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, authNotesFragment=%s ]", authSessionId, authNotesFragment);
|
return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, clientUUID=%s, authNotesFragment=%s ]",
|
||||||
|
authSessionId, clientUUID, authNotesFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ExternalizerImpl implements Externalizer<AuthenticationSessionAuthNoteUpdateEvent> {
|
public static class ExternalizerImpl implements Externalizer<AuthenticationSessionAuthNoteUpdateEvent> {
|
||||||
|
@ -73,6 +81,7 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
||||||
output.writeByte(VERSION_1);
|
output.writeByte(VERSION_1);
|
||||||
|
|
||||||
MarshallUtil.marshallString(value.authSessionId, output);
|
MarshallUtil.marshallString(value.authSessionId, output);
|
||||||
|
MarshallUtil.marshallString(value.clientUUID, output);
|
||||||
MarshallUtil.marshallMap(value.authNotesFragment, output);
|
MarshallUtil.marshallMap(value.authNotesFragment, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,6 +97,7 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
||||||
|
|
||||||
public AuthenticationSessionAuthNoteUpdateEvent readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
|
public AuthenticationSessionAuthNoteUpdateEvent readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
|
||||||
return create(
|
return create(
|
||||||
|
MarshallUtil.unmarshallString(input),
|
||||||
MarshallUtil.unmarshallString(input),
|
MarshallUtil.unmarshallString(input),
|
||||||
MarshallUtil.unmarshallMap(input, HashMap::new)
|
MarshallUtil.unmarshallMap(input, HashMap::new)
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,14 +23,13 @@ import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
|
||||||
import org.keycloak.common.util.Time;
|
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NOTE: Calling setter doesn't automatically enlist for update
|
* NOTE: Calling setter doesn't automatically enlist for update
|
||||||
|
@ -39,39 +38,37 @@ import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
*/
|
*/
|
||||||
public class AuthenticationSessionAdapter implements AuthenticationSessionModel {
|
public class AuthenticationSessionAdapter implements AuthenticationSessionModel {
|
||||||
|
|
||||||
private KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
private InfinispanAuthenticationSessionProvider provider;
|
private final RootAuthenticationSessionAdapter parent;
|
||||||
private Cache<String, AuthenticationSessionEntity> cache;
|
private final String clientUUID;
|
||||||
private RealmModel realm;
|
|
||||||
private AuthenticationSessionEntity entity;
|
private AuthenticationSessionEntity entity;
|
||||||
|
|
||||||
public AuthenticationSessionAdapter(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, Cache<String, AuthenticationSessionEntity> cache, RealmModel realm,
|
public AuthenticationSessionAdapter(KeycloakSession session, RootAuthenticationSessionAdapter parent, String clientUUID, AuthenticationSessionEntity entity) {
|
||||||
AuthenticationSessionEntity entity) {
|
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.provider = provider;
|
this.parent = parent;
|
||||||
this.cache = cache;
|
this.clientUUID = clientUUID;
|
||||||
this.realm = realm;
|
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
void update() {
|
private void update() {
|
||||||
provider.tx.replace(cache, entity.getId(), entity);
|
parent.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public RootAuthenticationSessionModel getParentSession() {
|
||||||
return entity.getId();
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RealmModel getRealm() {
|
public RealmModel getRealm() {
|
||||||
return realm;
|
return parent.getRealm();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ClientModel getClient() {
|
public ClientModel getClient() {
|
||||||
return realm.getClientById(entity.getClientUuid());
|
return getRealm().getClientById(clientUUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -85,16 +82,6 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getTimestamp() {
|
|
||||||
return entity.getTimestamp();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setTimestamp(int timestamp) {
|
|
||||||
entity.setTimestamp(timestamp);
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getAction() {
|
public String getAction() {
|
||||||
|
@ -303,7 +290,7 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserModel getAuthenticatedUser() {
|
public UserModel getAuthenticatedUser() {
|
||||||
return entity.getAuthUserId() == null ? null : session.users().getUserById(entity.getAuthUserId(), realm); }
|
return entity.getAuthUserId() == null ? null : session.users().getUserById(entity.getAuthUserId(), getRealm()); }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setAuthenticatedUser(UserModel user) {
|
public void setAuthenticatedUser(UserModel user) {
|
||||||
|
@ -312,20 +299,4 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
||||||
update();
|
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.setRealmId(realm.getId());
|
|
||||||
entity.setClientUuid(client.getId());
|
|
||||||
entity.setTimestamp(Time.currentTime());
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,15 +28,14 @@ import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
|
import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
|
|
||||||
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
|
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
|
||||||
import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
|
import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction;
|
||||||
import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate;
|
import org.keycloak.models.sessions.infinispan.stream.RootAuthenticationSessionPredicate;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.models.utils.RealmInfoUtil;
|
import org.keycloak.models.utils.RealmInfoUtil;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
|
||||||
import org.keycloak.sessions.AuthenticationSessionProvider;
|
import org.keycloak.sessions.AuthenticationSessionProvider;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -46,11 +45,11 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||||
private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProvider.class);
|
private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProvider.class);
|
||||||
|
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
private final Cache<String, AuthenticationSessionEntity> cache;
|
private final Cache<String, RootAuthenticationSessionEntity> cache;
|
||||||
protected final InfinispanKeycloakTransaction tx;
|
protected final InfinispanKeycloakTransaction tx;
|
||||||
protected final SessionEventsSenderTransaction clusterEventsSenderTx;
|
protected final SessionEventsSenderTransaction clusterEventsSenderTx;
|
||||||
|
|
||||||
public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache<String, AuthenticationSessionEntity> cache) {
|
public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache<String, RootAuthenticationSessionEntity> cache) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.cache = cache;
|
this.cache = cache;
|
||||||
|
|
||||||
|
@ -62,38 +61,33 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client) {
|
public RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm) {
|
||||||
String id = KeycloakModelUtils.generateId();
|
String id = KeycloakModelUtils.generateId();
|
||||||
return createAuthenticationSession(id, realm, client);
|
return createRootAuthenticationSession(id, realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client) {
|
public RootAuthenticationSessionModel createRootAuthenticationSession(String id, RealmModel realm) {
|
||||||
AuthenticationSessionEntity entity = new AuthenticationSessionEntity();
|
RootAuthenticationSessionEntity entity = new RootAuthenticationSessionEntity();
|
||||||
entity.setId(id);
|
entity.setId(id);
|
||||||
entity.setRealmId(realm.getId());
|
entity.setRealmId(realm.getId());
|
||||||
entity.setTimestamp(Time.currentTime());
|
entity.setTimestamp(Time.currentTime());
|
||||||
entity.setClientUuid(client.getId());
|
|
||||||
|
|
||||||
tx.put(cache, id, entity);
|
tx.put(cache, id, entity);
|
||||||
|
|
||||||
AuthenticationSessionAdapter wrap = wrap(realm, entity);
|
|
||||||
return wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AuthenticationSessionAdapter wrap(RealmModel realm, AuthenticationSessionEntity entity) {
|
|
||||||
return entity==null ? null : new AuthenticationSessionAdapter(session, this, cache, realm, entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AuthenticationSessionModel getAuthenticationSession(RealmModel realm, String authenticationSessionId) {
|
|
||||||
AuthenticationSessionEntity entity = getAuthenticationSessionEntity(realm, authenticationSessionId);
|
|
||||||
return wrap(realm, entity);
|
return wrap(realm, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthenticationSessionEntity getAuthenticationSessionEntity(RealmModel realm, String authSessionId) {
|
|
||||||
|
private RootAuthenticationSessionAdapter wrap(RealmModel realm, RootAuthenticationSessionEntity entity) {
|
||||||
|
return entity==null ? null : new RootAuthenticationSessionAdapter(session, this, cache, realm, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private RootAuthenticationSessionEntity getRootAuthenticationSessionEntity(RealmModel realm, String authSessionId) {
|
||||||
// Chance created in this transaction
|
// Chance created in this transaction
|
||||||
AuthenticationSessionEntity entity = tx.get(cache, authSessionId);
|
RootAuthenticationSessionEntity entity = tx.get(cache, authSessionId);
|
||||||
|
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
entity = cache.get(authSessionId);
|
entity = cache.get(authSessionId);
|
||||||
|
@ -102,10 +96,6 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authenticationSession) {
|
|
||||||
tx.remove(cache, authenticationSession.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeExpired(RealmModel realm) {
|
public void removeExpired(RealmModel realm) {
|
||||||
|
@ -115,16 +105,16 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||||
|
|
||||||
|
|
||||||
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
|
// Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account)
|
||||||
Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
|
Iterator<Map.Entry<String, RootAuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
|
||||||
.entrySet()
|
.entrySet()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(AuthenticationSessionPredicate.create(realm.getId()).expired(expired))
|
.filter(RootAuthenticationSessionPredicate.create(realm.getId()).expired(expired))
|
||||||
.iterator();
|
.iterator();
|
||||||
|
|
||||||
int counter = 0;
|
int counter = 0;
|
||||||
while (itr.hasNext()) {
|
while (itr.hasNext()) {
|
||||||
counter++;
|
counter++;
|
||||||
AuthenticationSessionEntity entity = itr.next().getValue();
|
RootAuthenticationSessionEntity entity = itr.next().getValue();
|
||||||
tx.remove(CacheDecorators.localCache(cache), entity.getId());
|
tx.remove(CacheDecorators.localCache(cache), entity.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,10 +131,10 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onRealmRemovedEvent(String realmId) {
|
protected void onRealmRemovedEvent(String realmId) {
|
||||||
Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
|
Iterator<Map.Entry<String, RootAuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
|
||||||
.entrySet()
|
.entrySet()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(AuthenticationSessionPredicate.create(realmId))
|
.filter(RootAuthenticationSessionPredicate.create(realmId))
|
||||||
.iterator();
|
.iterator();
|
||||||
|
|
||||||
while (itr.hasNext()) {
|
while (itr.hasNext()) {
|
||||||
|
@ -156,28 +146,20 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClientRemoved(RealmModel realm, ClientModel client) {
|
public void onClientRemoved(RealmModel realm, ClientModel client) {
|
||||||
// Send message to all DCs. The remoteCache will notify client listeners on all DCs for remove authentication sessions of this client
|
// No update anything on clientRemove for now. AuthenticationSessions of removed client will be handled at runtime if needed.
|
||||||
clusterEventsSenderTx.addEvent(
|
|
||||||
ClientRemovedSessionEvent.create(session, InfinispanAuthenticationSessionProviderFactory.CLIENT_REMOVED_AUTHSESSION_EVENT, realm.getId(), false, client.getId()),
|
// clusterEventsSenderTx.addEvent(
|
||||||
ClusterProvider.DCNotify.ALL_DCS);
|
// ClientRemovedSessionEvent.create(session, InfinispanAuthenticationSessionProviderFactory.CLIENT_REMOVED_AUTHSESSION_EVENT, realm.getId(), false, client.getId()),
|
||||||
|
// ClusterProvider.DCNotify.ALL_DCS);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void onClientRemovedEvent(String realmId, String clientUuid) {
|
protected void onClientRemovedEvent(String realmId, String clientUuid) {
|
||||||
Iterator<Map.Entry<String, AuthenticationSessionEntity>> itr = CacheDecorators.localCache(cache)
|
|
||||||
.entrySet()
|
|
||||||
.stream()
|
|
||||||
.filter(AuthenticationSessionPredicate.create(realmId).client(clientUuid))
|
|
||||||
.iterator();
|
|
||||||
|
|
||||||
while (itr.hasNext()) {
|
|
||||||
CacheDecorators.localCache(cache)
|
|
||||||
.remove(itr.next().getKey());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateNonlocalSessionAuthNotes(String authSessionId, Map<String, String> authNotesFragment) {
|
public void updateNonlocalSessionAuthNotes(String authSessionId, ClientModel client, Map<String, String> authNotesFragment) {
|
||||||
if (authSessionId == null) {
|
if (authSessionId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -185,18 +167,31 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||||
cluster.notify(
|
cluster.notify(
|
||||||
InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS,
|
InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS,
|
||||||
AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, authNotesFragment),
|
AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, client.getId(), authNotesFragment),
|
||||||
true,
|
true,
|
||||||
ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC
|
ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RootAuthenticationSessionModel getRootAuthenticationSession(RealmModel realm, String authenticationSessionId) {
|
||||||
|
RootAuthenticationSessionEntity entity = getRootAuthenticationSessionEntity(realm, authenticationSessionId);
|
||||||
|
return wrap(realm, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeRootAuthenticationSession(RealmModel realm, RootAuthenticationSessionModel authenticationSession) {
|
||||||
|
tx.remove(cache, authenticationSession.getId());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cache<String, AuthenticationSessionEntity> getCache() {
|
public Cache<String, RootAuthenticationSessionEntity> getCache() {
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
|
import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
||||||
|
import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.events.AbstractAuthSessionClusterListener;
|
import org.keycloak.models.sessions.infinispan.events.AbstractAuthSessionClusterListener;
|
||||||
import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
|
import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent;
|
||||||
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
|
import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent;
|
||||||
|
@ -46,7 +47,7 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
|
||||||
|
|
||||||
private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProviderFactory.class);
|
private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProviderFactory.class);
|
||||||
|
|
||||||
private volatile Cache<String, AuthenticationSessionEntity> authSessionsCache;
|
private volatile Cache<String, RootAuthenticationSessionEntity> authSessionsCache;
|
||||||
|
|
||||||
public static final String PROVIDER_ID = "infinispan";
|
public static final String PROVIDER_ID = "infinispan";
|
||||||
|
|
||||||
|
@ -113,11 +114,18 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationSessionAuthNoteUpdateEvent event = (AuthenticationSessionAuthNoteUpdateEvent) clEvent;
|
AuthenticationSessionAuthNoteUpdateEvent event = (AuthenticationSessionAuthNoteUpdateEvent) clEvent;
|
||||||
AuthenticationSessionEntity authSession = this.authSessionsCache.get(event.getAuthSessionId());
|
RootAuthenticationSessionEntity authSession = this.authSessionsCache.get(event.getAuthSessionId());
|
||||||
updateAuthSession(authSession, event.getAuthNotesFragment());
|
updateAuthSession(authSession, event.getClientUUID(), event.getAuthNotesFragment());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void updateAuthSession(AuthenticationSessionEntity authSession, Map<String, String> authNotesFragment) {
|
|
||||||
|
private static void updateAuthSession(RootAuthenticationSessionEntity rootAuthSession, String clientUUID, Map<String, String> authNotesFragment) {
|
||||||
|
if (rootAuthSession == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationSessionEntity authSession = rootAuthSession.getAuthenticationSessions().get(clientUUID);
|
||||||
|
|
||||||
if (authSession != null) {
|
if (authSession != null) {
|
||||||
if (authSession.getAuthNotes() == null) {
|
if (authSession.getAuthNotes() == null) {
|
||||||
authSession.setAuthNotes(new ConcurrentHashMap<>());
|
authSession.setAuthNotes(new ConcurrentHashMap<>());
|
||||||
|
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.models.sessions.infinispan;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
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;
|
||||||
|
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
||||||
|
import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class RootAuthenticationSessionAdapter implements RootAuthenticationSessionModel {
|
||||||
|
|
||||||
|
private KeycloakSession session;
|
||||||
|
private InfinispanAuthenticationSessionProvider provider;
|
||||||
|
private Cache<String, RootAuthenticationSessionEntity> cache;
|
||||||
|
private RealmModel realm;
|
||||||
|
private RootAuthenticationSessionEntity entity;
|
||||||
|
|
||||||
|
public RootAuthenticationSessionAdapter(KeycloakSession session, InfinispanAuthenticationSessionProvider provider,
|
||||||
|
Cache<String, RootAuthenticationSessionEntity> cache, RealmModel realm,
|
||||||
|
RootAuthenticationSessionEntity entity) {
|
||||||
|
this.session = session;
|
||||||
|
this.provider = provider;
|
||||||
|
this.cache = cache;
|
||||||
|
this.realm = realm;
|
||||||
|
this.entity = entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() {
|
||||||
|
provider.tx.replace(cache, entity.getId(), entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return entity.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RealmModel getRealm() {
|
||||||
|
return realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTimestamp() {
|
||||||
|
return entity.getTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTimestamp(int timestamp) {
|
||||||
|
entity.setTimestamp(timestamp);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, AuthenticationSessionModel> getAuthenticationSessions() {
|
||||||
|
Map<String, AuthenticationSessionModel> result = new HashMap<>();
|
||||||
|
|
||||||
|
for (Map.Entry<String, AuthenticationSessionEntity> entry : entity.getAuthenticationSessions().entrySet()) {
|
||||||
|
String clientUUID = entry.getKey();
|
||||||
|
result.put(clientUUID , new AuthenticationSessionAdapter(session, this, clientUUID, entry.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticationSessionModel getAuthenticationSession(ClientModel client) {
|
||||||
|
return client==null ? null : getAuthenticationSessions().get(client.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticationSessionModel createAuthenticationSession(ClientModel client) {
|
||||||
|
AuthenticationSessionEntity authSessionEntity = new AuthenticationSessionEntity();
|
||||||
|
entity.getAuthenticationSessions().put(client.getId(), authSessionEntity);
|
||||||
|
update();
|
||||||
|
|
||||||
|
return new AuthenticationSessionAdapter(session, this, client.getId(), authSessionEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void restartSession(RealmModel realm) {
|
||||||
|
entity.getAuthenticationSessions().clear();
|
||||||
|
entity.setTimestamp(Time.currentTime());
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,53 +17,34 @@
|
||||||
|
|
||||||
package org.keycloak.models.sessions.infinispan.entities;
|
package org.keycloak.models.sessions.infinispan.entities;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.io.Serializable;
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import org.infinispan.util.concurrent.ConcurrentHashSet;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class AuthenticationSessionEntity extends SessionEntity {
|
public class AuthenticationSessionEntity implements Serializable {
|
||||||
|
|
||||||
private String id;
|
|
||||||
|
|
||||||
private String clientUuid;
|
|
||||||
private String authUserId;
|
private String authUserId;
|
||||||
|
|
||||||
private String redirectUri;
|
private String redirectUri;
|
||||||
private int timestamp;
|
|
||||||
private String action;
|
private String action;
|
||||||
private Set<String> roles;
|
private Set<String> roles;
|
||||||
private Set<String> protocolMappers;
|
private Set<String> protocolMappers;
|
||||||
|
|
||||||
private Map<String, AuthenticationSessionModel.ExecutionStatus> executionStatus = new HashMap<>();
|
private Map<String, AuthenticationSessionModel.ExecutionStatus> executionStatus = new ConcurrentHashMap<>();
|
||||||
private String protocol;
|
private String protocol;
|
||||||
|
|
||||||
private Map<String, String> clientNotes;
|
private Map<String, String> clientNotes;
|
||||||
private Map<String, String> authNotes;
|
private Map<String, String> authNotes;
|
||||||
private Set<String> requiredActions = new HashSet<>();
|
private Set<String> requiredActions = new ConcurrentHashSet<>();
|
||||||
private Map<String, String> userSessionNotes;
|
private Map<String, String> userSessionNotes;
|
||||||
|
|
||||||
public String getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(String id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getClientUuid() {
|
|
||||||
return clientUuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setClientUuid(String clientUuid) {
|
|
||||||
this.clientUuid = clientUuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAuthUserId() {
|
public String getAuthUserId() {
|
||||||
return authUserId;
|
return authUserId;
|
||||||
}
|
}
|
||||||
|
@ -80,14 +61,6 @@ public class AuthenticationSessionEntity extends SessionEntity {
|
||||||
this.redirectUri = redirectUri;
|
this.redirectUri = redirectUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getTimestamp() {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimestamp(int timestamp) {
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAction() {
|
public String getAction() {
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
@ -160,25 +133,4 @@ public class AuthenticationSessionEntity extends SessionEntity {
|
||||||
this.authNotes = authNotes;
|
this.authNotes = authNotes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (this == o) return true;
|
|
||||||
if (!(o instanceof AuthenticationSessionEntity)) return false;
|
|
||||||
|
|
||||||
AuthenticationSessionEntity that = (AuthenticationSessionEntity) o;
|
|
||||||
|
|
||||||
if (id != null ? !id.equals(that.id) : that.id != null) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return id != null ? id.hashCode() : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return String.format("AuthenticationSessionEntity [id=%s, realm=%s, clientUuid=%s ]", getId(), getRealmId(), getClientUuid());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.models.sessions.infinispan.entities;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class RootAuthenticationSessionEntity extends SessionEntity {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private int timestamp;
|
||||||
|
private Map<String, AuthenticationSessionEntity> authenticationSessions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTimestamp() {
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTimestamp(int timestamp) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, AuthenticationSessionEntity> getAuthenticationSessions() {
|
||||||
|
return authenticationSessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthenticationSessions(Map<String, AuthenticationSessionEntity> authenticationSessions) {
|
||||||
|
this.authenticationSessions = authenticationSessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof RootAuthenticationSessionEntity)) return false;
|
||||||
|
|
||||||
|
RootAuthenticationSessionEntity that = (RootAuthenticationSessionEntity) o;
|
||||||
|
|
||||||
|
if (id != null ? !id.equals(that.id) : that.id != null) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return id != null ? id.hashCode() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("RootAuthenticationSessionEntity [ id=%s, realm=%s ]", getId(), getRealmId());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,146 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
|
||||||
* and other contributors as indicated by the @author tags.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.keycloak.models.sessions.infinispan.stream;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
|
||||||
import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.ObjectInput;
|
|
||||||
import java.io.ObjectOutput;
|
|
||||||
import org.infinispan.commons.marshall.Externalizer;
|
|
||||||
import org.infinispan.commons.marshall.MarshallUtil;
|
|
||||||
import org.infinispan.commons.marshall.SerializeWith;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
|
||||||
*/
|
|
||||||
@SerializeWith(AuthenticationSessionPredicate.ExternalizerImpl.class)
|
|
||||||
public class AuthenticationSessionPredicate implements Predicate<Map.Entry<String, AuthenticationSessionEntity>> {
|
|
||||||
|
|
||||||
private final String realm;
|
|
||||||
|
|
||||||
private String client;
|
|
||||||
|
|
||||||
private String user;
|
|
||||||
|
|
||||||
private Integer expired;
|
|
||||||
|
|
||||||
//private String brokerSessionId;
|
|
||||||
//private String brokerUserId;
|
|
||||||
|
|
||||||
private AuthenticationSessionPredicate(String realm) {
|
|
||||||
this.realm = realm;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static AuthenticationSessionPredicate create(String realm) {
|
|
||||||
return new AuthenticationSessionPredicate(realm);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthenticationSessionPredicate user(String user) {
|
|
||||||
this.user = user;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthenticationSessionPredicate client(String client) {
|
|
||||||
this.client = client;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthenticationSessionPredicate expired(Integer expired) {
|
|
||||||
this.expired = expired;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// public UserSessionPredicate brokerSessionId(String id) {
|
|
||||||
// this.brokerSessionId = id;
|
|
||||||
// return this;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public UserSessionPredicate brokerUserId(String id) {
|
|
||||||
// this.brokerUserId = id;
|
|
||||||
// return this;
|
|
||||||
// }
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean test(Map.Entry<String, AuthenticationSessionEntity> entry) {
|
|
||||||
AuthenticationSessionEntity entity = entry.getValue();
|
|
||||||
|
|
||||||
if (!realm.equals(entity.getRealmId())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user != null && !entity.getAuthUserId().equals(user)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (client != null && !entity.getClientUuid().equals(client)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (brokerSessionId != null && !brokerSessionId.equals(entity.getBrokerSessionId())) {
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (brokerUserId != null && !brokerUserId.equals(entity.getBrokerUserId())) {
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (expired != null && entity.getTimestamp() > expired) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class ExternalizerImpl implements Externalizer<AuthenticationSessionPredicate> {
|
|
||||||
|
|
||||||
private static final int VERSION_1 = 1;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeObject(ObjectOutput output, AuthenticationSessionPredicate obj) throws IOException {
|
|
||||||
output.writeByte(VERSION_1);
|
|
||||||
|
|
||||||
MarshallUtil.marshallString(obj.realm, output);
|
|
||||||
MarshallUtil.marshallString(obj.user, output);
|
|
||||||
MarshallUtil.marshallString(obj.client, output);
|
|
||||||
KeycloakMarshallUtil.marshall(obj.expired, output);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AuthenticationSessionPredicate readObject(ObjectInput input) throws IOException, ClassNotFoundException {
|
|
||||||
switch (input.readByte()) {
|
|
||||||
case VERSION_1:
|
|
||||||
return readObjectVersion1(input);
|
|
||||||
default:
|
|
||||||
throw new IOException("Unknown version");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthenticationSessionPredicate readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
|
|
||||||
AuthenticationSessionPredicate res = new AuthenticationSessionPredicate(MarshallUtil.unmarshallString(input));
|
|
||||||
res.user(MarshallUtil.unmarshallString(input));
|
|
||||||
res.client(MarshallUtil.unmarshallString(input));
|
|
||||||
res.expired(KeycloakMarshallUtil.unmarshallInteger(input));
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.models.sessions.infinispan.stream;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
|
||||||
|
import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInput;
|
||||||
|
import java.io.ObjectOutput;
|
||||||
|
import org.infinispan.commons.marshall.Externalizer;
|
||||||
|
import org.infinispan.commons.marshall.MarshallUtil;
|
||||||
|
import org.infinispan.commons.marshall.SerializeWith;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
@SerializeWith(RootAuthenticationSessionPredicate.ExternalizerImpl.class)
|
||||||
|
public class RootAuthenticationSessionPredicate implements Predicate<Map.Entry<String, RootAuthenticationSessionEntity>> {
|
||||||
|
|
||||||
|
private final String realm;
|
||||||
|
|
||||||
|
private Integer expired;
|
||||||
|
|
||||||
|
private RootAuthenticationSessionPredicate(String realm) {
|
||||||
|
this.realm = realm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RootAuthenticationSessionPredicate create(String realm) {
|
||||||
|
return new RootAuthenticationSessionPredicate(realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RootAuthenticationSessionPredicate expired(Integer expired) {
|
||||||
|
this.expired = expired;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean test(Map.Entry<String, RootAuthenticationSessionEntity> entry) {
|
||||||
|
RootAuthenticationSessionEntity entity = entry.getValue();
|
||||||
|
|
||||||
|
if (!realm.equals(entity.getRealmId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expired != null && entity.getTimestamp() > expired) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ExternalizerImpl implements Externalizer<RootAuthenticationSessionPredicate> {
|
||||||
|
|
||||||
|
private static final int VERSION_1 = 1;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeObject(ObjectOutput output, RootAuthenticationSessionPredicate obj) throws IOException {
|
||||||
|
output.writeByte(VERSION_1);
|
||||||
|
|
||||||
|
MarshallUtil.marshallString(obj.realm, output);
|
||||||
|
KeycloakMarshallUtil.marshall(obj.expired, output);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RootAuthenticationSessionPredicate readObject(ObjectInput input) throws IOException, ClassNotFoundException {
|
||||||
|
switch (input.readByte()) {
|
||||||
|
case VERSION_1:
|
||||||
|
return readObjectVersion1(input);
|
||||||
|
default:
|
||||||
|
throw new IOException("Unknown version");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RootAuthenticationSessionPredicate readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
|
||||||
|
RootAuthenticationSessionPredicate res = new RootAuthenticationSessionPredicate(MarshallUtil.unmarshallString(input));
|
||||||
|
res.expired(KeycloakMarshallUtil.unmarshallInteger(input));
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,6 +27,11 @@ import org.keycloak.sessions.CommonClientSessionModel;
|
||||||
*/
|
*/
|
||||||
public interface AuthenticatedClientSessionModel extends CommonClientSessionModel {
|
public interface AuthenticatedClientSessionModel extends CommonClientSessionModel {
|
||||||
|
|
||||||
|
String getId();
|
||||||
|
|
||||||
|
int getTimestamp();
|
||||||
|
void setTimestamp(int timestamp);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detaches the client session from its user session.
|
* Detaches the client session from its user session.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -21,7 +21,6 @@ import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.RealmModel;
|
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -31,10 +30,8 @@ import org.keycloak.models.UserModel;
|
||||||
*/
|
*/
|
||||||
public interface AuthenticationSessionModel extends CommonClientSessionModel {
|
public interface AuthenticationSessionModel extends CommonClientSessionModel {
|
||||||
|
|
||||||
//
|
|
||||||
// public UserSessionModel getUserSession();
|
|
||||||
// public void setUserSession(UserSessionModel userSession);
|
|
||||||
|
|
||||||
|
RootAuthenticationSessionModel getParentSession();
|
||||||
|
|
||||||
Map<String, ExecutionStatus> getExecutionStatus();
|
Map<String, ExecutionStatus> getExecutionStatus();
|
||||||
void setExecutionStatus(String authenticator, ExecutionStatus status);
|
void setExecutionStatus(String authenticator, ExecutionStatus status);
|
||||||
|
@ -125,8 +122,4 @@ public interface AuthenticationSessionModel extends CommonClientSessionModel {
|
||||||
*/
|
*/
|
||||||
void clearClientNotes();
|
void clearClientNotes();
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,26 +31,26 @@ public interface AuthenticationSessionProvider extends Provider {
|
||||||
* Creates and registers a new authentication session with random ID. Authentication session
|
* Creates and registers a new authentication session with random ID. Authentication session
|
||||||
* entity will be prefilled with current timestamp, the given realm and client.
|
* entity will be prefilled with current timestamp, the given realm and client.
|
||||||
*/
|
*/
|
||||||
AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client);
|
RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm);
|
||||||
|
|
||||||
AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client);
|
RootAuthenticationSessionModel createRootAuthenticationSession(String id, RealmModel realm);
|
||||||
|
|
||||||
AuthenticationSessionModel getAuthenticationSession(RealmModel realm, String authenticationSessionId);
|
RootAuthenticationSessionModel getRootAuthenticationSession(RealmModel realm, String authenticationSessionId);
|
||||||
|
|
||||||
void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authenticationSession);
|
void removeRootAuthenticationSession(RealmModel realm, RootAuthenticationSessionModel authenticationSession);
|
||||||
|
|
||||||
void removeExpired(RealmModel realm);
|
void removeExpired(RealmModel realm);
|
||||||
void onRealmRemoved(RealmModel realm);
|
void onRealmRemoved(RealmModel realm);
|
||||||
void onClientRemoved(RealmModel realm, ClientModel client);
|
void onClientRemoved(RealmModel realm, ClientModel client);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Requests update of authNotes of an authentication session that is not owned
|
* Requests update of authNotes of a root authentication session that is not owned
|
||||||
* by this instance but might exist somewhere in the cluster.
|
* by this instance but might exist somewhere in the cluster.
|
||||||
*
|
*
|
||||||
* @param authSessionId
|
* @param authSessionId
|
||||||
* @param authNotesFragment Map with authNote values. Auth note is removed if the corresponding value in the map is {@code null}.
|
* @param authNotesFragment Map with authNote values. Auth note is removed if the corresponding value in the map is {@code null}.
|
||||||
*/
|
*/
|
||||||
void updateNonlocalSessionAuthNotes(String authSessionId, Map<String, String> authNotesFragment);
|
void updateNonlocalSessionAuthNotes(String authSessionId, ClientModel client, Map<String, String> authNotesFragment);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,13 +33,9 @@ public interface CommonClientSessionModel {
|
||||||
public String getRedirectUri();
|
public String getRedirectUri();
|
||||||
public void setRedirectUri(String uri);
|
public void setRedirectUri(String uri);
|
||||||
|
|
||||||
public String getId();
|
|
||||||
public RealmModel getRealm();
|
public RealmModel getRealm();
|
||||||
public ClientModel getClient();
|
public ClientModel getClient();
|
||||||
|
|
||||||
public int getTimestamp();
|
|
||||||
public void setTimestamp(int timestamp);
|
|
||||||
|
|
||||||
public String getAction();
|
public String getAction();
|
||||||
public void setAction(String action);
|
public void setAction(String action);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2017 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.sessions;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents usually one browser session with potentially many browser tabs. Every browser tab is represented by {@link AuthenticationSessionModel}
|
||||||
|
* of different client.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public interface RootAuthenticationSessionModel {
|
||||||
|
|
||||||
|
String getId();
|
||||||
|
RealmModel getRealm();
|
||||||
|
|
||||||
|
int getTimestamp();
|
||||||
|
void setTimestamp(int timestamp);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key is client UUID, Value is AuthenticationSessionModel for particular client
|
||||||
|
* @return authentication sessions or empty map if no authenticationSessions presents. Never return null.
|
||||||
|
*/
|
||||||
|
Map<String, AuthenticationSessionModel> getAuthenticationSessions();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return authentication session for particular client or null if it doesn't yet exists.
|
||||||
|
*/
|
||||||
|
AuthenticationSessionModel getAuthenticationSession(ClientModel client);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new authentication session and returns it. Overwrites existing session for particular client if already exists.
|
||||||
|
*
|
||||||
|
* @param client
|
||||||
|
* @return non-null fresh authentication session
|
||||||
|
*/
|
||||||
|
AuthenticationSessionModel createAuthenticationSession(ClientModel client);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will completely restart whole state of authentication session. It will just keep same ID. It will setup it with provided realm.
|
||||||
|
*/
|
||||||
|
void restartSession(RealmModel realm);
|
||||||
|
|
||||||
|
}
|
|
@ -56,6 +56,7 @@ import org.keycloak.services.util.CacheControlUtil;
|
||||||
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.CommonClientSessionModel;
|
import org.keycloak.sessions.CommonClientSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
@ -222,7 +223,7 @@ public class AuthenticationProcessor {
|
||||||
|
|
||||||
public String generateCode() {
|
public String generateCode() {
|
||||||
ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getAuthenticationSession());
|
ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getAuthenticationSession());
|
||||||
authenticationSession.setTimestamp(Time.currentTime());
|
authenticationSession.getParentSession().setTimestamp(Time.currentTime());
|
||||||
return accessCode.getOrGenerateCode();
|
return accessCode.getOrGenerateCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -632,7 +633,10 @@ public class AuthenticationProcessor {
|
||||||
|
|
||||||
} else if (e.getError() == AuthenticationFlowError.FORK_FLOW) {
|
} else if (e.getError() == AuthenticationFlowError.FORK_FLOW) {
|
||||||
ForkFlowException reset = (ForkFlowException)e;
|
ForkFlowException reset = (ForkFlowException)e;
|
||||||
AuthenticationSessionModel clone = clone(session, authenticationSession);
|
|
||||||
|
RootAuthenticationSessionModel rootClone = clone(session, authenticationSession.getClient(), authenticationSession.getParentSession());
|
||||||
|
AuthenticationSessionModel clone = rootClone.getAuthenticationSession(authenticationSession.getClient());
|
||||||
|
|
||||||
clone.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
clone.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||||
setAuthenticationSession(clone);
|
setAuthenticationSession(clone);
|
||||||
|
|
||||||
|
@ -748,7 +752,7 @@ public class AuthenticationProcessor {
|
||||||
|
|
||||||
public static void resetFlow(AuthenticationSessionModel authSession, String flowPath) {
|
public static void resetFlow(AuthenticationSessionModel authSession, String flowPath) {
|
||||||
logger.debug("RESET FLOW");
|
logger.debug("RESET FLOW");
|
||||||
authSession.setTimestamp(Time.currentTime());
|
authSession.getParentSession().setTimestamp(Time.currentTime());
|
||||||
authSession.setAuthenticatedUser(null);
|
authSession.setAuthenticatedUser(null);
|
||||||
authSession.clearExecutionStatus();
|
authSession.clearExecutionStatus();
|
||||||
authSession.clearUserSessionNotes();
|
authSession.clearUserSessionNotes();
|
||||||
|
@ -759,20 +763,26 @@ public class AuthenticationProcessor {
|
||||||
authSession.setAuthNote(CURRENT_FLOW_PATH, flowPath);
|
authSession.setAuthNote(CURRENT_FLOW_PATH, flowPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AuthenticationSessionModel clone(KeycloakSession session, AuthenticationSessionModel authSession) {
|
public static RootAuthenticationSessionModel clone(KeycloakSession session, ClientModel client, RootAuthenticationSessionModel authSession) {
|
||||||
AuthenticationSessionModel clone = new AuthenticationSessionManager(session).createAuthenticationSession(authSession.getRealm(), authSession.getClient(), true);
|
RootAuthenticationSessionModel clone = new AuthenticationSessionManager(session).createAuthenticationSession(authSession.getRealm(), true);
|
||||||
|
|
||||||
// Transfer just the client "notes", but not "authNotes"
|
// Transfer just the client "notes", but not "authNotes"
|
||||||
for (Map.Entry<String, String> entry : authSession.getClientNotes().entrySet()) {
|
for (Map.Entry<String, AuthenticationSessionModel> entry : authSession.getAuthenticationSessions().entrySet()) {
|
||||||
clone.setClientNote(entry.getKey(), entry.getValue());
|
AuthenticationSessionModel asmOrig = entry.getValue();
|
||||||
|
AuthenticationSessionModel asmClone = clone.createAuthenticationSession(asmOrig.getClient());
|
||||||
|
|
||||||
|
asmClone.setRedirectUri(asmOrig.getRedirectUri());
|
||||||
|
asmClone.setProtocol(asmOrig.getProtocol());
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> clientNote : asmOrig.getClientNotes().entrySet()) {
|
||||||
|
asmClone.setClientNote(clientNote.getKey(), clientNote.getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clone.setRedirectUri(authSession.getRedirectUri());
|
|
||||||
clone.setProtocol(authSession.getProtocol());
|
|
||||||
clone.setTimestamp(Time.currentTime());
|
clone.setTimestamp(Time.currentTime());
|
||||||
|
|
||||||
clone.setAuthNote(FORKED_FROM, authSession.getId());
|
clone.getAuthenticationSession(client).setAuthNote(FORKED_FROM, authSession.getId());
|
||||||
logger.debugf("Forked authSession %s from authSession %s", clone.getId(), authSession.getId());
|
logger.debugf("Forked authSession %s from authSession %s . Client: '%s'", clone.getId(), authSession.getId(), client.getClientId());
|
||||||
|
|
||||||
return clone;
|
return clone;
|
||||||
|
|
||||||
|
@ -825,7 +835,8 @@ public class AuthenticationProcessor {
|
||||||
if (!code.isActionActive(ClientSessionCode.ActionType.LOGIN)) {
|
if (!code.isActionActive(ClientSessionCode.ActionType.LOGIN)) {
|
||||||
throw new AuthenticationFlowException(AuthenticationFlowError.EXPIRED_CODE);
|
throw new AuthenticationFlowException(AuthenticationFlowError.EXPIRED_CODE);
|
||||||
}
|
}
|
||||||
authenticationSession.setTimestamp(Time.currentTime());
|
|
||||||
|
authenticationSession.getParentSession().setTimestamp(Time.currentTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Response authenticateOnly() throws AuthenticationFlowException {
|
public Response authenticateOnly() throws AuthenticationFlowException {
|
||||||
|
@ -872,9 +883,9 @@ public class AuthenticationProcessor {
|
||||||
|
|
||||||
if (userSession == null) { // if no authenticator attached a usersession
|
if (userSession == null) { // if no authenticator attached a usersession
|
||||||
|
|
||||||
userSession = session.sessions().getUserSession(realm, authSession.getId());
|
userSession = session.sessions().getUserSession(realm, authSession.getParentSession().getId());
|
||||||
if (userSession == null) {
|
if (userSession == null) {
|
||||||
userSession = session.sessions().createUserSession(authSession.getId(), realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol()
|
userSession = session.sessions().createUserSession(authSession.getParentSession().getId(), realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol()
|
||||||
, remember, brokerSessionId, brokerUserId);
|
, remember, brokerSessionId, brokerUserId);
|
||||||
} else if (userSession.getUser() == null || !AuthenticationManager.isSessionValid(realm, userSession)) {
|
} else if (userSession.getUser() == null || !AuthenticationManager.isSessionValid(realm, userSession)) {
|
||||||
userSession.restartSession(realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol()
|
userSession.restartSession(realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol()
|
||||||
|
@ -936,7 +947,7 @@ public class AuthenticationProcessor {
|
||||||
if (nextRequiredAction != null) {
|
if (nextRequiredAction != null) {
|
||||||
return AuthenticationManager.redirectToRequiredActions(session, realm, authenticationSession, uriInfo, nextRequiredAction);
|
return AuthenticationManager.redirectToRequiredActions(session, realm, authenticationSession, uriInfo, nextRequiredAction);
|
||||||
} else {
|
} 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
|
event.detail(Details.CODE_ID, authenticationSession.getParentSession().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.
|
// the user has successfully logged in and we can clear his/her previous login failure attempts.
|
||||||
logSuccess();
|
logSuccess();
|
||||||
return AuthenticationManager.finishedRequiredActions(session, authenticationSession, userSession, connection, request, uriInfo, event);
|
return AuthenticationManager.finishedRequiredActions(session, authenticationSession, userSession, connection, request, uriInfo, event);
|
||||||
|
|
|
@ -149,7 +149,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
|
||||||
@Override
|
@Override
|
||||||
public String generateCode() {
|
public String generateCode() {
|
||||||
ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, getRealm(), getAuthenticationSession());
|
ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, getRealm(), getAuthenticationSession());
|
||||||
authenticationSession.setTimestamp(Time.currentTime());
|
authenticationSession.getParentSession().setTimestamp(Time.currentTime());
|
||||||
return accessCode.getOrGenerateCode();
|
return accessCode.getOrGenerateCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilderException;
|
import javax.ws.rs.core.UriBuilderException;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -111,7 +112,9 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
||||||
// set up the account service as the endpoint to call.
|
// set up the account service as the endpoint to call.
|
||||||
ClientModel client = realm.getClientByClientId(clientId == null ? Constants.ACCOUNT_MANAGEMENT_CLIENT_ID : clientId);
|
ClientModel client = realm.getClientByClientId(clientId == null ? Constants.ACCOUNT_MANAGEMENT_CLIENT_ID : clientId);
|
||||||
|
|
||||||
authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
|
RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, true);
|
||||||
|
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||||
|
|
||||||
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||||
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
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();
|
||||||
|
|
|
@ -75,8 +75,8 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander<
|
||||||
final KeycloakSession session = tokenContext.getSession();
|
final KeycloakSession session = tokenContext.getSession();
|
||||||
if (tokenContext.isAuthenticationSessionFresh()) {
|
if (tokenContext.isAuthenticationSessionFresh()) {
|
||||||
// Update the authentication session in the token
|
// Update the authentication session in the token
|
||||||
token.setAuthenticationSessionId(authSession.getId());
|
token.setAuthenticationSessionId(authSession.getParentSession().getId());
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
|
||||||
String confirmUri = builder.build(realm.getName()).toString();
|
String confirmUri = builder.build(realm.getName()).toString();
|
||||||
|
|
||||||
return session.getProvider(LoginFormsProvider.class)
|
return session.getProvider(LoginFormsProvider.class)
|
||||||
|
|
|
@ -31,6 +31,7 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
|
||||||
private static final String JSON_FIELD_IDENTITY_PROVIDER_USERNAME = "idpu";
|
private static final String JSON_FIELD_IDENTITY_PROVIDER_USERNAME = "idpu";
|
||||||
private static final String JSON_FIELD_IDENTITY_PROVIDER_ALIAS = "idpa";
|
private static final String JSON_FIELD_IDENTITY_PROVIDER_ALIAS = "idpa";
|
||||||
private static final String JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID = "oasid";
|
private static final String JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID = "oasid";
|
||||||
|
private static final String JSON_FIELD_ORIGINAL_CLIENT_UUID = "ocid";
|
||||||
|
|
||||||
@JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME)
|
@JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME)
|
||||||
private String identityProviderUsername;
|
private String identityProviderUsername;
|
||||||
|
@ -41,9 +42,13 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
|
||||||
@JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
|
@JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
|
||||||
private String originalAuthenticationSessionId;
|
private String originalAuthenticationSessionId;
|
||||||
|
|
||||||
public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId,
|
@JsonProperty(value = JSON_FIELD_ORIGINAL_CLIENT_UUID)
|
||||||
|
private String originalClientUUID;
|
||||||
|
|
||||||
|
public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String clientUUID,
|
||||||
String identityProviderUsername, String identityProviderAlias) {
|
String identityProviderUsername, String identityProviderAlias) {
|
||||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
|
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
|
||||||
|
this.originalClientUUID = clientUUID;
|
||||||
this.identityProviderUsername = identityProviderUsername;
|
this.identityProviderUsername = identityProviderUsername;
|
||||||
this.identityProviderAlias = identityProviderAlias;
|
this.identityProviderAlias = identityProviderAlias;
|
||||||
}
|
}
|
||||||
|
@ -74,4 +79,12 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
|
||||||
public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) {
|
public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) {
|
||||||
this.originalAuthenticationSessionId = originalAuthenticationSessionId;
|
this.originalAuthenticationSessionId = originalAuthenticationSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getOriginalClientUUID() {
|
||||||
|
return originalClientUUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOriginalClientUUID(String originalClientUUID) {
|
||||||
|
this.originalClientUUID = originalClientUUID;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.keycloak.authentication.actiontoken.*;
|
||||||
import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticator;
|
import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticator;
|
||||||
import org.keycloak.events.*;
|
import org.keycloak.events.*;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
@ -31,7 +32,7 @@ import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.AuthenticationSessionProvider;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
|
@ -76,8 +77,8 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
|
||||||
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
|
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
|
||||||
if (tokenContext.isAuthenticationSessionFresh()) {
|
if (tokenContext.isAuthenticationSessionFresh()) {
|
||||||
token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
|
token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
|
||||||
token.setAuthenticationSessionId(authSession.getId());
|
token.setAuthenticationSessionId(authSession.getParentSession().getId());
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
|
||||||
String confirmUri = builder.build(realm.getName()).toString();
|
String confirmUri = builder.build(realm.getName()).toString();
|
||||||
|
|
||||||
return session.getProvider(LoginFormsProvider.class)
|
return session.getProvider(LoginFormsProvider.class)
|
||||||
|
@ -94,14 +95,16 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
|
||||||
AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
||||||
asm.removeAuthenticationSession(realm, authSession, true);
|
asm.removeAuthenticationSession(realm, authSession, true);
|
||||||
|
|
||||||
AuthenticationSessionProvider authSessProvider = session.authenticationSessions();
|
ClientModel originalClient = realm.getClientById(token.getOriginalClientUUID());
|
||||||
authSession = authSessProvider.getAuthenticationSession(realm, token.getOriginalAuthenticationSessionId());
|
authSession = asm.getAuthenticationSessionByIdAndClient(realm, token.getOriginalAuthenticationSessionId(), originalClient);
|
||||||
|
|
||||||
if (authSession != null) {
|
if (authSession != null) {
|
||||||
authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
|
authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
|
||||||
} else {
|
} else {
|
||||||
authSessProvider.updateNonlocalSessionAuthNotes(
|
|
||||||
|
session.authenticationSessions().updateNonlocalSessionAuthNotes(
|
||||||
token.getAuthenticationSessionId(),
|
token.getAuthenticationSessionId(),
|
||||||
|
originalClient,
|
||||||
Collections.singletonMap(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername())
|
Collections.singletonMap(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,8 +77,8 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
|
||||||
if (tokenContext.isAuthenticationSessionFresh()) {
|
if (tokenContext.isAuthenticationSessionFresh()) {
|
||||||
// Update the authentication session in the token
|
// Update the authentication session in the token
|
||||||
token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
|
token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
|
||||||
token.setAuthenticationSessionId(authSession.getId());
|
token.setAuthenticationSessionId(authSession.getParentSession().getId());
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
|
||||||
String confirmUri = builder.build(realm.getName()).toString();
|
String confirmUri = builder.build(realm.getName()).toString();
|
||||||
|
|
||||||
return session.getProvider(LoginFormsProvider.class)
|
return session.getProvider(LoginFormsProvider.class)
|
||||||
|
|
|
@ -123,18 +123,17 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
|
||||||
.user(existingUser)
|
.user(existingUser)
|
||||||
.detail(Details.USERNAME, existingUser.getUsername())
|
.detail(Details.USERNAME, existingUser.getUsername())
|
||||||
.detail(Details.EMAIL, existingUser.getEmail())
|
.detail(Details.EMAIL, existingUser.getEmail())
|
||||||
.detail(Details.CODE_ID, authSession.getId())
|
.detail(Details.CODE_ID, authSession.getParentSession().getId())
|
||||||
.removeDetail(Details.AUTH_METHOD)
|
.removeDetail(Details.AUTH_METHOD)
|
||||||
.removeDetail(Details.AUTH_TYPE);
|
.removeDetail(Details.AUTH_TYPE);
|
||||||
|
|
||||||
IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
|
IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
|
||||||
existingUser.getId(), absoluteExpirationInSecs, authSession.getId(),
|
existingUser.getId(), absoluteExpirationInSecs, authSession.getParentSession().getId(), authSession.getClient().getId(),
|
||||||
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
|
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
|
||||||
);
|
);
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
|
||||||
String link = builder
|
String link = builder
|
||||||
.queryParam(Constants.EXECUTION, context.getExecution().getId())
|
.queryParam(Constants.EXECUTION, context.getExecution().getId())
|
||||||
.queryParam(Constants.CLIENT_ID, context.getExecution().getId())
|
|
||||||
.build(realm.getName()).toString();
|
.build(realm.getName()).toString();
|
||||||
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
||||||
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
||||||
|
|
||||||
// We send the secret in the email in a link as a query param.
|
// We send the secret in the email in a link as a query param.
|
||||||
ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, authenticationSession.getId());
|
ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, authenticationSession.getParentSession().getId());
|
||||||
String link = UriBuilder
|
String link = UriBuilder
|
||||||
.fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), context.getRealm(), context.getUriInfo())))
|
.fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), context.getRealm(), context.getUriInfo())))
|
||||||
.build()
|
.build()
|
||||||
|
@ -101,7 +101,7 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
||||||
event.clone().event(EventType.SEND_RESET_PASSWORD)
|
event.clone().event(EventType.SEND_RESET_PASSWORD)
|
||||||
.user(user)
|
.user(user)
|
||||||
.detail(Details.USERNAME, username)
|
.detail(Details.USERNAME, username)
|
||||||
.detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, authenticationSession.getId()).success();
|
.detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, authenticationSession.getParentSession().getId()).success();
|
||||||
context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT));
|
context.forkWithSuccessMessage(new FormMessage(Messages.EMAIL_SENT));
|
||||||
} catch (EmailException e) {
|
} catch (EmailException e) {
|
||||||
event.clone().event(EventType.SEND_RESET_PASSWORD)
|
event.clone().event(EventType.SEND_RESET_PASSWORD)
|
||||||
|
|
|
@ -134,8 +134,8 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
|
||||||
int validityInSecs = realm.getActionTokenGeneratedByUserLifespan(VerifyEmailActionToken.TOKEN_TYPE);
|
int validityInSecs = realm.getActionTokenGeneratedByUserLifespan(VerifyEmailActionToken.TOKEN_TYPE);
|
||||||
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
||||||
|
|
||||||
VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSession.getId(), user.getEmail());
|
VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSession.getParentSession().getId(), user.getEmail());
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
|
||||||
String link = builder.build(realm.getName()).toString();
|
String link = builder.build(realm.getName()).toString();
|
||||||
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
||||||
|
|
||||||
|
|
|
@ -236,7 +236,8 @@ public class PolicyEvaluationService {
|
||||||
ClientModel clientModel = realm.getClientById(clientId);
|
ClientModel clientModel = realm.getClientById(clientId);
|
||||||
String id = KeycloakModelUtils.generateId();
|
String id = KeycloakModelUtils.generateId();
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = keycloakSession.authenticationSessions().createAuthenticationSession(id, realm, clientModel);
|
AuthenticationSessionModel authSession = keycloakSession.authenticationSessions().createRootAuthenticationSession(id, realm)
|
||||||
|
.createAuthenticationSession(clientModel);
|
||||||
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
authSession.setAuthenticatedUser(userModel);
|
authSession.setAuthenticatedUser(userModel);
|
||||||
userSession = keycloakSession.sessions().createUserSession(id, realm, userModel, userModel.getUsername(), "127.0.0.1", "passwd", false, null, null);
|
userSession = keycloakSession.sessions().createUserSession(id, realm, userModel, userModel.getUsername(), "127.0.0.1", "passwd", false, null, null);
|
||||||
|
|
|
@ -40,6 +40,7 @@ import org.keycloak.services.resources.LoginActionsService;
|
||||||
import org.keycloak.services.util.CacheControlUtil;
|
import org.keycloak.services.util.CacheControlUtil;
|
||||||
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
@ -108,7 +109,7 @@ public abstract class AuthorizationEndpointBase {
|
||||||
AuthenticationFlowModel flow = getAuthenticationFlow();
|
AuthenticationFlowModel flow = getAuthenticationFlow();
|
||||||
String flowId = flow.getId();
|
String flowId = flow.getId();
|
||||||
AuthenticationProcessor processor = createProcessor(authSession, flowId, LoginActionsService.AUTHENTICATE_PATH);
|
AuthenticationProcessor processor = createProcessor(authSession, flowId, LoginActionsService.AUTHENTICATE_PATH);
|
||||||
event.detail(Details.CODE_ID, authSession.getId());
|
event.detail(Details.CODE_ID, authSession.getParentSession().getId());
|
||||||
if (isPassive) {
|
if (isPassive) {
|
||||||
// OIDC prompt == NONE or SAML 2 IsPassive flag
|
// OIDC prompt == NONE or SAML 2 IsPassive flag
|
||||||
// This means that client is just checking if the user is already completely logged in.
|
// This means that client is just checking if the user is already completely logged in.
|
||||||
|
@ -168,15 +169,21 @@ public abstract class AuthorizationEndpointBase {
|
||||||
protected AuthorizationEndpointChecks getOrCreateAuthenticationSession(ClientModel client, String requestState) {
|
protected AuthorizationEndpointChecks getOrCreateAuthenticationSession(ClientModel client, String requestState) {
|
||||||
AuthenticationSessionManager manager = new AuthenticationSessionManager(session);
|
AuthenticationSessionManager manager = new AuthenticationSessionManager(session);
|
||||||
String authSessionId = manager.getCurrentAuthenticationSessionId(realm);
|
String authSessionId = manager.getCurrentAuthenticationSessionId(realm);
|
||||||
AuthenticationSessionModel authSession = authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
|
RootAuthenticationSessionModel rootAuthSession = authSessionId==null ? null : session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||||
|
AuthenticationSessionModel authSession;
|
||||||
|
|
||||||
|
if (rootAuthSession != null) {
|
||||||
|
|
||||||
|
authSession = rootAuthSession.getAuthenticationSession(client);
|
||||||
|
|
||||||
if (authSession != null) {
|
if (authSession != null) {
|
||||||
|
|
||||||
ClientSessionCode<AuthenticationSessionModel> check = new ClientSessionCode<>(session, realm, authSession);
|
ClientSessionCode<AuthenticationSessionModel> check = new ClientSessionCode<>(session, realm, authSession);
|
||||||
if (!check.isActionActive(ClientSessionCode.ActionType.LOGIN)) {
|
if (!check.isActionActive(ClientSessionCode.ActionType.LOGIN)) {
|
||||||
|
|
||||||
logger.debugf("Authentication session '%s' exists, but is expired. Restart existing authentication session", authSession.getId());
|
logger.debugf("Authentication session '%s' exists, but is expired. Restart existing authentication session", rootAuthSession.getId());
|
||||||
authSession.restartSession(realm, client);
|
rootAuthSession.restartSession(realm);
|
||||||
|
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||||
|
|
||||||
return new AuthorizationEndpointChecks(authSession);
|
return new AuthorizationEndpointChecks(authSession);
|
||||||
|
|
||||||
} else if (isNewRequest(authSession, client, requestState)) {
|
} else if (isNewRequest(authSession, client, requestState)) {
|
||||||
|
@ -184,12 +191,15 @@ public abstract class AuthorizationEndpointBase {
|
||||||
// Otherwise update just client information from the AuthorizationEndpoint request.
|
// Otherwise update just client information from the AuthorizationEndpoint request.
|
||||||
// This difference is needed, because of logout from JS applications in multiple browser tabs.
|
// This difference is needed, because of logout from JS applications in multiple browser tabs.
|
||||||
if (shouldRestartAuthSession(authSession)) {
|
if (shouldRestartAuthSession(authSession)) {
|
||||||
logger.debug("New request from application received, but authentication session already exists. Restart existing authentication session");
|
logger.debugf("New request from application received, but authentication session '%s' already exists and has client '%s'. Restart child authentication session for client.",
|
||||||
authSession.restartSession(realm, client);
|
rootAuthSession.getId(), client.getClientId());
|
||||||
|
|
||||||
|
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
logger.debug("New request from application received, but authentication session already exists. Update client information in existing authentication session");
|
logger.debugf("New request from application received, but authentication session '%s' already exists and has client '%s'. Update client information in existing authentication session.",
|
||||||
authSession.clearClientNotes(); // update client data
|
rootAuthSession.getId(), client.getClientId());
|
||||||
authSession.updateClient(client);
|
authSession.clearClientNotes();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AuthorizationEndpointChecks(authSession);
|
return new AuthorizationEndpointChecks(authSession);
|
||||||
|
@ -208,16 +218,25 @@ public abstract class AuthorizationEndpointBase {
|
||||||
return new AuthorizationEndpointChecks(response);
|
return new AuthorizationEndpointChecks(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
logger.debugf("Sent request to authz endpoint. Authentication session with ID '%s' exists, but doesn't have client: '%s' . Adding client to authentication session",
|
||||||
|
rootAuthSession.getId(), client.getClientId());
|
||||||
|
|
||||||
|
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||||
|
return new AuthorizationEndpointChecks(authSession);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UserSessionModel userSession = authSessionId==null ? null : new UserSessionCrossDCManager(session).getUserSessionIfExistsRemotely(realm, authSessionId);
|
UserSessionModel userSession = authSessionId==null ? null : new UserSessionCrossDCManager(session).getUserSessionIfExistsRemotely(realm, authSessionId);
|
||||||
|
|
||||||
if (userSession != null) {
|
if (userSession != null) {
|
||||||
logger.debugf("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);
|
logger.debugf("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);
|
rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(authSessionId, realm);
|
||||||
|
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||||
} else {
|
} else {
|
||||||
authSession = manager.createAuthenticationSession(realm, client, true);
|
rootAuthSession = manager.createAuthenticationSession(realm, true);
|
||||||
logger.debugf("Sent request to authz endpoint. Created new authentication session with ID '%s'", authSession.getId());
|
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||||
|
logger.debugf("Sent request to authz endpoint. Created new authentication session with ID '%s'", rootAuthSession.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AuthorizationEndpointChecks(authSession);
|
return new AuthorizationEndpointChecks(authSession);
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
import org.keycloak.services.util.CookieHelper;
|
import org.keycloak.services.util.CookieHelper;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.ws.rs.core.Cookie;
|
import javax.ws.rs.core.Cookie;
|
||||||
|
@ -143,7 +144,8 @@ public class RestartLoginCookie {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static AuthenticationSessionModel restartSession(KeycloakSession session, RealmModel realm) throws Exception {
|
public static AuthenticationSessionModel restartSession(KeycloakSession session, RealmModel realm,
|
||||||
|
RootAuthenticationSessionModel rootSession, String expectedClientId) throws Exception {
|
||||||
Cookie cook = session.getContext().getRequestHeaders().getCookies().get(KC_RESTART);
|
Cookie cook = session.getContext().getRequestHeaders().getCookies().get(KC_RESTART);
|
||||||
if (cook == null) {
|
if (cook == null) {
|
||||||
logger.debug("KC_RESTART cookie doesn't exist");
|
logger.debug("KC_RESTART cookie doesn't exist");
|
||||||
|
@ -161,7 +163,18 @@ public class RestartLoginCookie {
|
||||||
ClientModel client = realm.getClientByClientId(cookie.getClientId());
|
ClientModel client = realm.getClientByClientId(cookie.getClientId());
|
||||||
if (client == null) return null;
|
if (client == null) return null;
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
|
// Restart just if client from cookie matches client from the URL.
|
||||||
|
if (!client.getClientId().equals(expectedClientId)) {
|
||||||
|
logger.debugf("Skip restarting from the KC_RESTART. Clients doesn't match: Cookie client: %s, Requested client: %s", client.getClientId(), expectedClientId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Need to create brand new session and setup cookie
|
||||||
|
if (rootSession == null) {
|
||||||
|
rootSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationSessionModel authSession = rootSession.createAuthenticationSession(client);
|
||||||
authSession.setProtocol(cookie.getAuthMethod());
|
authSession.setProtocol(cookie.getAuthMethod());
|
||||||
authSession.setRedirectUri(cookie.getRedirectUri());
|
authSession.setRedirectUri(cookie.getRedirectUri());
|
||||||
authSession.setAction(cookie.getAction());
|
authSession.setAction(cookie.getAction());
|
||||||
|
|
|
@ -80,6 +80,7 @@ import org.keycloak.services.resources.admin.AdminAuth;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.services.validation.Validation;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
import org.keycloak.util.TokenUtil;
|
import org.keycloak.util.TokenUtil;
|
||||||
import org.keycloak.utils.ProfileHelper;
|
import org.keycloak.utils.ProfileHelper;
|
||||||
|
|
||||||
|
@ -255,7 +256,7 @@ public class TokenEndpoint {
|
||||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
|
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, session, realm, event, AuthenticatedClientSessionModel.class);
|
ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, session, realm, client, event, AuthenticatedClientSessionModel.class);
|
||||||
if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) {
|
if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) {
|
||||||
AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
|
AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
|
||||||
|
|
||||||
|
@ -469,7 +470,9 @@ public class TokenEndpoint {
|
||||||
}
|
}
|
||||||
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
|
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, false);
|
RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false);
|
||||||
|
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client);
|
||||||
|
|
||||||
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
authSession.setAction(AuthenticatedClientSessionModel.Action.AUTHENTICATE.name());
|
authSession.setAction(AuthenticatedClientSessionModel.Action.AUTHENTICATE.name());
|
||||||
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
||||||
|
@ -553,13 +556,16 @@ public class TokenEndpoint {
|
||||||
|
|
||||||
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
|
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, false);
|
RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false);
|
||||||
|
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client);
|
||||||
|
|
||||||
authSession.setAuthenticatedUser(clientUser);
|
authSession.setAuthenticatedUser(clientUser);
|
||||||
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
||||||
authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
|
authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope);
|
||||||
|
|
||||||
UserSessionModel userSession = session.sessions().createUserSession(authSession.getId(), realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
|
UserSessionModel userSession = session.sessions().createUserSession(authSession.getParentSession().getId(), realm, clientUser, clientUsername,
|
||||||
|
clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null);
|
||||||
event.session(userSession);
|
event.session(userSession);
|
||||||
|
|
||||||
AuthenticationManager.setRolesAndMappersInSession(authSession);
|
AuthenticationManager.setRolesAndMappersInSession(authSession);
|
||||||
|
@ -763,7 +769,9 @@ public class TokenEndpoint {
|
||||||
|
|
||||||
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
|
String scope = formParams.getFirst(OAuth2Constants.SCOPE);
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, targetClient, false);
|
RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false);
|
||||||
|
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(targetClient);
|
||||||
|
|
||||||
authSession.setAuthenticatedUser(targetUser);
|
authSession.setAuthenticatedUser(targetUser);
|
||||||
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
|
||||||
|
|
|
@ -182,9 +182,11 @@ public class Urls {
|
||||||
return loginResetCredentialsBuilder(baseUri).build(realmName);
|
return loginResetCredentialsBuilder(baseUri).build(realmName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString) {
|
public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString, String clientId) {
|
||||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken")
|
return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken")
|
||||||
.queryParam("key", tokenString);
|
.queryParam("key", tokenString)
|
||||||
|
.queryParam(Constants.CLIENT_ID, clientId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UriBuilder loginResetCredentialsBuilder(URI baseUri) {
|
public static UriBuilder loginResetCredentialsBuilder(URI baseUri) {
|
||||||
|
|
|
@ -56,6 +56,7 @@ import org.keycloak.services.util.CookieHelper;
|
||||||
import org.keycloak.services.util.P3PHelper;
|
import org.keycloak.services.util.P3PHelper;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.CommonClientSessionModel;
|
import org.keycloak.sessions.CommonClientSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.ws.rs.core.Cookie;
|
import javax.ws.rs.core.Cookie;
|
||||||
|
@ -215,16 +216,20 @@ public class AuthenticationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AuthenticationSessionModel createOrJoinLogoutSession(RealmModel realm, final AuthenticationSessionManager asm, boolean browserCookie) {
|
private static AuthenticationSessionModel createOrJoinLogoutSession(RealmModel realm, final AuthenticationSessionManager asm, boolean browserCookie) {
|
||||||
|
// Account management client is used as a placeholder
|
||||||
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
||||||
AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm);
|
|
||||||
|
AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm, client);
|
||||||
// Try to join existing logout session if it exists and browser session is required
|
// Try to join existing logout session if it exists and browser session is required
|
||||||
if (browserCookie && logoutAuthSession != null) {
|
if (browserCookie && logoutAuthSession != null) {
|
||||||
if (Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), logoutAuthSession.getAction())) {
|
if (Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), logoutAuthSession.getAction())) {
|
||||||
return logoutAuthSession;
|
return logoutAuthSession;
|
||||||
}
|
}
|
||||||
logoutAuthSession.restartSession(realm, client);
|
// Re-create the authentication session for logout
|
||||||
|
logoutAuthSession = logoutAuthSession.getParentSession().createAuthenticationSession(client);
|
||||||
} else {
|
} else {
|
||||||
logoutAuthSession = asm.createAuthenticationSession(realm, client, browserCookie);
|
RootAuthenticationSessionModel rootLogoutSession = asm.createAuthenticationSession(realm, browserCookie);
|
||||||
|
logoutAuthSession = rootLogoutSession.createAuthenticationSession(client);
|
||||||
}
|
}
|
||||||
logoutAuthSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name());
|
logoutAuthSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name());
|
||||||
return logoutAuthSession;
|
return logoutAuthSession;
|
||||||
|
@ -381,8 +386,8 @@ public class AuthenticationManager {
|
||||||
/**
|
/**
|
||||||
* Sets logout state of the particular client into the {@code logoutAuthSession}
|
* Sets logout state of the particular client into the {@code logoutAuthSession}
|
||||||
* @param logoutAuthSession logoutAuthSession. May be {@code null} in which case this is a no-op.
|
* @param logoutAuthSession logoutAuthSession. May be {@code null} in which case this is a no-op.
|
||||||
* @param client Client. Must not be {@code null}
|
* @param clientUuid Client. Must not be {@code null}
|
||||||
* @param state
|
* @param action
|
||||||
*/
|
*/
|
||||||
public static void setClientLogoutAction(AuthenticationSessionModel logoutAuthSession, String clientUuid, AuthenticationSessionModel.Action action) {
|
public static void setClientLogoutAction(AuthenticationSessionModel logoutAuthSession, String clientUuid, AuthenticationSessionModel.Action action) {
|
||||||
if (logoutAuthSession != null && clientUuid != null) {
|
if (logoutAuthSession != null && clientUuid != null) {
|
||||||
|
@ -479,8 +484,11 @@ public class AuthenticationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Response finishBrowserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
|
public static Response finishBrowserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
|
||||||
|
// Account management client is used as a placeholder
|
||||||
|
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
||||||
|
|
||||||
final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
||||||
AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm);
|
AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm, client);
|
||||||
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
|
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
|
||||||
|
|
||||||
expireIdentityCookie(realm, uriInfo, connection);
|
expireIdentityCookie(realm, uriInfo, connection);
|
||||||
|
@ -832,7 +840,7 @@ public class AuthenticationManager {
|
||||||
|
|
||||||
logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired());
|
logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired());
|
||||||
|
|
||||||
event.detail(Details.CODE_ID, authSession.getId());
|
event.detail(Details.CODE_ID, authSession.getParentSession().getId());
|
||||||
|
|
||||||
Set<String> requiredActions = user.getRequiredActions();
|
Set<String> requiredActions = user.getRequiredActions();
|
||||||
Response action = executionActions(session, authSession, request, event, realm, user, requiredActions);
|
Response action = executionActions(session, authSession, request, event, realm, user, requiredActions);
|
||||||
|
|
|
@ -28,6 +28,7 @@ import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.protocol.RestartLoginCookie;
|
import org.keycloak.protocol.RestartLoginCookie;
|
||||||
import org.keycloak.services.util.CookieHelper;
|
import org.keycloak.services.util.CookieHelper;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.StickySessionEncoderProvider;
|
import org.keycloak.sessions.StickySessionEncoderProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,22 +46,22 @@ public class AuthenticationSessionManager {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a fresh authentication session for the given realm and client. Optionally sets the browser
|
* Creates a fresh authentication session for the given realm . Optionally sets the browser
|
||||||
* authentication session cookie {@link #AUTH_SESSION_ID} with the ID of the new session.
|
* authentication session cookie {@link #AUTH_SESSION_ID} with the ID of the new session.
|
||||||
* @param realm
|
* @param realm
|
||||||
* @param client
|
|
||||||
* @param browserCookie Set the cookie in the browser for the
|
* @param browserCookie Set the cookie in the browser for the
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client, boolean browserCookie) {
|
public RootAuthenticationSessionModel createAuthenticationSession(RealmModel realm, boolean browserCookie) {
|
||||||
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client);
|
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(realm);
|
||||||
|
|
||||||
if (browserCookie) {
|
if (browserCookie) {
|
||||||
setAuthSessionCookie(authSession.getId(), realm);
|
setAuthSessionCookie(rootAuthSession.getId(), realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
return authSession;
|
return rootAuthSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -73,14 +74,20 @@ public class AuthenticationSessionManager {
|
||||||
return getAuthSessionCookieDecoded(realm);
|
return getAuthSessionCookieDecoded(realm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns current authentication session if it exists, otherwise returns {@code null}.
|
* Returns current authentication session if it exists, otherwise returns {@code null}.
|
||||||
* @param realm
|
* @param realm
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm) {
|
public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm, ClientModel client) {
|
||||||
String authSessionId = getAuthSessionCookieDecoded(realm);
|
String authSessionId = getAuthSessionCookieDecoded(realm);
|
||||||
return authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
|
|
||||||
|
if (authSessionId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getAuthenticationSessionByIdAndClient(realm, authSessionId, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -124,8 +131,10 @@ public class AuthenticationSessionManager {
|
||||||
|
|
||||||
|
|
||||||
public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authSession, boolean expireRestartCookie) {
|
public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authSession, boolean expireRestartCookie) {
|
||||||
log.debugf("Removing authSession '%s'. Expire restart cookie: %b", authSession.getId(), expireRestartCookie);
|
RootAuthenticationSessionModel rootAuthSession = authSession.getParentSession();
|
||||||
session.authenticationSessions().removeAuthenticationSession(realm, authSession);
|
|
||||||
|
log.debugf("Removing authSession '%s'. Expire restart cookie: %b", rootAuthSession.getId(), expireRestartCookie);
|
||||||
|
session.authenticationSessions().removeRootAuthenticationSession(realm, rootAuthSession);
|
||||||
|
|
||||||
// expire restart cookie
|
// expire restart cookie
|
||||||
if (expireRestartCookie) {
|
if (expireRestartCookie) {
|
||||||
|
@ -138,7 +147,14 @@ public class AuthenticationSessionManager {
|
||||||
|
|
||||||
// Check to see if we already have authenticationSession with same ID
|
// Check to see if we already have authenticationSession with same ID
|
||||||
public UserSessionModel getUserSession(AuthenticationSessionModel authSession) {
|
public UserSessionModel getUserSession(AuthenticationSessionModel authSession) {
|
||||||
return session.sessions().getUserSession(authSession.getRealm(), authSession.getId());
|
return session.sessions().getUserSession(authSession.getRealm(), authSession.getParentSession().getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Don't look at cookie. Just lookup authentication session based on the ID and client. Return null if not found
|
||||||
|
public AuthenticationSessionModel getAuthenticationSessionByIdAndClient(RealmModel realm, String authSessionId, ClientModel client) {
|
||||||
|
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||||
|
return rootAuthSession==null ? null : rootAuthSession.getAuthenticationSession(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,8 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <CLIENT_SESSION extends CommonClientSessionModel> ParseResult<CLIENT_SESSION> parseResult(String code, KeycloakSession session, RealmModel realm, EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
|
public static <CLIENT_SESSION extends CommonClientSessionModel> ParseResult<CLIENT_SESSION> parseResult(String code, KeycloakSession session, RealmModel realm, ClientModel client,
|
||||||
|
EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
|
||||||
ParseResult<CLIENT_SESSION> result = new ParseResult<>();
|
ParseResult<CLIENT_SESSION> result = new ParseResult<>();
|
||||||
if (code == null) {
|
if (code == null) {
|
||||||
result.illegalHash = true;
|
result.illegalHash = true;
|
||||||
|
@ -88,7 +89,7 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
|
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
|
||||||
result.clientSession = getClientSession(code, session, realm, event, clientSessionParser);
|
result.clientSession = getClientSession(code, session, realm, client, event, clientSessionParser);
|
||||||
if (result.clientSession == null) {
|
if (result.clientSession == null) {
|
||||||
result.authSessionNotFound = true;
|
result.authSessionNotFound = true;
|
||||||
return result;
|
return result;
|
||||||
|
@ -113,15 +114,16 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
|
public static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, ClientModel client,
|
||||||
|
EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
|
||||||
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
|
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
|
||||||
return getClientSession(code, session, realm, event, clientSessionParser);
|
return getClientSession(code, session, realm, client, event, clientSessionParser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event,
|
private static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event,
|
||||||
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser) {
|
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser) {
|
||||||
return clientSessionParser.parseSession(code, session, realm, event);
|
return clientSessionParser.parseSession(code, session, realm, client, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -135,7 +137,8 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isActionActive(ActionType actionType) {
|
public boolean isActionActive(ActionType actionType) {
|
||||||
int timestamp = commonLoginSession.getTimestamp();
|
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = (CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION>) CodeGenerateUtil.getParser(commonLoginSession.getClass());
|
||||||
|
int timestamp = clientSessionParser.getTimestamp(commonLoginSession);
|
||||||
|
|
||||||
int lifespan;
|
int lifespan;
|
||||||
switch (actionType) {
|
switch (actionType) {
|
||||||
|
@ -210,7 +213,9 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
||||||
|
|
||||||
public void setAction(String action) {
|
public void setAction(String action) {
|
||||||
commonLoginSession.setAction(action);
|
commonLoginSession.setAction(action);
|
||||||
commonLoginSession.setTimestamp(Time.currentTime());
|
|
||||||
|
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = (CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION>) CodeGenerateUtil.getParser(commonLoginSession.getClass());
|
||||||
|
clientSessionParser.setTimestamp(commonLoginSession, Time.currentTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getOrGenerateCode() {
|
public String getOrGenerateCode() {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.jose.jwe.JWEException;
|
import org.keycloak.jose.jwe.JWEException;
|
||||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.CodeToTokenStoreProvider;
|
import org.keycloak.models.CodeToTokenStoreProvider;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
@ -78,7 +79,7 @@ class CodeGenerateUtil {
|
||||||
|
|
||||||
interface ClientSessionParser<CS extends CommonClientSessionModel> {
|
interface ClientSessionParser<CS extends CommonClientSessionModel> {
|
||||||
|
|
||||||
CS parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event);
|
CS parseSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event);
|
||||||
|
|
||||||
String retrieveCode(KeycloakSession session, CS clientSession);
|
String retrieveCode(KeycloakSession session, CS clientSession);
|
||||||
|
|
||||||
|
@ -88,6 +89,9 @@ class CodeGenerateUtil {
|
||||||
|
|
||||||
boolean isExpired(KeycloakSession session, String code, CS clientSession);
|
boolean isExpired(KeycloakSession session, String code, CS clientSession);
|
||||||
|
|
||||||
|
int getTimestamp(CS clientSession);
|
||||||
|
void setTimestamp(CS clientSession, int timestamp);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -97,9 +101,9 @@ class CodeGenerateUtil {
|
||||||
private static class AuthenticationSessionModelParser implements ClientSessionParser<AuthenticationSessionModel> {
|
private static class AuthenticationSessionModelParser implements ClientSessionParser<AuthenticationSessionModel> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event) {
|
public AuthenticationSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event) {
|
||||||
// Read authSessionID from cookie. Code is ignored for now
|
// Read authSessionID from cookie. Code is ignored for now
|
||||||
return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm);
|
return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -141,6 +145,16 @@ class CodeGenerateUtil {
|
||||||
public boolean isExpired(KeycloakSession session, String code, AuthenticationSessionModel clientSession) {
|
public boolean isExpired(KeycloakSession session, String code, AuthenticationSessionModel clientSession) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTimestamp(AuthenticationSessionModel clientSession) {
|
||||||
|
return clientSession.getParentSession().getTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTimestamp(AuthenticationSessionModel clientSession, int timestamp) {
|
||||||
|
clientSession.getParentSession().setTimestamp(timestamp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,7 +163,7 @@ class CodeGenerateUtil {
|
||||||
private CodeJWT codeJWT;
|
private CodeJWT codeJWT;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticatedClientSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event) {
|
public AuthenticatedClientSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event) {
|
||||||
SecretKey aesKey = session.keys().getActiveAesKey(realm).getSecretKey();
|
SecretKey aesKey = session.keys().getActiveAesKey(realm).getSecretKey();
|
||||||
SecretKey hmacKey = session.keys().getActiveHmacKey(realm).getSecretKey();
|
SecretKey hmacKey = session.keys().getActiveHmacKey(realm).getSecretKey();
|
||||||
|
|
||||||
|
@ -241,6 +255,16 @@ class CodeGenerateUtil {
|
||||||
public boolean isExpired(KeycloakSession session, String code, AuthenticatedClientSessionModel clientSession) {
|
public boolean isExpired(KeycloakSession session, String code, AuthenticatedClientSessionModel clientSession) {
|
||||||
return !codeJWT.isActive();
|
return !codeJWT.isActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTimestamp(AuthenticatedClientSessionModel clientSession) {
|
||||||
|
return clientSession.getTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTimestamp(AuthenticatedClientSessionModel clientSession, int timestamp) {
|
||||||
|
clientSession.setTimestamp(timestamp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -292,7 +292,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
|
|
||||||
// Create AuthenticationSessionModel with same ID like userSession and refresh cookie
|
// Create AuthenticationSessionModel with same ID like userSession and refresh cookie
|
||||||
UserSessionModel userSession = cookieResult.getSession();
|
UserSessionModel userSession = cookieResult.getSession();
|
||||||
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(userSession.getId(), realmModel, client);
|
AuthenticationSessionModel authSession = session.authenticationSessions().createRootAuthenticationSession(userSession.getId(), realmModel).createAuthenticationSession(client);
|
||||||
new AuthenticationSessionManager(session).setAuthSessionCookie(userSession.getId(), realmModel);
|
new AuthenticationSessionManager(session).setAuthSessionCookie(userSession.getId(), realmModel);
|
||||||
|
|
||||||
ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
|
ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
|
||||||
|
@ -588,7 +588,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
AuthenticationSessionModel authSession = clientSessionCode.getClientSession();
|
AuthenticationSessionModel authSession = clientSessionCode.getClientSession();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.event.detail(Details.CODE_ID, authSession.getId())
|
this.event.detail(Details.CODE_ID, authSession.getParentSession().getId())
|
||||||
.removeDetail("auth_method");
|
.removeDetail("auth_method");
|
||||||
|
|
||||||
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
|
SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE);
|
||||||
|
@ -685,7 +685,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
|
|
||||||
logger.debugf("Redirect to postBrokerLogin flow after authentication with identityProvider '%s'.", context.getIdpConfig().getAlias());
|
logger.debugf("Redirect to postBrokerLogin flow after authentication with identityProvider '%s'.", context.getIdpConfig().getAlias());
|
||||||
|
|
||||||
authSession.setTimestamp(Time.currentTime());
|
authSession.getParentSession().setTimestamp(Time.currentTime());
|
||||||
|
|
||||||
SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context);
|
SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context);
|
||||||
ctx.saveToAuthenticationSession(authSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
|
ctx.saveToAuthenticationSession(authSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT);
|
||||||
|
@ -789,7 +789,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
if (nextRequiredAction != null) {
|
if (nextRequiredAction != null) {
|
||||||
return AuthenticationManager.redirectToRequiredActions(session, realmModel, authSession, uriInfo, nextRequiredAction);
|
return AuthenticationManager.redirectToRequiredActions(session, realmModel, authSession, uriInfo, nextRequiredAction);
|
||||||
} else {
|
} else {
|
||||||
event.detail(Details.CODE_ID, authSession.getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set
|
event.detail(Details.CODE_ID, authSession.getParentSession().getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set
|
||||||
return AuthenticationManager.finishedRequiredActions(session, authSession, null, clientConnection, request, uriInfo, event);
|
return AuthenticationManager.finishedRequiredActions(session, authSession, null, clientConnection, request, uriInfo, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,7 @@ import org.keycloak.services.util.CacheControlUtil;
|
||||||
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
||||||
import org.keycloak.services.util.BrowserHistoryHelper;
|
import org.keycloak.services.util.BrowserHistoryHelper;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
@ -333,7 +334,8 @@ public class LoginActionsService {
|
||||||
public Response resetCredentialsGET(@QueryParam("code") String code,
|
public Response resetCredentialsGET(@QueryParam("code") String code,
|
||||||
@QueryParam("execution") String execution,
|
@QueryParam("execution") String execution,
|
||||||
@QueryParam("client_id") String clientId) {
|
@QueryParam("client_id") String clientId) {
|
||||||
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm);
|
ClientModel client = realm.getClientByClientId(clientId);
|
||||||
|
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client);
|
||||||
|
|
||||||
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
|
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
|
||||||
if (authSession == null && code == null) {
|
if (authSession == null && code == null) {
|
||||||
|
@ -357,7 +359,10 @@ public class LoginActionsService {
|
||||||
|
|
||||||
// set up the account service as the endpoint to call.
|
// set up the account service as the endpoint to call.
|
||||||
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
||||||
authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true);
|
|
||||||
|
RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, true);
|
||||||
|
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||||
|
|
||||||
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||||
//authSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
|
//authSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
|
||||||
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
|
@ -413,9 +418,8 @@ public class LoginActionsService {
|
||||||
ActionTokenContext<T> tokenContext;
|
ActionTokenContext<T> tokenContext;
|
||||||
String eventError = null;
|
String eventError = null;
|
||||||
String defaultErrorMessage = null;
|
String defaultErrorMessage = null;
|
||||||
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm);
|
|
||||||
|
|
||||||
event.event(EventType.EXECUTE_ACTION_TOKEN);
|
AuthenticationSessionModel authSession = null;
|
||||||
|
|
||||||
// Setup client, so error page will contain "back to application" link
|
// Setup client, so error page will contain "back to application" link
|
||||||
ClientModel client = null;
|
ClientModel client = null;
|
||||||
|
@ -424,8 +428,11 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
session.getContext().setClient(client);
|
session.getContext().setClient(client);
|
||||||
|
authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.event(EventType.EXECUTE_ACTION_TOKEN);
|
||||||
|
|
||||||
// First resolve action token handler
|
// First resolve action token handler
|
||||||
try {
|
try {
|
||||||
if (tokenString == null) {
|
if (tokenString == null) {
|
||||||
|
@ -500,7 +507,7 @@ public class LoginActionsService {
|
||||||
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
|
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
|
||||||
tokenContext.setAuthenticationSession(authSession, true);
|
tokenContext.setAuthenticationSession(authSession, true);
|
||||||
} else if (tokenAuthSessionId == null ||
|
} else if (tokenAuthSessionId == null ||
|
||||||
! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, tokenAuthSessionId)) {
|
! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, tokenAuthSessionId, client)) {
|
||||||
// There exists an authentication session but no auth session ID was received in the action token
|
// There exists an authentication session but no auth session ID was received in the action token
|
||||||
logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId());
|
logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId());
|
||||||
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
|
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
|
||||||
|
@ -737,7 +744,7 @@ public class LoginActionsService {
|
||||||
|
|
||||||
public static Response redirectToAfterBrokerLoginEndpoint(KeycloakSession session, RealmModel realm, UriInfo uriInfo, AuthenticationSessionModel authSession, boolean firstBrokerLogin) {
|
public static Response redirectToAfterBrokerLoginEndpoint(KeycloakSession session, RealmModel realm, UriInfo uriInfo, AuthenticationSessionModel authSession, boolean firstBrokerLogin) {
|
||||||
ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authSession);
|
ClientSessionCode<AuthenticationSessionModel> accessCode = new ClientSessionCode<>(session, realm, authSession);
|
||||||
authSession.setTimestamp(Time.currentTime());
|
authSession.getParentSession().setTimestamp(Time.currentTime());
|
||||||
|
|
||||||
String clientId = authSession.getClient().getClientId();
|
String clientId = authSession.getClient().getClientId();
|
||||||
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId) :
|
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId) :
|
||||||
|
@ -816,7 +823,7 @@ public class LoginActionsService {
|
||||||
OIDCResponseMode responseMode = OIDCResponseMode.parse(respMode, OIDCResponseType.parse(responseType));
|
OIDCResponseMode responseMode = OIDCResponseMode.parse(respMode, OIDCResponseType.parse(responseType));
|
||||||
|
|
||||||
event.event(EventType.LOGIN).client(authSession.getClient())
|
event.event(EventType.LOGIN).client(authSession.getClient())
|
||||||
.detail(Details.CODE_ID, authSession.getId())
|
.detail(Details.CODE_ID, authSession.getParentSession().getId())
|
||||||
.detail(Details.REDIRECT_URI, authSession.getRedirectUri())
|
.detail(Details.REDIRECT_URI, authSession.getRedirectUri())
|
||||||
.detail(Details.AUTH_METHOD, authSession.getProtocol())
|
.detail(Details.AUTH_METHOD, authSession.getProtocol())
|
||||||
.detail(Details.RESPONSE_TYPE, responseType)
|
.detail(Details.RESPONSE_TYPE, responseType)
|
||||||
|
|
|
@ -35,6 +35,8 @@ import org.keycloak.sessions.CommonClientSessionModel.Action;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @author hmlnarik
|
* @author hmlnarik
|
||||||
|
@ -75,7 +77,6 @@ public class LoginActionsServiceChecks {
|
||||||
* If there is an action required in the session, furthermore it is not the expected one, and the required
|
* If there is an action required in the session, furthermore it is not the expected one, and the required
|
||||||
* action is redirection to "required actions", it throws with response performing the redirect to required
|
* action is redirection to "required actions", it throws with response performing the redirect to required
|
||||||
* actions.
|
* actions.
|
||||||
* @param <T>
|
|
||||||
*/
|
*/
|
||||||
public static class IsActionRequired implements Predicate<JsonWebToken> {
|
public static class IsActionRequired implements Predicate<JsonWebToken> {
|
||||||
|
|
||||||
|
@ -250,7 +251,7 @@ public class LoginActionsServiceChecks {
|
||||||
*
|
*
|
||||||
* @param <T>
|
* @param <T>
|
||||||
*/
|
*/
|
||||||
public static <T extends JsonWebToken> boolean doesAuthenticationSessionFromCookieMatchOneFromToken(ActionTokenContext<T> context, String authSessionIdFromToken) throws VerificationException {
|
public static <T extends JsonWebToken> boolean doesAuthenticationSessionFromCookieMatchOneFromToken(ActionTokenContext<T> context, String authSessionIdFromToken, ClientModel client) throws VerificationException {
|
||||||
if (authSessionIdFromToken == null) {
|
if (authSessionIdFromToken == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -262,9 +263,8 @@ public class LoginActionsServiceChecks {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationSessionModel authSessionFromCookie = context.getSession()
|
AuthenticationSessionModel authSessionFromCookie = asm.getAuthenticationSessionByIdAndClient(context.getRealm(), authSessionIdFromCookie, client);
|
||||||
.authenticationSessions().getAuthenticationSession(context.getRealm(), authSessionIdFromCookie);
|
if (authSessionFromCookie == null) { // Not our client in root session
|
||||||
if (authSessionFromCookie == null) { // Cookie contains ID of expired auth session
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,14 +278,16 @@ public class LoginActionsServiceChecks {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationSessionModel authSessionFromParent = context.getSession()
|
AuthenticationSessionModel authSessionFromParent = asm.getAuthenticationSessionByIdAndClient(context.getRealm(), parentSessionId, client);
|
||||||
.authenticationSessions().getAuthenticationSession(context.getRealm(), parentSessionId);
|
if (authSessionFromParent == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// It's the correct browser. Let's remove forked session as we won't continue
|
// It's the correct browser. Let's remove forked session as we won't continue
|
||||||
// from the login form (browser flow) but from the token's flow
|
// from the login form (browser flow) but from the token's flow
|
||||||
// Don't expire KC_RESTART cookie at this point
|
// Don't expire KC_RESTART cookie at this point
|
||||||
asm.removeAuthenticationSession(context.getRealm(), authSessionFromCookie, false);
|
asm.removeAuthenticationSession(context.getRealm(), authSessionFromCookie, false);
|
||||||
LOG.debugf("Removed forked session: %s", authSessionFromCookie.getId());
|
LOG.debugf("Removed forked session: %s", authSessionFromCookie.getParentSession().getId());
|
||||||
|
|
||||||
// Refresh browser cookie
|
// Refresh browser cookie
|
||||||
asm.setAuthSessionCookie(parentSessionId, context.getRealm());
|
asm.setAuthSessionCookie(parentSessionId, context.getRealm());
|
||||||
|
|
|
@ -47,6 +47,7 @@ import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.util.BrowserHistoryHelper;
|
import org.keycloak.services.util.BrowserHistoryHelper;
|
||||||
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
|
|
||||||
public class SessionCodeChecks {
|
public class SessionCodeChecks {
|
||||||
|
@ -132,12 +133,6 @@ public class SessionCodeChecks {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// object retrieve
|
|
||||||
AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(code, session, realm, event, AuthenticationSessionModel.class);
|
|
||||||
if (authSession != null) {
|
|
||||||
return authSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup client to be shown on error/info page based on "client_id" parameter
|
// Setup client to be shown on error/info page based on "client_id" parameter
|
||||||
logger.debugf("Will use client '%s' in back-to-application link", clientId);
|
logger.debugf("Will use client '%s' in back-to-application link", clientId);
|
||||||
ClientModel client = null;
|
ClientModel client = null;
|
||||||
|
@ -148,8 +143,16 @@ public class SessionCodeChecks {
|
||||||
session.getContext().setClient(client);
|
session.getContext().setClient(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// object retrieve
|
||||||
|
AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session);
|
||||||
|
AuthenticationSessionModel authSession = authSessionManager.getCurrentAuthenticationSession(realm, client);
|
||||||
|
if (authSession != null) {
|
||||||
|
return authSession;
|
||||||
|
}
|
||||||
|
|
||||||
// See if we are already authenticated and userSession with same ID exists.
|
// See if we are already authenticated and userSession with same ID exists.
|
||||||
String sessionId = new AuthenticationSessionManager(session).getCurrentAuthenticationSessionId(realm);
|
String sessionId = authSessionManager.getCurrentAuthenticationSessionId(realm);
|
||||||
|
RootAuthenticationSessionModel existingRootAuthSession = null;
|
||||||
if (sessionId != null) {
|
if (sessionId != null) {
|
||||||
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
|
UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId);
|
||||||
if (userSession != null) {
|
if (userSession != null) {
|
||||||
|
@ -164,10 +167,13 @@ public class SessionCodeChecks {
|
||||||
response = loginForm.createInfoPage();
|
response = loginForm.createInfoPage();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
existingRootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise just try to restart from the cookie
|
// Otherwise just try to restart from the cookie
|
||||||
response = restartAuthenticationSessionFromCookie();
|
response = restartAuthenticationSessionFromCookie(existingRootAuthSession);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,7 +192,7 @@ public class SessionCodeChecks {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client checks
|
// Client checks
|
||||||
event.detail(Details.CODE_ID, authSession.getId());
|
event.detail(Details.CODE_ID, authSession.getParentSession().getId());
|
||||||
ClientModel client = authSession.getClient();
|
ClientModel client = authSession.getClient();
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
event.error(Errors.CLIENT_NOT_FOUND);
|
event.error(Errors.CLIENT_NOT_FOUND);
|
||||||
|
@ -240,7 +246,7 @@ public class SessionCodeChecks {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, session, realm, event, AuthenticationSessionModel.class);
|
ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, session, realm, client, event, AuthenticationSessionModel.class);
|
||||||
clientCode = result.getCode();
|
clientCode = result.getCode();
|
||||||
if (clientCode == null) {
|
if (clientCode == null) {
|
||||||
|
|
||||||
|
@ -341,11 +347,12 @@ public class SessionCodeChecks {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Response restartAuthenticationSessionFromCookie() {
|
private Response restartAuthenticationSessionFromCookie(RootAuthenticationSessionModel existingRootSession) {
|
||||||
logger.debug("Authentication session not found. Trying to restart from cookie.");
|
logger.debug("Authentication session not found. Trying to restart from cookie.");
|
||||||
AuthenticationSessionModel authSession = null;
|
AuthenticationSessionModel authSession = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
authSession = RestartLoginCookie.restartSession(session, realm);
|
authSession = RestartLoginCookie.restartSession(session, realm, existingRootSession, clientId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
|
ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@ import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.managers.AppAuthManager;
|
import org.keycloak.services.managers.AppAuthManager;
|
||||||
import org.keycloak.services.managers.Auth;
|
import org.keycloak.services.managers.Auth;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
import org.keycloak.services.managers.UserSessionManager;
|
import org.keycloak.services.managers.UserSessionManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.AbstractSecuredLocalService;
|
import org.keycloak.services.resources.AbstractSecuredLocalService;
|
||||||
|
@ -60,6 +61,7 @@ import org.keycloak.services.resources.RealmsResource;
|
||||||
import org.keycloak.services.util.ResolveRelative;
|
import org.keycloak.services.util.ResolveRelative;
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.services.validation.Validation;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
import org.keycloak.storage.ReadOnlyException;
|
import org.keycloak.storage.ReadOnlyException;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
@ -179,7 +181,7 @@ public class AccountFormService extends AbstractSecuredLocalService {
|
||||||
setReferrerOnPage();
|
setReferrerOnPage();
|
||||||
|
|
||||||
UserSessionModel userSession = auth.getSession();
|
UserSessionModel userSession = auth.getSession();
|
||||||
AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, userSession.getId());
|
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getAuthenticationSessionByIdAndClient(realm, userSession.getId(), client);
|
||||||
if (authSession != null) {
|
if (authSession != null) {
|
||||||
String forwardedError = authSession.getAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
|
String forwardedError = authSession.getAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
|
||||||
if (forwardedError != null) {
|
if (forwardedError != null) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
import org.keycloak.broker.provider.IdentityBrokerException;
|
import org.keycloak.broker.provider.IdentityBrokerException;
|
||||||
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
|
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
|
||||||
import org.keycloak.broker.provider.IdentityProvider;
|
import org.keycloak.broker.provider.IdentityProvider;
|
||||||
|
import org.keycloak.broker.provider.util.IdentityBrokerState;
|
||||||
import org.keycloak.broker.social.SocialIdentityProvider;
|
import org.keycloak.broker.social.SocialIdentityProvider;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
|
@ -197,7 +198,9 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
||||||
|
|
||||||
twitter.setOAuthConsumer(getConfig().getClientId(), getConfig().getClientSecret());
|
twitter.setOAuthConsumer(getConfig().getClientId(), getConfig().getClientSecret());
|
||||||
|
|
||||||
authSession = ClientSessionCode.getClientSession(state, session, realm, event, AuthenticationSessionModel.class);
|
String clientId = IdentityBrokerState.encoded(state).getClientId();
|
||||||
|
ClientModel client = realm.getClientByClientId(clientId);
|
||||||
|
authSession = ClientSessionCode.getClientSession(state, session, realm, client, event, AuthenticationSessionModel.class);
|
||||||
|
|
||||||
String twitterToken = authSession.getAuthNote(TWITTER_TOKEN);
|
String twitterToken = authSession.getAuthNote(TWITTER_TOKEN);
|
||||||
String twitterSecret = authSession.getAuthNote(TWITTER_TOKENSECRET);
|
String twitterSecret = authSession.getAuthNote(TWITTER_TOKENSECRET);
|
||||||
|
|
|
@ -56,13 +56,14 @@ public class MailUtils {
|
||||||
assertEquals("text/html; charset=UTF-8", htmlContentType);
|
assertEquals("text/html; charset=UTF-8", htmlContentType);
|
||||||
|
|
||||||
final String htmlBody = (String) multipart.getBodyPart(1).getContent();
|
final String htmlBody = (String) multipart.getBodyPart(1).getContent();
|
||||||
|
final String htmlChangePwdUrl = MailUtils.getLink(htmlBody);
|
||||||
// .replace() accounts for escaping the ampersand
|
// .replace() accounts for escaping the ampersand
|
||||||
// It's not escaped in the html version because html retrieved from a
|
// It's not escaped in the html version because html retrieved from a
|
||||||
// message bundle is considered safe and it must be unescaped to display
|
// message bundle is considered safe and it must be unescaped to display
|
||||||
// properly.
|
// properly.
|
||||||
final String htmlChangePwdUrl = MailUtils.getLink(htmlBody).replace("&", "&");
|
final String htmlChangePwdUrlToCompare = htmlChangePwdUrl.replace("&", "&");
|
||||||
|
|
||||||
assertEquals(htmlChangePwdUrl, textChangePwdUrl);
|
assertEquals(htmlChangePwdUrlToCompare, textChangePwdUrl);
|
||||||
|
|
||||||
return htmlChangePwdUrl;
|
return htmlChangePwdUrl;
|
||||||
}
|
}
|
||||||
|
|
|
@ -578,25 +578,7 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
||||||
|
|
||||||
|
|
||||||
public static String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
|
public static String getPasswordResetEmailLink(MimeMessage message) throws IOException, MessagingException {
|
||||||
Multipart multipart = (Multipart) message.getContent();
|
return MailUtils.getPasswordResetEmailLink(message);
|
||||||
|
|
||||||
final String textContentType = multipart.getBodyPart(0).getContentType();
|
|
||||||
|
|
||||||
assertEquals("text/plain; charset=UTF-8", textContentType);
|
|
||||||
|
|
||||||
final String textBody = (String) multipart.getBodyPart(0).getContent();
|
|
||||||
final String textChangePwdUrl = MailUtils.getLink(textBody);
|
|
||||||
|
|
||||||
final String htmlContentType = multipart.getBodyPart(1).getContentType();
|
|
||||||
|
|
||||||
assertEquals("text/html; charset=UTF-8", htmlContentType);
|
|
||||||
|
|
||||||
final String htmlBody = (String) multipart.getBodyPart(1).getContent();
|
|
||||||
final String htmlChangePwdUrl = MailUtils.getLink(htmlBody);
|
|
||||||
|
|
||||||
assertEquals(htmlChangePwdUrl, textChangePwdUrl);
|
|
||||||
|
|
||||||
return htmlChangePwdUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,8 +159,6 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// TODO:mposolda Disable periodic cleaner now on all Keycloak nodes. Make sure it's re-enabled after finish
|
|
||||||
|
|
||||||
// Ensure to remove all current sessions and offline sessions
|
// Ensure to remove all current sessions and offline sessions
|
||||||
setTimeOffset(10000000);
|
setTimeOffset(10000000);
|
||||||
getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
|
getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
|
||||||
|
@ -281,8 +279,6 @@ public class LastSessionRefreshCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
|
|
||||||
) throws Exception {
|
) throws Exception {
|
||||||
|
|
||||||
// TODO:mposolda Disable periodic cleaner now on all Keycloak nodes. Make sure it's re-enabled after finish
|
|
||||||
|
|
||||||
// Ensure to remove all current sessions and offline sessions
|
// Ensure to remove all current sessions and offline sessions
|
||||||
setTimeOffset(10000000);
|
setTimeOffset(10000000);
|
||||||
getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
|
getTestingClientForStartedNodeInDc(0).testing("test").removeExpired("test");
|
||||||
|
|
|
@ -630,23 +630,4 @@ public class SessionExpirationCrossDCTest extends AbstractAdminCrossDCTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testClientRemoveAuthSessions(
|
|
||||||
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
|
|
||||||
|
|
||||||
createInitialAuthSessions();
|
|
||||||
|
|
||||||
channelStatisticsCrossDc.reset();
|
|
||||||
|
|
||||||
// Remove test-app client
|
|
||||||
ApiUtil.findClientByClientId(getAdminClient().realm(REALM_NAME), "test-app").remove();
|
|
||||||
|
|
||||||
// Assert sessions removed on node1 and node2 and on remote caches.
|
|
||||||
assertAuthSessionsStatisticsExpected("After client removed", channelStatisticsCrossDc,
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.forms;
|
package org.keycloak.testsuite.forms;
|
||||||
|
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
|
@ -281,5 +282,36 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// KEYCLOAK-5797
|
||||||
|
@Test
|
||||||
|
public void loginWithDifferentClients() throws Exception {
|
||||||
|
// Open tab1 and start login here
|
||||||
|
oauth.openLoginForm();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("login-test", "bad-password");
|
||||||
|
String tab1Url = driver.getCurrentUrl();
|
||||||
|
|
||||||
|
// Go to tab2 and start login with different client "root-url-client"
|
||||||
|
oauth.clientId("root-url-client");
|
||||||
|
oauth.redirectUri("http://localhost:8180/foo/bar/baz");
|
||||||
|
oauth.openLoginForm();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
String tab2Url = driver.getCurrentUrl();
|
||||||
|
|
||||||
|
// Go back to tab1 and finish login here
|
||||||
|
driver.navigate().to(tab1Url);
|
||||||
|
loginPage.login("login-test", "password");
|
||||||
|
updatePasswordPage.changePassword("password", "password");
|
||||||
|
updateProfilePage.update("John", "Doe3", "john@doe3.com");
|
||||||
|
|
||||||
|
// Assert I am redirected to the appPage in tab1
|
||||||
|
appPage.assertCurrent();
|
||||||
|
|
||||||
|
// Go back to tab2 and finish login here. Should be on the root-url-client page
|
||||||
|
driver.navigate().to(tab2Url);
|
||||||
|
String currentUrl = driver.getCurrentUrl();
|
||||||
|
Assert.assertThat(currentUrl, Matchers.startsWith("http://localhost:8180/foo/bar/baz"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ public class RestartCookieTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
// KEYCLOAK-5440
|
// KEYCLOAK-5440
|
||||||
@Test
|
@Test
|
||||||
public void invalidLoginAndBackButton() throws IOException, MessagingException {
|
public void testRestartCookieBackwardsCompatible() throws IOException, MessagingException {
|
||||||
String oldRestartCookie = testingClient.server().fetchString((KeycloakSession session) -> {
|
String oldRestartCookie = testingClient.server().fetchString((KeycloakSession session) -> {
|
||||||
try {
|
try {
|
||||||
String cookieVal = OLD_RESTART_COOKIE_JSON.replace("\n", "").replace(" ", "");
|
String cookieVal = OLD_RESTART_COOKIE_JSON.replace("\n", "").replace(" ", "");
|
||||||
|
|
|
@ -50,6 +50,7 @@ public class MailAssert {
|
||||||
if (message.getContent() instanceof MimeMultipart) {
|
if (message.getContent() instanceof MimeMultipart) {
|
||||||
MimeMultipart mimeMultipart = (MimeMultipart) message.getContent();
|
MimeMultipart mimeMultipart = (MimeMultipart) message.getContent();
|
||||||
|
|
||||||
|
// TEXT content is on index 0
|
||||||
messageContent = String.valueOf(mimeMultipart.getBodyPart(0).getContent());
|
messageContent = String.valueOf(mimeMultipart.getBodyPart(0).getContent());
|
||||||
} else {
|
} else {
|
||||||
messageContent = String.valueOf(message.getContent());
|
messageContent = String.valueOf(message.getContent());
|
||||||
|
@ -61,6 +62,9 @@ public class MailAssert {
|
||||||
assertTrue(errorMessage, messageContent.contains(content));
|
assertTrue(errorMessage, messageContent.contains(content));
|
||||||
for (String string : messageContent.split("\n")) {
|
for (String string : messageContent.split("\n")) {
|
||||||
if (string.contains("http://")) {
|
if (string.contains("http://")) {
|
||||||
|
|
||||||
|
// Ampersand escaped in the text version. Needs to be replaced to have correct URL
|
||||||
|
string = string.replace("&", "&");
|
||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -357,14 +357,15 @@ public abstract class AbstractIdentityProviderTest {
|
||||||
|
|
||||||
final String htmlBody = (String) multipart.getBodyPart(1).getContent();
|
final String htmlBody = (String) multipart.getBodyPart(1).getContent();
|
||||||
|
|
||||||
|
final String htmlChangePwdUrl = MailUtil.getLink(htmlBody);
|
||||||
// .replace() accounts for escaping the ampersand
|
// .replace() accounts for escaping the ampersand
|
||||||
// It's not escaped in the html version because html retrieved from a
|
// It's not escaped in the html version because html retrieved from a
|
||||||
// message bundle is considered safe and it must be unescaped to display
|
// message bundle is considered safe and it must be unescaped to display
|
||||||
// properly.
|
// properly.
|
||||||
final String htmlVerificationUrl = MailUtil.getLink(htmlBody).replace("&", "&");
|
final String htmlChangePwdUrlToCompare = htmlChangePwdUrl.replace("&", "&");
|
||||||
|
|
||||||
assertEquals(htmlVerificationUrl, textVerificationUrl);
|
assertEquals(htmlChangePwdUrlToCompare, textVerificationUrl);
|
||||||
|
|
||||||
return htmlVerificationUrl;
|
return htmlChangePwdUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.keycloak.services.managers.ClientManager;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.CommonClientSessionModel;
|
import org.keycloak.sessions.CommonClientSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
import org.keycloak.testsuite.rule.KeycloakRule;
|
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
@ -83,37 +84,40 @@ public class AuthenticationSessionProviderTest {
|
||||||
ClientModel client1 = realm.getClientByClientId("test-app");
|
ClientModel client1 = realm.getClientByClientId("test-app");
|
||||||
UserModel user1 = session.users().getUserByUsername("user1", realm);
|
UserModel user1 = session.users().getUserByUsername("user1", realm);
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client1);
|
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(realm);
|
||||||
|
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client1);
|
||||||
|
|
||||||
authSession.setAction("foo");
|
authSession.setAction("foo");
|
||||||
authSession.setTimestamp(100);
|
rootAuthSession.setTimestamp(100);
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
// Ensure session is here
|
// Ensure session is here
|
||||||
authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
|
rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId());
|
||||||
testAuthenticationSession(authSession, client1.getId(), null, "foo");
|
testAuthenticationSession(authSession, client1.getId(), null, "foo");
|
||||||
Assert.assertEquals(100, authSession.getTimestamp());
|
Assert.assertEquals(100, rootAuthSession.getTimestamp());
|
||||||
|
|
||||||
// Update and commit
|
// Update and commit
|
||||||
authSession.setAction("foo-updated");
|
authSession.setAction("foo-updated");
|
||||||
authSession.setTimestamp(200);
|
rootAuthSession.setTimestamp(200);
|
||||||
authSession.setAuthenticatedUser(session.users().getUserByUsername("user1", realm));
|
authSession.setAuthenticatedUser(session.users().getUserByUsername("user1", realm));
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
// Ensure session was updated
|
// Ensure session was updated
|
||||||
authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
|
rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId());
|
||||||
|
client1 = realm.getClientByClientId("test-app");
|
||||||
|
authSession = rootAuthSession.getAuthenticationSession(client1);
|
||||||
testAuthenticationSession(authSession, client1.getId(), user1.getId(), "foo-updated");
|
testAuthenticationSession(authSession, client1.getId(), user1.getId(), "foo-updated");
|
||||||
Assert.assertEquals(200, authSession.getTimestamp());
|
Assert.assertEquals(200, rootAuthSession.getTimestamp());
|
||||||
|
|
||||||
// Remove and commit
|
// Remove and commit
|
||||||
session.authenticationSessions().removeAuthenticationSession(realm, authSession);
|
session.authenticationSessions().removeRootAuthenticationSession(realm, rootAuthSession);
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
// Ensure session was removed
|
// Ensure session was removed
|
||||||
Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSession.getId()));
|
Assert.assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId()));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,10 +126,10 @@ public class AuthenticationSessionProviderTest {
|
||||||
ClientModel client1 = realm.getClientByClientId("test-app");
|
ClientModel client1 = realm.getClientByClientId("test-app");
|
||||||
UserModel user1 = session.users().getUserByUsername("user1", realm);
|
UserModel user1 = session.users().getUserByUsername("user1", realm);
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client1);
|
AuthenticationSessionModel authSession = session.authenticationSessions().createRootAuthenticationSession(realm).createAuthenticationSession(client1);
|
||||||
|
|
||||||
authSession.setAction("foo");
|
authSession.setAction("foo");
|
||||||
authSession.setTimestamp(100);
|
authSession.getParentSession().setTimestamp(100);
|
||||||
|
|
||||||
authSession.setAuthenticatedUser(user1);
|
authSession.setAuthenticatedUser(user1);
|
||||||
authSession.setAuthNote("foo", "bar");
|
authSession.setAuthNote("foo", "bar");
|
||||||
|
@ -134,20 +138,17 @@ public class AuthenticationSessionProviderTest {
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
|
// Test restart root authentication session
|
||||||
client1 = realm.getClientByClientId("test-app");
|
client1 = realm.getClientByClientId("test-app");
|
||||||
authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
|
authSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSession.getParentSession().getId())
|
||||||
authSession.restartSession(realm, client1);
|
.getAuthenticationSession(client1);
|
||||||
|
authSession.getParentSession().restartSession(realm);
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId());
|
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSession.getParentSession().getId());
|
||||||
testAuthenticationSession(authSession, client1.getId(), null, null);
|
Assert.assertNull(rootAuthSession.getAuthenticationSession(client1));
|
||||||
Assert.assertTrue(authSession.getTimestamp() > 0);
|
Assert.assertTrue(rootAuthSession.getTimestamp() > 0);
|
||||||
|
|
||||||
Assert.assertTrue(authSession.getClientNotes().isEmpty());
|
|
||||||
Assert.assertNull(authSession.getAuthNote("foo2"));
|
|
||||||
Assert.assertTrue(authSession.getExecutionStatus().isEmpty());
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -159,58 +160,59 @@ public class AuthenticationSessionProviderTest {
|
||||||
realm.setAccessCodeLifespanLogin(30);
|
realm.setAccessCodeLifespanLogin(30);
|
||||||
|
|
||||||
// Login lifespan is largest
|
// Login lifespan is largest
|
||||||
String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
|
String authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId();
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
Time.setOffset(25);
|
Time.setOffset(25);
|
||||||
session.authenticationSessions().removeExpired(realm);
|
session.authenticationSessions().removeExpired(realm);
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
|
assertNotNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId));
|
||||||
|
|
||||||
Time.setOffset(35);
|
Time.setOffset(35);
|
||||||
session.authenticationSessions().removeExpired(realm);
|
session.authenticationSessions().removeExpired(realm);
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
|
assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId));
|
||||||
|
|
||||||
// User action is largest
|
// User action is largest
|
||||||
realm.setAccessCodeLifespanUserAction(40);
|
realm.setAccessCodeLifespanUserAction(40);
|
||||||
|
|
||||||
Time.setOffset(0);
|
Time.setOffset(0);
|
||||||
authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
|
authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId();
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
Time.setOffset(35);
|
Time.setOffset(35);
|
||||||
session.authenticationSessions().removeExpired(realm);
|
session.authenticationSessions().removeExpired(realm);
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
|
assertNotNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId));
|
||||||
|
|
||||||
Time.setOffset(45);
|
Time.setOffset(45);
|
||||||
session.authenticationSessions().removeExpired(realm);
|
session.authenticationSessions().removeExpired(realm);
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
|
assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId));
|
||||||
|
|
||||||
// Access code is largest
|
// Access code is largest
|
||||||
realm.setAccessCodeLifespan(50);
|
realm.setAccessCodeLifespan(50);
|
||||||
|
|
||||||
Time.setOffset(0);
|
Time.setOffset(0);
|
||||||
authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
|
authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId();
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
Time.setOffset(45);
|
Time.setOffset(45);
|
||||||
session.authenticationSessions().removeExpired(realm);
|
session.authenticationSessions().removeExpired(realm);
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
|
assertNotNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId));
|
||||||
|
|
||||||
Time.setOffset(55);
|
Time.setOffset(55);
|
||||||
session.authenticationSessions().removeExpired(realm);
|
session.authenticationSessions().removeExpired(realm);
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId));
|
assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId));
|
||||||
} finally {
|
} finally {
|
||||||
Time.setOffset(0);
|
Time.setOffset(0);
|
||||||
|
|
||||||
|
@ -227,8 +229,8 @@ public class AuthenticationSessionProviderTest {
|
||||||
RealmModel fooRealm = session.realms().createRealm("foo-realm");
|
RealmModel fooRealm = session.realms().createRealm("foo-realm");
|
||||||
ClientModel fooClient = fooRealm.addClient("foo-client");
|
ClientModel fooClient = fooRealm.addClient("foo-client");
|
||||||
|
|
||||||
String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
|
String authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId();
|
||||||
String authSessionId2 = session.authenticationSessions().createAuthenticationSession(fooRealm, fooClient).getId();
|
String authSessionId2 = session.authenticationSessions().createRootAuthenticationSession(fooRealm).getId();
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
|
@ -236,27 +238,36 @@ public class AuthenticationSessionProviderTest {
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
|
RootAuthenticationSessionModel authSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||||
testAuthenticationSession(authSession, realm.getClientByClientId("test-app").getId(), null, null);
|
Assert.assertNotNull(authSession);
|
||||||
Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId2));
|
Assert.assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId2));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testOnClientRemoved() {
|
public void testOnClientRemoved() {
|
||||||
String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId();
|
String authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId();
|
||||||
String authSessionId2 = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("third-party")).getId();
|
AuthenticationSessionModel authSession1 = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId).createAuthenticationSession(realm.getClientByClientId("test-app"));
|
||||||
|
AuthenticationSessionModel authSession2 = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId).createAuthenticationSession(realm.getClientByClientId("third-party"));
|
||||||
|
|
||||||
|
authSession1.setAuthNote("foo", "bar");
|
||||||
|
authSession2.setAuthNote("foo", "baz");
|
||||||
|
|
||||||
String testAppClientUUID = realm.getClientByClientId("test-app").getId();
|
String testAppClientUUID = realm.getClientByClientId("test-app").getId();
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
|
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||||
|
Assert.assertEquals(2, rootAuthSession.getAuthenticationSessions().size());
|
||||||
|
Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app")).getAuthNote("foo"));
|
||||||
|
Assert.assertEquals("baz", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("third-party")).getAuthNote("foo"));
|
||||||
|
|
||||||
new ClientManager(new RealmManager(session)).removeClient(realm, realm.getClientByClientId("third-party"));
|
new ClientManager(new RealmManager(session)).removeClient(realm, realm.getClientByClientId("third-party"));
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, authSessionId);
|
rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||||
testAuthenticationSession(authSession, testAppClientUUID, null, null);
|
Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app")).getAuthNote("foo"));
|
||||||
Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId2));
|
Assert.assertNull(rootAuthSession.getAuthenticationSession(realm.getClientByClientId("third-party")));
|
||||||
|
|
||||||
// Revert client
|
// Revert client
|
||||||
realm.addClient("third-party");
|
realm.addClient("third-party");
|
||||||
|
|
Loading…
Reference in a new issue