diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7121f0b601..3c61509fd2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -121,7 +121,7 @@ jobs:
run: |
declare -A PARAMS TESTGROUP
PARAMS["quarkus"]="-Pauth-server-quarkus"
- PARAMS["undertow-map"]="-Pauth-server-undertow -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map -Dkeycloak.role.provider=map"
+ PARAMS["undertow-map"]="-Pauth-server-undertow -Dkeycloak.client.provider=map -Dkeycloak.group.provider=map -Dkeycloak.role.provider=map -Dkeycloak.authSession.provider=map"
PARAMS["wildfly"]="-Pauth-server-wildfly"
TESTGROUP["group1"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(a[abc]|ad[a-l]|[^a-q]).*]" # Tests alphabetically before admin tests and those after "r"
TESTGROUP["group2"]="-Dtest=!**.crossdc.**,!**.cluster.**,%regex[org.keycloak.testsuite.(ad[^a-l]|a[^a-d]|b).*]" # Admin tests and those starting with "b"
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
index dae8a790d6..673136b141 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
@@ -69,12 +69,12 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
@Override
public RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm) {
String id = keyGenerator.generateKeyString(session, cache);
- return createRootAuthenticationSession(id, realm);
+ return createRootAuthenticationSession(realm, id);
}
@Override
- public RootAuthenticationSessionModel createRootAuthenticationSession(String id, RealmModel realm) {
+ public RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm, String id) {
RootAuthenticationSessionEntity entity = new RootAuthenticationSessionEntity();
entity.setId(id);
entity.setRealmId(realm.getId());
diff --git a/model/map/src/main/java/org/keycloak/models/map/authSession/AbstractRootAuthenticationSessionEntity.java b/model/map/src/main/java/org/keycloak/models/map/authSession/AbstractRootAuthenticationSessionEntity.java
new file mode 100644
index 0000000000..859a112206
--- /dev/null
+++ b/model/map/src/main/java/org/keycloak/models/map/authSession/AbstractRootAuthenticationSessionEntity.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2020 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.map.authSession;
+
+import org.keycloak.models.map.common.AbstractEntity;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author Martin Kanis
+ */
+public abstract class AbstractRootAuthenticationSessionEntity implements AbstractEntity {
+
+ private K id;
+ private String realmId;
+
+ /**
+ * Flag signalizing that any of the setters has been meaningfully used.
+ */
+ protected boolean updated;
+ private int timestamp;
+ private Map authenticationSessions = new ConcurrentHashMap<>();
+
+ protected AbstractRootAuthenticationSessionEntity() {
+ this.id = null;
+ this.realmId = null;
+ }
+
+ public AbstractRootAuthenticationSessionEntity(K id, String realmId) {
+ Objects.requireNonNull(id, "id");
+ Objects.requireNonNull(realmId, "realmId");
+
+ this.id = id;
+ this.realmId = realmId;
+ }
+
+ @Override
+ public K getId() {
+ return this.id;
+ }
+
+ @Override
+ public boolean isUpdated() {
+ return this.updated;
+ }
+
+ public String getRealmId() {
+ return realmId;
+ }
+
+ public void setRealmId(String realmId) {
+ this.updated |= !Objects.equals(this.realmId, realmId);
+ this.realmId = realmId;
+ }
+
+ public int getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(int timestamp) {
+ this.updated |= !Objects.equals(this.timestamp, timestamp);
+ this.timestamp = timestamp;
+ }
+
+ public Map getAuthenticationSessions() {
+ return authenticationSessions;
+ }
+
+ public void setAuthenticationSessions(Map authenticationSessions) {
+ this.updated |= !Objects.equals(this.authenticationSessions, authenticationSessions);
+ this.authenticationSessions = authenticationSessions;
+ }
+
+ public MapAuthenticationSessionEntity removeAuthenticationSession(String tabId) {
+ MapAuthenticationSessionEntity entity = this.authenticationSessions.remove(tabId);
+ this.updated |= entity != null;
+ return entity;
+ }
+
+ public void addAuthenticationSession(String tabId, MapAuthenticationSessionEntity entity) {
+ this.updated |= !Objects.equals(this.authenticationSessions.put(tabId, entity), entity);
+ }
+
+ public void clearAuthenticationSessions() {
+ this.updated |= !this.authenticationSessions.isEmpty();
+ this.authenticationSessions.clear();
+ }
+}
diff --git a/model/map/src/main/java/org/keycloak/models/map/authSession/AbstractRootAuthenticationSessionModel.java b/model/map/src/main/java/org/keycloak/models/map/authSession/AbstractRootAuthenticationSessionModel.java
new file mode 100644
index 0000000000..a9f5df96fc
--- /dev/null
+++ b/model/map/src/main/java/org/keycloak/models/map/authSession/AbstractRootAuthenticationSessionModel.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020 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.map.authSession;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.map.common.AbstractEntity;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
+
+import java.util.Objects;
+
+/**
+ * @author Martin Kanis
+ */
+public abstract class AbstractRootAuthenticationSessionModel implements RootAuthenticationSessionModel {
+
+ protected final KeycloakSession session;
+ protected final RealmModel realm;
+ protected final E entity;
+
+ public AbstractRootAuthenticationSessionModel(KeycloakSession session, RealmModel realm, E entity) {
+ Objects.requireNonNull(entity, "entity");
+ Objects.requireNonNull(realm, "realm");
+
+ this.session = session;
+ this.realm = realm;
+ this.entity = entity;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof RootAuthenticationSessionModel)) return false;
+
+ RootAuthenticationSessionModel that = (RootAuthenticationSessionModel) o;
+ return Objects.equals(that.getId(), getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return getId().hashCode();
+ }
+}
diff --git a/model/map/src/main/java/org/keycloak/models/map/authSession/MapAuthenticationSessionAdapter.java b/model/map/src/main/java/org/keycloak/models/map/authSession/MapAuthenticationSessionAdapter.java
new file mode 100644
index 0000000000..f23fd54008
--- /dev/null
+++ b/model/map/src/main/java/org/keycloak/models/map/authSession/MapAuthenticationSessionAdapter.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2020 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.map.authSession;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author Martin Kanis
+ */
+public class MapAuthenticationSessionAdapter implements AuthenticationSessionModel {
+
+ private final KeycloakSession session;
+ private final MapRootAuthenticationSessionAdapter parent;
+ private final String tabId;
+ private MapAuthenticationSessionEntity entity;
+
+ public MapAuthenticationSessionAdapter(KeycloakSession session, MapRootAuthenticationSessionAdapter parent,
+ String tabId, MapAuthenticationSessionEntity entity) {
+ this.session = session;
+ this.parent = parent;
+ this.tabId = tabId;
+ this.entity = entity;
+ }
+
+ @Override
+ public String getTabId() {
+ return tabId;
+ }
+
+ @Override
+ public RootAuthenticationSessionModel getParentSession() {
+ return parent;
+ }
+
+ @Override
+ public Map getExecutionStatus() {
+ return entity.getExecutionStatus();
+ }
+
+ @Override
+ public void setExecutionStatus(String authenticator, ExecutionStatus status) {
+ Objects.requireNonNull(authenticator, "The provided authenticator can't be null!");
+ Objects.requireNonNull(status, "The provided execution status can't be null!");
+ parent.setUpdated(!Objects.equals(entity.getExecutionStatus().put(authenticator, status), status));
+ }
+
+ @Override
+ public void clearExecutionStatus() {
+ parent.setUpdated(!entity.getExecutionStatus().isEmpty());
+ entity.getExecutionStatus().clear();
+ }
+
+ @Override
+ public UserModel getAuthenticatedUser() {
+ return entity.getAuthUserId() == null ? null : session.users().getUserById(entity.getAuthUserId(), getRealm());
+ }
+
+ @Override
+ public void setAuthenticatedUser(UserModel user) {
+ String userId = (user == null) ? null : user.getId();
+ parent.setUpdated(!Objects.equals(userId, entity.getAuthUserId()));
+ entity.setAuthUserId(userId);
+ }
+
+ @Override
+ public Set getRequiredActions() {
+ return new HashSet<>(entity.getRequiredActions());
+ }
+
+ @Override
+ public void addRequiredAction(String action) {
+ Objects.requireNonNull(action, "The provided action can't be null!");
+ parent.setUpdated(entity.getRequiredActions().add(action));
+ }
+
+ @Override
+ public void removeRequiredAction(String action) {
+ Objects.requireNonNull(action, "The provided action can't be null!");
+ parent.setUpdated(entity.getRequiredActions().remove(action));
+ }
+
+ @Override
+ public void addRequiredAction(UserModel.RequiredAction action) {
+ Objects.requireNonNull(action, "The provided action can't be null!");
+ addRequiredAction(action.name());
+ }
+
+ @Override
+ public void removeRequiredAction(UserModel.RequiredAction action) {
+ Objects.requireNonNull(action, "The provided action can't be null!");
+ removeRequiredAction(action.name());
+ }
+
+ @Override
+ public void setUserSessionNote(String name, String value) {
+ if (name != null) {
+ if (value == null) {
+ parent.setUpdated(entity.getUserSessionNotes().remove(name) != null);
+ } else {
+ parent.setUpdated(!Objects.equals(entity.getUserSessionNotes().put(name, value), value));
+ }
+ }
+ }
+
+ @Override
+ public Map getUserSessionNotes() {
+ return new ConcurrentHashMap<>(entity.getUserSessionNotes());
+ }
+
+ @Override
+ public void clearUserSessionNotes() {
+ parent.setUpdated(!entity.getUserSessionNotes().isEmpty());
+ entity.getUserSessionNotes().clear();
+ }
+
+ @Override
+ public String getAuthNote(String name) {
+ return (name != null) ? entity.getAuthNotes().get(name) : null;
+ }
+
+ @Override
+ public void setAuthNote(String name, String value) {
+ if (name != null) {
+ if (value == null) {
+ parent.setUpdated(entity.getAuthNotes().remove(name) != null);
+ } else {
+ parent.setUpdated(!Objects.equals(entity.getAuthNotes().put(name, value), value));
+ }
+ }
+ }
+
+ @Override
+ public void removeAuthNote(String name) {
+ if (name != null) {
+ parent.setUpdated(entity.getAuthNotes().remove(name) != null);
+ }
+ }
+
+ @Override
+ public void clearAuthNotes() {
+ parent.setUpdated(!entity.getAuthNotes().isEmpty());
+ entity.getAuthNotes().clear();
+ }
+
+ @Override
+ public String getClientNote(String name) {
+ return (name != null) ? entity.getClientNotes().get(name) : null;
+ }
+
+ @Override
+ public void setClientNote(String name, String value) {
+ if (name != null) {
+ if (value == null) {
+ parent.setUpdated(entity.getClientNotes().remove(name) != null);
+ } else {
+ parent.setUpdated(!Objects.equals(entity.getClientNotes().put(name, value), value));
+ }
+ }
+ }
+
+ @Override
+ public void removeClientNote(String name) {
+ if (name != null) {
+ parent.setUpdated(entity.getClientNotes().remove(name) != null);
+ }
+ }
+
+ @Override
+ public Map getClientNotes() {
+ return new ConcurrentHashMap<>(entity.getClientNotes());
+ }
+
+ @Override
+ public void clearClientNotes() {
+ parent.setUpdated(!entity.getClientNotes().isEmpty());
+ entity.getClientNotes().clear();
+ }
+
+ @Override
+ public Set getClientScopes() {
+ return new HashSet<>(entity.getClientScopes());
+ }
+
+ @Override
+ public void setClientScopes(Set clientScopes) {
+ Objects.requireNonNull(clientScopes, "The provided client scopes set can't be null!");
+ parent.setUpdated(!Objects.equals(entity.getClientScopes(), clientScopes));
+ entity.setClientScopes(new HashSet<>(clientScopes));
+ }
+
+ @Override
+ public String getRedirectUri() {
+ return entity.getRedirectUri();
+ }
+
+ @Override
+ public void setRedirectUri(String uri) {
+ parent.setUpdated(!Objects.equals(entity.getRedirectUri(), uri));
+ entity.setRedirectUri(uri);
+ }
+
+ @Override
+ public RealmModel getRealm() {
+ return parent.getRealm();
+ }
+
+ @Override
+ public ClientModel getClient() {
+ return parent.getRealm().getClientById(entity.getClientUUID());
+ }
+
+ @Override
+ public String getAction() {
+ return entity.getAction();
+ }
+
+ @Override
+ public void setAction(String action) {
+ parent.setUpdated(!Objects.equals(entity.getAction(), action));
+ entity.setAction(action);
+ }
+
+ @Override
+ public String getProtocol() {
+ return entity.getProtocol();
+ }
+
+ @Override
+ public void setProtocol(String method) {
+ parent.setUpdated(!Objects.equals(entity.getProtocol(), method));
+ entity.setProtocol(method);
+ }
+}
diff --git a/model/map/src/main/java/org/keycloak/models/map/authSession/MapAuthenticationSessionAuthNoteUpdateEvent.java b/model/map/src/main/java/org/keycloak/models/map/authSession/MapAuthenticationSessionAuthNoteUpdateEvent.java
new file mode 100644
index 0000000000..5f31b5415a
--- /dev/null
+++ b/model/map/src/main/java/org/keycloak/models/map/authSession/MapAuthenticationSessionAuthNoteUpdateEvent.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2020 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.map.authSession;
+
+import org.keycloak.cluster.ClusterEvent;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * @author Martin Kanis
+ */
+public class MapAuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
+
+ private String authSessionId;
+ private String tabId;
+ private String clientUUID;
+
+ private Map authNotesFragment;
+
+ /**
+ * Creates an instance of the event.
+ * @param authSessionId
+ * @param authNotesFragment
+ * @return Event. Note that {@code authNotesFragment} property is not thread safe which is fine for now.
+ */
+ public static MapAuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, String tabId, String clientUUID,
+ Map authNotesFragment) {
+ MapAuthenticationSessionAuthNoteUpdateEvent event = new MapAuthenticationSessionAuthNoteUpdateEvent();
+ event.authSessionId = authSessionId;
+ event.tabId = tabId;
+ event.clientUUID = clientUUID;
+ event.authNotesFragment = new LinkedHashMap<>(authNotesFragment);
+ return event;
+ }
+
+ public String getAuthSessionId() {
+ return authSessionId;
+ }
+
+ public String getTabId() {
+ return tabId;
+ }
+
+ public String getClientUUID() {
+ return clientUUID;
+ }
+
+ public Map getAuthNotesFragment() {
+ return authNotesFragment;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, tabId=%s, clientUUID=%s, authNotesFragment=%s ]",
+ authSessionId, clientUUID, authNotesFragment);
+ }
+}
diff --git a/model/map/src/main/java/org/keycloak/models/map/authSession/MapAuthenticationSessionEntity.java b/model/map/src/main/java/org/keycloak/models/map/authSession/MapAuthenticationSessionEntity.java
new file mode 100644
index 0000000000..fdcbc81c9b
--- /dev/null
+++ b/model/map/src/main/java/org/keycloak/models/map/authSession/MapAuthenticationSessionEntity.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2020 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.map.authSession;
+
+import org.keycloak.sessions.AuthenticationSessionModel;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author Martin Kanis
+ */
+public class MapAuthenticationSessionEntity {
+
+ private String clientUUID;
+
+ private String authUserId;
+
+ private String redirectUri;
+ private String action;
+ private Set clientScopes = new HashSet<>();
+
+ private Map executionStatus = new ConcurrentHashMap<>();
+ private String protocol;
+
+ private Map clientNotes= new ConcurrentHashMap<>();;
+ private Map authNotes = new ConcurrentHashMap<>();;
+ private Set requiredActions = new HashSet<>();
+ private Map userSessionNotes = new ConcurrentHashMap<>();
+
+ public Map getUserSessionNotes() {
+ return userSessionNotes;
+ }
+
+ public void setUserSessionNotes(Map userSessionNotes) {
+ this.userSessionNotes = userSessionNotes;
+ }
+
+ public String getClientUUID() {
+ return clientUUID;
+ }
+
+ public void setClientUUID(String clientUUID) {
+ this.clientUUID = clientUUID;
+ }
+
+ public String getAuthUserId() {
+ return authUserId;
+ }
+
+ public void setAuthUserId(String authUserId) {
+ this.authUserId = authUserId;
+ }
+
+ public String getRedirectUri() {
+ return redirectUri;
+ }
+
+ public void setRedirectUri(String redirectUri) {
+ this.redirectUri = redirectUri;
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ public void setAction(String action) {
+ this.action = action;
+ }
+
+ public Set getClientScopes() {
+ return clientScopes;
+ }
+
+ public void setClientScopes(Set clientScopes) {
+ this.clientScopes = clientScopes;
+ }
+
+ public Set getRequiredActions() {
+ return requiredActions;
+ }
+
+ public void setRequiredActions(Set requiredActions) {
+ this.requiredActions = requiredActions;
+ }
+
+ public String getProtocol() {
+ return protocol;
+ }
+
+ public void setProtocol(String protocol) {
+ this.protocol = protocol;
+ }
+
+ public Map getClientNotes() {
+ return clientNotes;
+ }
+
+ public void setClientNotes(Map clientNotes) {
+ this.clientNotes = clientNotes;
+ }
+
+ public Map getAuthNotes() {
+ return authNotes;
+ }
+
+ public void setAuthNotes(Map authNotes) {
+ this.authNotes = authNotes;
+ }
+
+ public Map getExecutionStatus() {
+ return executionStatus;
+ }
+
+ public void setExecutionStatus(Map executionStatus) {
+ this.executionStatus = executionStatus;
+ }
+}
diff --git a/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionAdapter.java b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionAdapter.java
new file mode 100644
index 0000000000..2ebf39b27d
--- /dev/null
+++ b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionAdapter.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2020 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.map.authSession;
+
+import org.keycloak.common.util.Base64Url;
+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.utils.KeycloakModelUtils;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.AuthenticationSessionProvider;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * @author Martin Kanis
+ */
+public class MapRootAuthenticationSessionAdapter extends AbstractRootAuthenticationSessionModel {
+
+ public MapRootAuthenticationSessionAdapter(KeycloakSession session, RealmModel realm, MapRootAuthenticationSessionEntity entity) {
+ super(session, realm, entity);
+ }
+
+ @Override
+ public String getId() {
+ return entity.getId().toString();
+ }
+
+ @Override
+ public RealmModel getRealm() {
+ return session.realms().getRealm(entity.getRealmId());
+ }
+
+ @Override
+ public int getTimestamp() {
+ return entity.getTimestamp();
+ }
+
+ @Override
+ public void setTimestamp(int timestamp) {
+ entity.setTimestamp(timestamp);
+ }
+
+ @Override
+ public Map getAuthenticationSessions() {
+ return entity.getAuthenticationSessions().entrySet()
+ .stream()
+ .collect(Collectors.toMap(Map.Entry::getKey,
+ entry -> new MapAuthenticationSessionAdapter(session, this, entry.getKey(), entry.getValue())));
+ }
+
+ @Override
+ public AuthenticationSessionModel getAuthenticationSession(ClientModel client, String tabId) {
+ if (client == null || tabId == null) {
+ return null;
+ }
+
+ AuthenticationSessionModel authSession = getAuthenticationSessions().get(tabId);
+ if (authSession != null && client.equals(authSession.getClient())) {
+ session.getContext().setAuthenticationSession(authSession);
+ return authSession;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public AuthenticationSessionModel createAuthenticationSession(ClientModel client) {
+ Objects.requireNonNull(client, "The provided client can't be null!");
+
+ MapAuthenticationSessionEntity authSessionEntity = new MapAuthenticationSessionEntity();
+ authSessionEntity.setClientUUID(client.getId());
+
+ String tabId = generateTabId();
+ entity.getAuthenticationSessions().put(tabId, authSessionEntity);
+
+ // Update our timestamp when adding new authenticationSession
+ entity.setTimestamp(Time.currentTime());
+
+ MapAuthenticationSessionAdapter authSession = new MapAuthenticationSessionAdapter(session, this, tabId, authSessionEntity);
+ session.getContext().setAuthenticationSession(authSession);
+ return authSession;
+ }
+
+ @Override
+ public void removeAuthenticationSessionByTabId(String tabId) {
+ if (entity.removeAuthenticationSession(tabId) != null) {
+ if (entity.getAuthenticationSessions().isEmpty()) {
+ MapRootAuthenticationSessionProvider authenticationSessionProvider =
+ (MapRootAuthenticationSessionProvider) session.authenticationSessions();
+ authenticationSessionProvider.tx.remove(entity.getId());
+ } else {
+ entity.setTimestamp(Time.currentTime());
+ }
+ }
+ }
+
+ @Override
+ public void restartSession(RealmModel realm) {
+ entity.clearAuthenticationSessions();
+ entity.setTimestamp(Time.currentTime());
+ }
+
+ public void setUpdated(boolean updated) {
+ entity.updated |= updated;
+ }
+
+ private String generateTabId() {
+ return Base64Url.encode(KeycloakModelUtils.generateSecret(8));
+ }
+}
diff --git a/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionEntity.java b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionEntity.java
new file mode 100644
index 0000000000..055ec41944
--- /dev/null
+++ b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionEntity.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020 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.map.authSession;
+
+import java.util.UUID;
+
+/**
+ * @author Martin Kanis
+ */
+public class MapRootAuthenticationSessionEntity extends AbstractRootAuthenticationSessionEntity {
+
+ protected MapRootAuthenticationSessionEntity() {
+ super();
+ }
+
+ public MapRootAuthenticationSessionEntity(UUID id, String realmId) {
+ super(id, realmId);
+ }
+}
diff --git a/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProvider.java b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProvider.java
new file mode 100644
index 0000000000..93a811f61c
--- /dev/null
+++ b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProvider.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2020 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.map.authSession;
+
+import org.jboss.logging.Logger;
+import org.keycloak.cluster.ClusterProvider;
+import org.keycloak.common.util.Time;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.map.common.Serialization;
+import org.keycloak.models.map.storage.MapKeycloakTransaction;
+import org.keycloak.models.map.storage.MapStorage;
+import org.keycloak.models.utils.RealmInfoUtil;
+import org.keycloak.sessions.AuthenticationSessionCompoundId;
+import org.keycloak.sessions.AuthenticationSessionProvider;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import static org.keycloak.common.util.StackUtil.getShortStackTrace;
+
+/**
+ * @author Martin Kanis
+ */
+public class MapRootAuthenticationSessionProvider implements AuthenticationSessionProvider {
+
+ private static final Logger LOG = Logger.getLogger(MapRootAuthenticationSessionProvider.class);
+ private final KeycloakSession session;
+ protected final MapKeycloakTransaction tx;
+ private final MapStorage sessionStore;
+
+ private static final Predicate ALWAYS_FALSE = role -> false;
+ private static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS";
+
+ public MapRootAuthenticationSessionProvider(KeycloakSession session, MapStorage sessionStore) {
+ this.session = session;
+ this.sessionStore = sessionStore;
+ this.tx = new MapKeycloakTransaction<>(sessionStore);
+
+ session.getTransactionManager().enlistAfterCompletion(tx);
+ }
+
+ private Function entityToAdapterFunc(RealmModel realm) {
+ // Clone entity before returning back, to avoid giving away a reference to the live object to the caller
+
+ return origEntity -> new MapRootAuthenticationSessionAdapter(session, realm, registerEntityForChanges(origEntity));
+ }
+
+ private MapRootAuthenticationSessionEntity registerEntityForChanges(MapRootAuthenticationSessionEntity origEntity) {
+ MapRootAuthenticationSessionEntity res = tx.get(origEntity.getId(), id -> Serialization.from(origEntity));
+ tx.putIfChanged(origEntity.getId(), res, MapRootAuthenticationSessionEntity::isUpdated);
+ return res;
+ }
+
+ private Predicate entityRealmFilter(String realmId) {
+ if (realmId == null) {
+ return MapRootAuthenticationSessionProvider.ALWAYS_FALSE;
+ }
+ return entity -> Objects.equals(realmId, entity.getRealmId());
+ }
+
+ @Override
+ public RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm) {
+ Objects.requireNonNull(realm, "The provided realm can't be null!");
+ return createRootAuthenticationSession(realm, null);
+ }
+
+ @Override
+ public RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm, String id) {
+ Objects.requireNonNull(realm, "The provided realm can't be null!");
+
+ final UUID entityId = id == null ? UUID.randomUUID() : UUID.fromString(id);
+
+ LOG.tracef("createRootAuthenticationSession(%s)%s", realm.getName(), getShortStackTrace());
+
+ // create map authentication session entity
+ MapRootAuthenticationSessionEntity entity = new MapRootAuthenticationSessionEntity(entityId, realm.getId());
+ entity.setRealmId(realm.getId());
+ entity.setTimestamp(Time.currentTime());
+
+ if (tx.get(entity.getId(), sessionStore::get) != null) {
+ throw new ModelDuplicateException("Root authentication session exists: " + entity.getId());
+ }
+
+ tx.putIfAbsent(entity.getId(), entity);
+
+ return entityToAdapterFunc(realm).apply(entity);
+ }
+
+ @Override
+ public RootAuthenticationSessionModel getRootAuthenticationSession(RealmModel realm, String authenticationSessionId) {
+ Objects.requireNonNull(realm, "The provided realm can't be null!");
+ if (authenticationSessionId == null) {
+ return null;
+ }
+
+ LOG.tracef("getRootAuthenticationSession(%s, %s)%s", realm.getName(), authenticationSessionId, getShortStackTrace());
+
+ MapRootAuthenticationSessionEntity entity = tx.get(UUID.fromString(authenticationSessionId), sessionStore::get);
+ return (entity == null || !entityRealmFilter(realm.getId()).test(entity))
+ ? null
+ : entityToAdapterFunc(realm).apply(entity);
+ }
+
+ @Override
+ public void removeRootAuthenticationSession(RealmModel realm, RootAuthenticationSessionModel authenticationSession) {
+ Objects.requireNonNull(authenticationSession, "The provided root authentication session can't be null!");
+ tx.remove(UUID.fromString(authenticationSession.getId()));
+ }
+
+ @Override
+ public void removeExpired(RealmModel realm) {
+ Objects.requireNonNull(realm, "The provided realm can't be null!");
+ LOG.debugf("Removing expired sessions");
+
+ int expired = Time.currentTime() - RealmInfoUtil.getDettachedClientSessionLifespan(realm);
+
+ List sessionIds = sessionStore.entrySet().stream()
+ .filter(entity -> entityRealmFilter(realm.getId()).test(entity.getValue()))
+ .filter(entity -> entity.getValue().getTimestamp() < expired)
+ .map(Map.Entry::getKey)
+ .collect(Collectors.toList());
+
+ LOG.debugf("Removed %d expired authentication sessions for realm '%s'", sessionIds.size(), realm.getName());
+
+ sessionIds.forEach(tx::remove);
+ }
+
+ @Override
+ public void onRealmRemoved(RealmModel realm) {
+ Objects.requireNonNull(realm, "The provided realm can't be null!");
+ sessionStore.entrySet().stream()
+ .filter(entity -> entityRealmFilter(realm.getId()).test(entity.getValue()))
+ .map(Map.Entry::getKey)
+ .collect(Collectors.toList())
+ .forEach(tx::remove);
+ }
+
+ @Override
+ public void onClientRemoved(RealmModel realm, ClientModel client) {
+
+ }
+
+ @Override
+ public void updateNonlocalSessionAuthNotes(AuthenticationSessionCompoundId compoundId, Map authNotesFragment) {
+ if (compoundId == null) {
+ return;
+ }
+ Objects.requireNonNull(authNotesFragment, "The provided authentication's notes map can't be null!");
+
+ ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+ cluster.notify(
+ AUTHENTICATION_SESSION_EVENTS,
+ MapAuthenticationSessionAuthNoteUpdateEvent.create(compoundId.getRootSessionId(), compoundId.getTabId(),
+ compoundId.getClientUUID(), authNotesFragment),
+ true,
+ ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC
+ );
+ }
+
+ @Override
+ public void close() {
+
+ }
+}
diff --git a/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProviderFactory.java b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProviderFactory.java
new file mode 100644
index 0000000000..304c623915
--- /dev/null
+++ b/model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProviderFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2020 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.map.authSession;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.map.common.AbstractMapProviderFactory;
+import org.keycloak.models.map.storage.MapStorage;
+import org.keycloak.models.map.storage.MapStorageProvider;
+import org.keycloak.sessions.AuthenticationSessionProvider;
+import org.keycloak.sessions.AuthenticationSessionProviderFactory;
+
+import java.util.UUID;
+
+/**
+ * @author Martin Kanis
+ */
+public class MapRootAuthenticationSessionProviderFactory extends AbstractMapProviderFactory
+ implements AuthenticationSessionProviderFactory {
+
+ private MapStorage store;
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ MapStorageProvider sp = (MapStorageProvider) factory.getProviderFactory(MapStorageProvider.class);
+ this.store = sp.getStorage("sessions", UUID.class, MapRootAuthenticationSessionEntity.class);
+ }
+
+ @Override
+ public AuthenticationSessionProvider create(KeycloakSession session) {
+ return new MapRootAuthenticationSessionProvider(session, store);
+ }
+}
diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java b/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java
index 3fae262cdd..4b9ec56b41 100644
--- a/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java
+++ b/model/map/src/main/java/org/keycloak/models/map/storage/MapKeycloakTransaction.java
@@ -115,7 +115,7 @@ public class MapKeycloakTransaction implements KeycloakTransaction {
* Adds a given task if not exists for the given key
*/
private void addTask(MapOperation op, K key, V value) {
- log.tracev("Adding operation {0} for {1}", op, key);
+ log.tracef("Adding operation %s for %s @ %08x", op, key, System.identityHashCode(value));
K taskKey = key;
tasks.merge(taskKey, op.taskFor(key, value), MapTaskCompose::new);
@@ -149,7 +149,7 @@ public class MapKeycloakTransaction implements KeycloakTransaction {
}
public void putIfChanged(K key, V value, Predicate shouldPut) {
- log.tracev("Adding operation UPDATE_IF_CHANGED for {0}", key);
+ log.tracef("Adding operation UPDATE_IF_CHANGED for %s @ %08x", key, System.identityHashCode(value));
K taskKey = key;
MapTaskWithValue op = new MapTaskWithValue(value) {
diff --git a/model/map/src/main/resources/META-INF/services/org.keycloak.sessions.AuthenticationSessionProviderFactory b/model/map/src/main/resources/META-INF/services/org.keycloak.sessions.AuthenticationSessionProviderFactory
new file mode 100644
index 0000000000..d7ac3a5a5f
--- /dev/null
+++ b/model/map/src/main/resources/META-INF/services/org.keycloak.sessions.AuthenticationSessionProviderFactory
@@ -0,0 +1,18 @@
+#
+# Copyright 2020 Red Hat, Inc. and/or its affiliates
+# and other contributors as indicated by the @author tags.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderFactory
\ No newline at end of file
diff --git a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java
index b3c976b188..030ef3e35a 100644
--- a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java
@@ -37,104 +37,171 @@ public interface AuthenticationSessionModel extends CommonClientSessionModel {
*/
String getTabId();
+ /**
+ * Returns the root authentication session that is parent of this authentication session.
+ * @return {@code RootAuthenticationSessionModel}
+ */
RootAuthenticationSessionModel getParentSession();
+ /**
+ * Returns execution status of the authentication session.
+ * @return {@code Map} Never returns {@code null}.
+ */
Map getExecutionStatus();
+
+ /**
+ * Sets execution status of the authentication session.
+ * @param authenticator {@code String} Can't be {@code null}.
+ * @param status {@code ExecutionStatus} Can't be {@code null}.
+ */
void setExecutionStatus(String authenticator, ExecutionStatus status);
+
+ /**
+ * Clears execution status of the authentication session.
+ */
void clearExecutionStatus();
+
+ /**
+ * Returns authenticated user that is associated to the authentication session.
+ * @return {@code UserModel} or null if there's no authenticated user.
+ */
UserModel getAuthenticatedUser();
+
+ /**
+ * Sets authenticated user that is associated to the authentication session.
+ * @param user {@code UserModel} If {@code null} then {@code null} will be set to the authenticated user.
+ */
void setAuthenticatedUser(UserModel user);
/**
- * Required actions that are attached to this client session.
- *
- * @return
+ * Returns required actions that are attached to this client session.
+ * @return {@code Set} Never returns {@code null}.
*/
Set getRequiredActions();
+ /**
+ * Adds a required action to the authentication session.
+ * @param action {@code String} Can't be {@code null}.
+ */
void addRequiredAction(String action);
+ /**
+ * Removes a required action from the authentication session.
+ * @param action {@code String} Can't be {@code null}.
+ */
void removeRequiredAction(String action);
+ /**
+ * Adds a required action to the authentication session.
+ * @param action {@code UserModel.RequiredAction} Can't be {@code null}.
+ */
void addRequiredAction(UserModel.RequiredAction action);
+ /**
+ * Removes a required action from the authentication session.
+ * @param action {@code UserModel.RequiredAction} Can't be {@code null}.
+ */
void removeRequiredAction(UserModel.RequiredAction action);
-
/**
- * Sets the given user session note to the given value. User session notes are notes
- * you want be applied to the UserSessionModel when the client session is attached to it.
+ * Sets the given user session note to the given value. User session notes are notes
+ * you want be applied to the UserSessionModel when the client session is attached to it.
+ * @param name {@code String} If {@code null} is provided the method won't have an effect.
+ * @param value {@code String} If {@code null} is provided the method won't have an effect.
*/
void setUserSessionNote(String name, String value);
+
/**
- * Retrieves value of given user session note. User session notes are notes
- * you want be applied to the UserSessionModel when the client session is attached to it.
+ * Retrieves value of given user session note. User session notes are notes
+ * you want be applied to the UserSessionModel when the client session is attached to it.
+ * @return {@code Map} never returns {@code null}
*/
Map getUserSessionNotes();
+
/**
- * Clears all user session notes. User session notes are notes
- * you want be applied to the UserSessionModel when the client session is attached to it.
+ * Clears all user session notes. User session notes are notes
+ * you want be applied to the UserSessionModel when the client session is attached to it.
*/
void clearUserSessionNotes();
/**
- * Retrieves value of the given authentication note to the given value. Authentication notes are notes
- * used typically by authenticators and authentication flows. They are cleared when
- * authentication session is restarted
+ * Retrieves value of the given authentication note to the given value. Authentication notes are notes
+ * used typically by authenticators and authentication flows. They are cleared when
+ * authentication session is restarted.
+ * @param name {@code String} If {@code null} if provided then the method will return {@code null}.
+ * @return {@code String} or {@code null} if no authentication note is set.
*/
String getAuthNote(String name);
+
/**
- * Sets the given authentication note to the given value. Authentication notes are notes
- * used typically by authenticators and authentication flows. They are cleared when
- * authentication session is restarted
+ * Sets the given authentication note to the given value. Authentication notes are notes
+ * used typically by authenticators and authentication flows. They are cleared when
+ * authentication session is restarted.
+ * @param name {@code String} If {@code null} is provided the method won't have an effect.
+ * @param value {@code String} If {@code null} is provided the method won't have an effect.
*/
void setAuthNote(String name, String value);
+
/**
- * Removes the given authentication note. Authentication notes are notes
- * used typically by authenticators and authentication flows. They are cleared when
- * authentication session is restarted
+ * Removes the given authentication note. Authentication notes are notes
+ * used typically by authenticators and authentication flows. They are cleared when
+ * authentication session is restarted.
+ * @param name {@code String} If {@code null} is provided the method won't have an effect.
*/
void removeAuthNote(String name);
+
/**
- * Clears all authentication note. Authentication notes are notes
- * used typically by authenticators and authentication flows. They are cleared when
- * authentication session is restarted
+ * Clears all authentication note. Authentication notes are notes
+ * used typically by authenticators and authentication flows. They are cleared when
+ * authentication session is restarted.
*/
void clearAuthNotes();
/**
- * Retrieves value of the given client note to the given value. Client notes are notes
- * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ * Retrieves value of the given client note to the given value. Client notes are notes
+ * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ * @param name {@code String} If {@code null} if provided then the method will return {@code null}.
+ * @return {@code String} or {@code null} if no client's note is set.
*/
String getClientNote(String name);
+
/**
- * Sets the given client note to the given value. Client notes are notes
- * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ * Sets the given client note to the given value. Client notes are notes
+ * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ * @param name {@code String} If {@code null} is provided the method won't have an effect.
+ * @param value {@code String} If {@code null} is provided the method won't have an effect.
*/
void setClientNote(String name, String value);
+
/**
- * Removes the given client note. Client notes are notes
- * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ * Removes the given client note. Client notes are notes
+ * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ * @param name {@code String} If {@code null} is provided the method won't have an effect.
*/
void removeClientNote(String name);
+
/**
- * Retrieves the (name, value) map of client notes. Client notes are notes
- * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ * Retrieves the (name, value) map of client notes. Client notes are notes
+ * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ * @return {@code Map} never returns {@code null}.
*/
Map getClientNotes();
+
/**
- * Clears all client notes. Client notes are notes
- * specific to client protocol. They are NOT cleared when authentication session is restarted.
+ * Clears all client notes. Client notes are notes
+ * specific to client protocol. They are NOT cleared when authentication session is restarted.
*/
void clearClientNotes();
/**
- * Get client scope IDs
+ * Gets client scope IDs from the authentication session.
+ * @return {@code Set} never returns {@code null}.
*/
Set getClientScopes();
/**
- * Set client scope IDs
+ * Sets client scope IDs to the authentication session.
+ * @param clientScopes {@code Set} Can't be {@code null}.
*/
void setClientScopes(Set clientScopes);
diff --git a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
index bc32430bac..cafb5934a9 100644
--- a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
+++ b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
@@ -31,27 +31,72 @@ public interface AuthenticationSessionProvider extends Provider {
/**
* Creates and registers a new authentication session with random ID. Authentication session
* entity will be prefilled with current timestamp, the given realm and client.
+ * @param realm {@code RealmModel} Can't be {@code null}.
+ * @return Returns created {@code RootAuthenticationSessionModel}. Never returns {@code null}.
*/
RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm);
- RootAuthenticationSessionModel createRootAuthenticationSession(String id, RealmModel realm);
+ /**
+ * Creates a new root authentication session specified by the provided id and realm.
+ * @param id {@code String} Id of newly created root authentication session. If {@code null} a random id will be generated.
+ * @param realm {@code RealmModel} Can't be {@code null}.
+ * @return Returns created {@code RootAuthenticationSessionModel}. Never returns {@code null}.
+ * @deprecated Use {@link #createRootAuthenticationSession(RealmModel, String)} createRootAuthenticationSession} instead.
+ */
+ @Deprecated
+ default RootAuthenticationSessionModel createRootAuthenticationSession(String id, RealmModel realm) {
+ return createRootAuthenticationSession(realm, id);
+ }
+ /**
+ * Creates a new root authentication session specified by the provided realm and id.
+ * @param realm {@code RealmModel} Can't be {@code null}.
+ * @param id {@code String} Id of newly created root authentication session. If {@code null} a random id will be generated.
+ * @return Returns created {@code RootAuthenticationSessionModel}. Never returns {@code null}.
+ */
+ RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm, String id);
+
+ /**
+ * Returns the root authentication session specified by the provided realm and id.
+ * @param realm {@code RealmModel} Can't be {@code null}.
+ * @param authenticationSessionId {@code RootAuthenticationSessionModel} If {@code null} then {@code null} will be returned.
+ * @return Returns found {@code RootAuthenticationSessionModel} or {@code null} if no root authentication session is found.
+ */
RootAuthenticationSessionModel getRootAuthenticationSession(RealmModel realm, String authenticationSessionId);
+ /**
+ * Removes provided root authentication session.
+ * @param realm {@code RealmModel} Associated realm to the given root authentication session.
+ * @param authenticationSession {@code RootAuthenticationSessionModel} Can't be {@code null}.
+ */
void removeRootAuthenticationSession(RealmModel realm, RootAuthenticationSessionModel authenticationSession);
+ /**
+ * Removes all expired root authentication sessions for the given realm.
+ * @param realm {@code RealmModel} Can't be {@code null}.
+ */
void removeExpired(RealmModel realm);
+
+ /**
+ * Removes all associated root authentication sessions to the given realm which was removed.
+ * @param realm {@code RealmModel} Can't be {@code null}.
+ */
void onRealmRemoved(RealmModel realm);
+
+ /**
+ * Removes all associated root authentication sessions to the given realm and client which was removed.
+ * @param realm {@code RealmModel} Can't be {@code null}.
+ * @param client {@code ClientModel} Can't be {@code null}.
+ */
void onClientRemoved(RealmModel realm, ClientModel client);
/**
* Requests update of authNotes of a root authentication session that is not owned
* by this instance but might exist somewhere in the cluster.
*
- * @param compoundId
- * @param authNotesFragment Map with authNote values. Auth note is removed if the corresponding value in the map is {@code null}.
+ * @param compoundId {@code AuthenticationSessionCompoundId} The method has no effect if {@code null}.
+ * @param authNotesFragment {@code Map} Map with authNote values.
+ * Auth note is removed if the corresponding value in the map is {@code null}. Map itself can't be {@code null}.
*/
void updateNonlocalSessionAuthNotes(AuthenticationSessionCompoundId compoundId, Map authNotesFragment);
-
-
}
diff --git a/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java
index 5854a9891b..9ca6252122 100644
--- a/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java
@@ -23,50 +23,70 @@ 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.
+ * Represents usually one browser session with potentially many browser tabs. Every browser tab is represented by
+ * {@link AuthenticationSessionModel} of different client.
*
* @author Marek Posolda
*/
public interface RootAuthenticationSessionModel {
+ /**
+ * Returns id of the root authentication session.
+ * @return {@code String}
+ */
String getId();
- RealmModel getRealm();
-
- int getTimestamp();
- void setTimestamp(int timestamp);
-
/**
+ * Returns realm associated to the root authentication session.
+ * @return {@code RealmModel}
+ */
+ RealmModel getRealm();
+
+ /**
+ * Returns timestamp when the root authentication session was created or updated.
+ * @return {@code int}
+ */
+ int getTimestamp();
+
+ /**
+ * Sets a timestamp when the root authentication session was created or updated.
+ * @param timestamp {@code int}
+ */
+ void setTimestamp(int timestamp);
+
+ /**
+ * Returns authentication sessions for the root authentication session.
* Key is tabId, Value is AuthenticationSessionModel.
- * @return authentication sessions or empty map if no authenticationSessions presents. Never return null.
+ * @return {@code Map} authentication sessions or empty map if no
+ * authentication sessions are present. Never return null.
*/
Map getAuthenticationSessions();
-
/**
- * @return authentication session for particular client and tab or null if it doesn't yet exists.
+ * Returns an authentication session for the particular client and tab or null if it doesn't yet exists.
+ * @param client {@code ClientModel} If {@code null} is provided the method will return {@code null}.
+ * @param tabId {@code String} If {@code null} is provided the method will return {@code null}.
+ * @return {@code AuthenticationSessionModel} or {@code null} in no authentication session is found.
*/
AuthenticationSessionModel getAuthenticationSession(ClientModel client, String tabId);
-
/**
- * Create new authentication session and returns it. Overwrites existing session for particular client if already exists.
- *
- * @param client
- * @return non-null fresh authentication session
+ * Create a new authentication session and returns it. Overwrites existing session for particular client if already exists.
+ * @param client {@code ClientModel} Can't be {@code null}.
+ * @return {@code AuthenticationSessionModel} non-null fresh authentication session. Never returns {@code null}.
*/
AuthenticationSessionModel createAuthenticationSession(ClientModel client);
/**
- * Removes authentication session from root authentication session.
+ * Removes the authentication session specified by tab id from the root authentication session.
* If there's no child authentication session left in the root authentication session, it's removed as well.
- * @param tabId String
+ * @param tabId {@code String} Can't be {@code null}.
*/
void removeAuthenticationSessionByTabId(String tabId);
/**
* Will completely restart whole state of authentication session. It will just keep same ID. It will setup it with provided realm.
+ * @param realm {@code RealmModel} Associated realm to the root authentication session.
*/
void restartSession(RealmModel realm);
diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
index b41e17c0c9..c4523a276f 100644
--- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
+++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java
@@ -306,7 +306,7 @@ public class AuthorizationTokenService {
if (rootAuthSession == null) {
if (userSessionModel.getUser().getServiceAccountClientLink() == null) {
- rootAuthSession = keycloakSession.authenticationSessions().createRootAuthenticationSession(userSessionModel.getId(), realm);
+ rootAuthSession = keycloakSession.authenticationSessions().createRootAuthenticationSession(realm, userSessionModel.getId());
} else {
// if the user session is associated with a service account
rootAuthSession = new AuthenticationSessionManager(keycloakSession).createAuthenticationSession(realm, false);
diff --git a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
index 6d02d12c47..aef3420758 100755
--- a/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
+++ b/services/src/main/java/org/keycloak/protocol/AuthorizationEndpointBase.java
@@ -196,7 +196,7 @@ public abstract class AuthorizationEndpointBase {
AuthenticationManager.backchannelLogout(session, userSession, true);
} else {
String userSessionId = userSession.getId();
- rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(userSessionId, realm);
+ rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(realm, userSessionId);
authSession = rootAuthSession.createAuthenticationSession(client);
logger.debugf("Sent request to authz endpoint. We don't have root authentication session with ID '%s' but we have userSession." +
"Re-created root authentication session with same ID. Client is: %s . New authentication session tab ID: %s", userSessionId, client.getClientId(), authSession.getTabId());
diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
index 2d979f3731..f3646f0c0c 100755
--- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java
@@ -314,7 +314,7 @@ public class AuthenticationManager {
}
if (rootLogoutSession == null) {
- rootLogoutSession = session.authenticationSessions().createRootAuthenticationSession(authSessionId, realm);
+ rootLogoutSession = session.authenticationSessions().createRootAuthenticationSession(realm, authSessionId);
}
if (browserCookie && !browserCookiePresent) {
// Update cookie if needed
diff --git a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
index 686524d0d6..6c62c851b0 100644
--- a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
+++ b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java
@@ -17,31 +17,22 @@
package org.keycloak.services.managers;
+import org.jboss.logging.Logger;
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.CommonClientSessionModel;
+import org.keycloak.sessions.RootAuthenticationSessionModel;
+
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
-import java.util.UUID;
import java.util.function.Supplier;
-import javax.crypto.SecretKey;
-
-import org.jboss.logging.Logger;
-import org.keycloak.common.util.Base64Url;
-import org.keycloak.common.util.Time;
-import org.keycloak.events.Details;
-import org.keycloak.events.EventBuilder;
-import org.keycloak.jose.jwe.JWEException;
-import org.keycloak.models.AuthenticatedClientSessionModel;
-import org.keycloak.models.ClientModel;
-import org.keycloak.models.CodeToTokenStoreProvider;
-import org.keycloak.models.KeycloakSession;
-import org.keycloak.models.RealmModel;
-import org.keycloak.models.UserSessionModel;
-import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.sessions.CommonClientSessionModel;
-import org.keycloak.sessions.AuthenticationSessionModel;
-import org.keycloak.util.TokenUtil;
-
/**
* TODO: Remove this and probably also ClientSessionParser. It's uneccessary genericity and abstraction, which is not needed anymore when clientSessionModel was fully removed.
*
@@ -110,6 +101,18 @@ class CodeGenerateUtil {
if (nextCode == null) {
String actionId = Base64Url.encode(KeycloakModelUtils.generateSecret());
authSession.setAuthNote(ACTIVE_CODE, actionId);
+
+ // We need to set the active code to the authSession in the separate sub-transaction as well
+ // to make sure the change is committed if the main transaction is rolled back later.
+ KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession currentSession) -> {
+ final RootAuthenticationSessionModel rootAuthenticationSession = currentSession.authenticationSessions()
+ .getRootAuthenticationSession(authSession.getRealm(), authSession.getParentSession().getId());
+ AuthenticationSessionModel authenticationSession = rootAuthenticationSession == null ? null : rootAuthenticationSession
+ .getAuthenticationSession(authSession.getClient(), authSession.getTabId());
+ if (authenticationSession != null) {
+ authenticationSession.setAuthNote(ACTIVE_CODE, actionId);
+ }
+ });
nextCode = actionId;
} else {
logger.debug("Code already generated for authentication session, using same code");
@@ -135,6 +138,15 @@ class CodeGenerateUtil {
authSession.removeAuthNote(ACTIVE_CODE);
+ // We need to remove the active code from the authSession in the separate sub-transaction as well
+ // to make sure the change is committed if the main transaction is rolled back later.
+ KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession currentSession) -> {
+ AuthenticationSessionModel authenticationSession = currentSession.authenticationSessions()
+ .getRootAuthenticationSession(authSession.getRealm(), authSession.getParentSession().getId())
+ .getAuthenticationSession(authSession.getClient(), authSession.getTabId());
+ authenticationSession.removeAuthNote(ACTIVE_CODE);
+ });
+
return MessageDigest.isEqual(code.getBytes(), activeCode.getBytes());
}
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index 0568cd3dcd..f5cb6864b0 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -309,7 +309,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
// Auth session with ID corresponding to our userSession may already exists in some rare cases (EG. if some client tried to login in another browser tab with "prompt=login")
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realmModel, userSession.getId());
if (rootAuthSession == null) {
- rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(userSession.getId(), realmModel);
+ rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(realmModel, userSession.getId());
}
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
index 2723794cda..072de26ab3 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
@@ -56,6 +56,10 @@
"provider": "${keycloak.role.provider:jpa}"
},
+ "authenticationSessions": {
+ "provider": "${keycloak.authSession.provider:infinispan}"
+ },
+
"mapStorage": {
"provider": "${keycloak.mapStorage.provider:concurrenthashmap}",
"concurrenthashmap": {
diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json
index 37cecc4a3b..c0793b9123 100755
--- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json
+++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json
@@ -30,6 +30,10 @@
"provider": "${keycloak.role.provider:jpa}"
},
+ "authenticationSessions": {
+ "provider": "${keycloak.authSession.provider:infinispan}"
+ },
+
"mapStorage": {
"provider": "${keycloak.mapStorage.provider:concurrenthashmap}",
"concurrenthashmap": {