KEYCLOAK-14556 Authentication session map store

This commit is contained in:
Martin Kanis 2020-11-25 10:01:01 +01:00 committed by Hynek Mlnařík
parent 7f916ad20c
commit f6be378eca
23 changed files with 1275 additions and 85 deletions

View file

@ -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"

View file

@ -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());

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public abstract class AbstractRootAuthenticationSessionEntity<K> implements AbstractEntity<K> {
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<String, MapAuthenticationSessionEntity> 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<String, MapAuthenticationSessionEntity> getAuthenticationSessions() {
return authenticationSessions;
}
public void setAuthenticationSessions(Map<String, MapAuthenticationSessionEntity> 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();
}
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public abstract class AbstractRootAuthenticationSessionModel<E extends AbstractEntity> 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();
}
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
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<String, ExecutionStatus> 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<String> 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<String, String> 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<String, String> getClientNotes() {
return new ConcurrentHashMap<>(entity.getClientNotes());
}
@Override
public void clearClientNotes() {
parent.setUpdated(!entity.getClientNotes().isEmpty());
entity.getClientNotes().clear();
}
@Override
public Set<String> getClientScopes() {
return new HashSet<>(entity.getClientScopes());
}
@Override
public void setClientScopes(Set<String> 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);
}
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapAuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
private String authSessionId;
private String tabId;
private String clientUUID;
private Map<String, String> 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<String, String> 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<String, String> getAuthNotesFragment() {
return authNotesFragment;
}
@Override
public String toString() {
return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, tabId=%s, clientUUID=%s, authNotesFragment=%s ]",
authSessionId, clientUUID, authNotesFragment);
}
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapAuthenticationSessionEntity {
private String clientUUID;
private String authUserId;
private String redirectUri;
private String action;
private Set<String> clientScopes = new HashSet<>();
private Map<String, AuthenticationSessionModel.ExecutionStatus> executionStatus = new ConcurrentHashMap<>();
private String protocol;
private Map<String, String> clientNotes= new ConcurrentHashMap<>();;
private Map<String, String> authNotes = new ConcurrentHashMap<>();;
private Set<String> requiredActions = new HashSet<>();
private Map<String, String> userSessionNotes = new ConcurrentHashMap<>();
public Map<String, String> getUserSessionNotes() {
return userSessionNotes;
}
public void setUserSessionNotes(Map<String, String> 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<String> getClientScopes() {
return clientScopes;
}
public void setClientScopes(Set<String> clientScopes) {
this.clientScopes = clientScopes;
}
public Set<String> getRequiredActions() {
return requiredActions;
}
public void setRequiredActions(Set<String> requiredActions) {
this.requiredActions = requiredActions;
}
public String getProtocol() {
return protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public Map<String, String> getClientNotes() {
return clientNotes;
}
public void setClientNotes(Map<String, String> clientNotes) {
this.clientNotes = clientNotes;
}
public Map<String, String> getAuthNotes() {
return authNotes;
}
public void setAuthNotes(Map<String, String> authNotes) {
this.authNotes = authNotes;
}
public Map<String, AuthenticationSessionModel.ExecutionStatus> getExecutionStatus() {
return executionStatus;
}
public void setExecutionStatus(Map<String, AuthenticationSessionModel.ExecutionStatus> executionStatus) {
this.executionStatus = executionStatus;
}
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapRootAuthenticationSessionAdapter extends AbstractRootAuthenticationSessionModel<MapRootAuthenticationSessionEntity> {
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<String, AuthenticationSessionModel> 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));
}
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapRootAuthenticationSessionEntity extends AbstractRootAuthenticationSessionEntity<UUID> {
protected MapRootAuthenticationSessionEntity() {
super();
}
public MapRootAuthenticationSessionEntity(UUID id, String realmId) {
super(id, realmId);
}
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapRootAuthenticationSessionProvider implements AuthenticationSessionProvider {
private static final Logger LOG = Logger.getLogger(MapRootAuthenticationSessionProvider.class);
private final KeycloakSession session;
protected final MapKeycloakTransaction<UUID, MapRootAuthenticationSessionEntity> tx;
private final MapStorage<UUID, MapRootAuthenticationSessionEntity> sessionStore;
private static final Predicate<MapRootAuthenticationSessionEntity> ALWAYS_FALSE = role -> false;
private static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS";
public MapRootAuthenticationSessionProvider(KeycloakSession session, MapStorage<UUID, MapRootAuthenticationSessionEntity> sessionStore) {
this.session = session;
this.sessionStore = sessionStore;
this.tx = new MapKeycloakTransaction<>(sessionStore);
session.getTransactionManager().enlistAfterCompletion(tx);
}
private Function<MapRootAuthenticationSessionEntity, RootAuthenticationSessionModel> 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<MapRootAuthenticationSessionEntity> 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<UUID> 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<String, String> 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() {
}
}

View file

@ -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 <a href="mailto:mkanis@redhat.com">Martin Kanis</a>
*/
public class MapRootAuthenticationSessionProviderFactory extends AbstractMapProviderFactory<AuthenticationSessionProvider>
implements AuthenticationSessionProviderFactory {
private MapStorage<UUID, MapRootAuthenticationSessionEntity> 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);
}
}

View file

@ -115,7 +115,7 @@ public class MapKeycloakTransaction<K, V> 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<K, V> implements KeycloakTransaction {
}
public void putIfChanged(K key, V value, Predicate<V> 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<K, V> op = new MapTaskWithValue<K, V>(value) {

View file

@ -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

View file

@ -37,40 +37,87 @@ 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<String, ExecutionStatus>} Never returns {@code null}.
*/
Map<String, ExecutionStatus> 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<String>} Never returns {@code null}.
*/
Set<String> 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.
* @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.
* @return {@code Map<String, String>} never returns {@code null}
*/
Map<String, String> 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.
@ -80,48 +127,66 @@ public interface AuthenticationSessionModel extends CommonClientSessionModel {
/**
* 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
* 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
* 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
* 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
* 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.
* @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.
* @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.
* @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.
* @return {@code Map<String, String>} never returns {@code null}.
*/
Map<String, String> getClientNotes();
/**
* Clears all client notes. Client notes are notes
* specific to client protocol. They are NOT cleared when authentication session is restarted.
@ -129,12 +194,14 @@ public interface AuthenticationSessionModel extends CommonClientSessionModel {
void clearClientNotes();
/**
* Get client scope IDs
* Gets client scope IDs from the authentication session.
* @return {@code Set<String>} never returns {@code null}.
*/
Set<String> getClientScopes();
/**
* Set client scope IDs
* Sets client scope IDs to the authentication session.
* @param clientScopes {@code Set<String>} Can't be {@code null}.
*/
void setClientScopes(Set<String> clientScopes);

View file

@ -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<String, String>} 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<String, String> authNotesFragment);
}

View file

@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
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<String, AuthenticationSessionModel>} authentication sessions or empty map if no
* authentication sessions are present. Never return null.
*/
Map<String, AuthenticationSessionModel> 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);

View file

@ -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);

View file

@ -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());

View file

@ -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

View file

@ -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());
}

View file

@ -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);

View file

@ -56,6 +56,10 @@
"provider": "${keycloak.role.provider:jpa}"
},
"authenticationSessions": {
"provider": "${keycloak.authSession.provider:infinispan}"
},
"mapStorage": {
"provider": "${keycloak.mapStorage.provider:concurrenthashmap}",
"concurrenthashmap": {

View file

@ -30,6 +30,10 @@
"provider": "${keycloak.role.provider:jpa}"
},
"authenticationSessions": {
"provider": "${keycloak.authSession.provider:infinispan}"
},
"mapStorage": {
"provider": "${keycloak.mapStorage.provider:concurrenthashmap}",
"concurrenthashmap": {