From f6be378ecae926c8ec0a5d2521e5021a06eb33da Mon Sep 17 00:00:00 2001 From: Martin Kanis Date: Wed, 25 Nov 2020 10:01:01 +0100 Subject: [PATCH] KEYCLOAK-14556 Authentication session map store --- .github/workflows/ci.yml | 2 +- ...finispanAuthenticationSessionProvider.java | 4 +- ...stractRootAuthenticationSessionEntity.java | 104 +++++++ ...bstractRootAuthenticationSessionModel.java | 57 ++++ .../MapAuthenticationSessionAdapter.java | 258 ++++++++++++++++++ ...henticationSessionAuthNoteUpdateEvent.java | 72 +++++ .../MapAuthenticationSessionEntity.java | 134 +++++++++ .../MapRootAuthenticationSessionAdapter.java | 128 +++++++++ .../MapRootAuthenticationSessionEntity.java | 33 +++ .../MapRootAuthenticationSessionProvider.java | 187 +++++++++++++ ...tAuthenticationSessionProviderFactory.java | 47 ++++ .../map/storage/MapKeycloakTransaction.java | 4 +- ...sions.AuthenticationSessionProviderFactory | 18 ++ .../sessions/AuthenticationSessionModel.java | 135 ++++++--- .../AuthenticationSessionProvider.java | 55 +++- .../RootAuthenticationSessionModel.java | 54 ++-- .../AuthorizationTokenService.java | 2 +- .../protocol/AuthorizationEndpointBase.java | 2 +- .../managers/AuthenticationManager.java | 2 +- .../services/managers/CodeGenerateUtil.java | 52 ++-- .../resources/IdentityBrokerService.java | 2 +- .../resources/META-INF/keycloak-server.json | 4 + .../resources/META-INF/keycloak-server.json | 4 + 23 files changed, 1275 insertions(+), 85 deletions(-) create mode 100644 model/map/src/main/java/org/keycloak/models/map/authSession/AbstractRootAuthenticationSessionEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authSession/AbstractRootAuthenticationSessionModel.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authSession/MapAuthenticationSessionAdapter.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authSession/MapAuthenticationSessionAuthNoteUpdateEvent.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authSession/MapAuthenticationSessionEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionAdapter.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionEntity.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProvider.java create mode 100644 model/map/src/main/java/org/keycloak/models/map/authSession/MapRootAuthenticationSessionProviderFactory.java create mode 100644 model/map/src/main/resources/META-INF/services/org.keycloak.sessions.AuthenticationSessionProviderFactory 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": {