From 7b03eed9c802e07b4bb8d9a5c0d719de18e1d078 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 28 Nov 2017 17:21:58 +0100 Subject: [PATCH] KEYCLOAK-5797 Refactoring authenticationSessions to support login in multiple browser tabs with different clients --- ...henticationSessionAuthNoteUpdateEvent.java | 14 +- .../AuthenticationSessionAdapter.java | 59 ++----- ...finispanAuthenticationSessionProvider.java | 93 ++++++----- ...nAuthenticationSessionProviderFactory.java | 16 +- .../RootAuthenticationSessionAdapter.java | 112 ++++++++++++++ .../entities/AuthenticationSessionEntity.java | 60 +------ .../RootAuthenticationSessionEntity.java | 77 +++++++++ .../AuthenticationSessionPredicate.java | 146 ------------------ .../RootAuthenticationSessionPredicate.java | 100 ++++++++++++ .../AuthenticatedClientSessionModel.java | 5 + .../sessions/AuthenticationSessionModel.java | 9 +- .../AuthenticationSessionProvider.java | 12 +- .../sessions/CommonClientSessionModel.java | 4 - .../RootAuthenticationSessionModel.java | 67 ++++++++ .../AuthenticationProcessor.java | 41 +++-- .../RequiredActionContextResult.java | 2 +- .../actiontoken/ActionTokenContext.java | 5 +- .../ExecuteActionsActionTokenHandler.java | 4 +- .../IdpVerifyAccountLinkActionToken.java | 15 +- ...dpVerifyAccountLinkActionTokenHandler.java | 15 +- .../VerifyEmailActionTokenHandler.java | 4 +- .../IdpEmailVerificationAuthenticator.java | 7 +- .../resetcred/ResetCredentialEmail.java | 4 +- .../requiredactions/VerifyEmail.java | 4 +- .../admin/PolicyEvaluationService.java | 3 +- .../protocol/AuthorizationEndpointBase.java | 89 ++++++----- .../keycloak/protocol/RestartLoginCookie.java | 17 +- .../oidc/endpoints/TokenEndpoint.java | 18 ++- .../main/java/org/keycloak/services/Urls.java | 6 +- .../managers/AuthenticationManager.java | 22 ++- .../AuthenticationSessionManager.java | 38 +++-- .../services/managers/ClientSessionCode.java | 21 ++- .../services/managers/CodeGenerateUtil.java | 32 +++- .../resources/IdentityBrokerService.java | 8 +- .../resources/LoginActionsService.java | 21 ++- .../resources/LoginActionsServiceChecks.java | 18 ++- .../services/resources/SessionCodeChecks.java | 31 ++-- .../resources/account/AccountFormService.java | 4 +- .../twitter/TwitterIdentityProvider.java | 5 +- .../keycloak/testsuite/util/MailUtils.java | 5 +- .../RequiredActionEmailVerificationTest.java | 20 +-- .../LastSessionRefreshCrossDCTest.java | 4 - .../crossdc/SessionExpirationCrossDCTest.java | 19 --- .../forms/MultipleTabsLoginTest.java | 32 ++++ .../testsuite/forms/RestartCookieTest.java | 2 +- .../keycloak/testsuite/util/MailAssert.java | 4 + .../broker/AbstractIdentityProviderTest.java | 9 +- .../AuthenticationSessionProviderTest.java | 91 ++++++----- 48 files changed, 844 insertions(+), 550 deletions(-) create mode 100644 model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/RootAuthenticationSessionAdapter.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/RootAuthenticationSessionEntity.java delete mode 100644 model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/AuthenticationSessionPredicate.java create mode 100644 model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/RootAuthenticationSessionPredicate.java create mode 100644 server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java index 34e22e3277..b29922bbe2 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java @@ -36,6 +36,8 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent { private String authSessionId; + private String clientUUID; + private Map authNotesFragment; /** @@ -44,9 +46,10 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent { * @param authNotesFragment * @return Event. Note that {@code authNotesFragment} property is not thread safe which is fine for now. */ - public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, Map authNotesFragment) { + public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, String clientUUID, Map authNotesFragment) { AuthenticationSessionAuthNoteUpdateEvent event = new AuthenticationSessionAuthNoteUpdateEvent(); event.authSessionId = authSessionId; + event.clientUUID = clientUUID; event.authNotesFragment = new LinkedHashMap<>(authNotesFragment); return event; } @@ -55,13 +58,18 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent { return authSessionId; } + public String getClientUUID() { + return clientUUID; + } + public Map getAuthNotesFragment() { return authNotesFragment; } @Override public String toString() { - return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, authNotesFragment=%s ]", authSessionId, authNotesFragment); + return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, clientUUID=%s, authNotesFragment=%s ]", + authSessionId, clientUUID, authNotesFragment); } public static class ExternalizerImpl implements Externalizer { @@ -73,6 +81,7 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent { output.writeByte(VERSION_1); MarshallUtil.marshallString(value.authSessionId, output); + MarshallUtil.marshallString(value.clientUUID, output); MarshallUtil.marshallMap(value.authNotesFragment, output); } @@ -88,6 +97,7 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent { public AuthenticationSessionAuthNoteUpdateEvent readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException { return create( + MarshallUtil.unmarshallString(input), MarshallUtil.unmarshallString(input), MarshallUtil.unmarshallMap(input, HashMap::new) ); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java index 5736188623..9ab59456bc 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java @@ -23,14 +23,13 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import org.infinispan.Cache; -import org.keycloak.common.util.Time; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.RootAuthenticationSessionModel; /** * NOTE: Calling setter doesn't automatically enlist for update @@ -39,39 +38,37 @@ import org.keycloak.sessions.AuthenticationSessionModel; */ public class AuthenticationSessionAdapter implements AuthenticationSessionModel { - private KeycloakSession session; - private InfinispanAuthenticationSessionProvider provider; - private Cache cache; - private RealmModel realm; + private final KeycloakSession session; + private final RootAuthenticationSessionAdapter parent; + private final String clientUUID; private AuthenticationSessionEntity entity; - public AuthenticationSessionAdapter(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, Cache cache, RealmModel realm, - AuthenticationSessionEntity entity) { + public AuthenticationSessionAdapter(KeycloakSession session, RootAuthenticationSessionAdapter parent, String clientUUID, AuthenticationSessionEntity entity) { this.session = session; - this.provider = provider; - this.cache = cache; - this.realm = realm; + this.parent = parent; + this.clientUUID = clientUUID; this.entity = entity; } - void update() { - provider.tx.replace(cache, entity.getId(), entity); + private void update() { + parent.update(); } @Override - public String getId() { - return entity.getId(); + public RootAuthenticationSessionModel getParentSession() { + return parent; } + @Override public RealmModel getRealm() { - return realm; + return parent.getRealm(); } @Override public ClientModel getClient() { - return realm.getClientById(entity.getClientUuid()); + return getRealm().getClientById(clientUUID); } @Override @@ -85,16 +82,6 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel update(); } - @Override - public int getTimestamp() { - return entity.getTimestamp(); - } - - @Override - public void setTimestamp(int timestamp) { - entity.setTimestamp(timestamp); - update(); - } @Override public String getAction() { @@ -303,7 +290,7 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel @Override public UserModel getAuthenticatedUser() { - return entity.getAuthUserId() == null ? null : session.users().getUserById(entity.getAuthUserId(), realm); } + return entity.getAuthUserId() == null ? null : session.users().getUserById(entity.getAuthUserId(), getRealm()); } @Override public void setAuthenticatedUser(UserModel user) { @@ -312,20 +299,4 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel update(); } - @Override - public void updateClient(ClientModel client) { - entity.setClientUuid(client.getId()); - update(); - } - - @Override - public void restartSession(RealmModel realm, ClientModel client) { - String id = entity.getId(); - entity = new AuthenticationSessionEntity(); - entity.setId(id); - entity.setRealmId(realm.getId()); - entity.setClientUuid(client.getId()); - entity.setTimestamp(Time.currentTime()); - update(); - } } 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 5ae841632d..47174f8fb5 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 @@ -28,15 +28,14 @@ import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent; -import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; -import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent; +import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity; import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; import org.keycloak.models.sessions.infinispan.events.SessionEventsSenderTransaction; -import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate; +import org.keycloak.models.sessions.infinispan.stream.RootAuthenticationSessionPredicate; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RealmInfoUtil; -import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.AuthenticationSessionProvider; +import org.keycloak.sessions.RootAuthenticationSessionModel; /** * @author Marek Posolda @@ -46,11 +45,11 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProvider.class); private final KeycloakSession session; - private final Cache cache; + private final Cache cache; protected final InfinispanKeycloakTransaction tx; protected final SessionEventsSenderTransaction clusterEventsSenderTx; - public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache cache) { + public InfinispanAuthenticationSessionProvider(KeycloakSession session, Cache cache) { this.session = session; this.cache = cache; @@ -62,38 +61,33 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe } @Override - public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client) { + public RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm) { String id = KeycloakModelUtils.generateId(); - return createAuthenticationSession(id, realm, client); + return createRootAuthenticationSession(id, realm); } + @Override - public AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client) { - AuthenticationSessionEntity entity = new AuthenticationSessionEntity(); + public RootAuthenticationSessionModel createRootAuthenticationSession(String id, RealmModel realm) { + RootAuthenticationSessionEntity entity = new RootAuthenticationSessionEntity(); entity.setId(id); entity.setRealmId(realm.getId()); entity.setTimestamp(Time.currentTime()); - entity.setClientUuid(client.getId()); tx.put(cache, id, entity); - AuthenticationSessionAdapter wrap = wrap(realm, entity); - return wrap; - } - - private AuthenticationSessionAdapter wrap(RealmModel realm, AuthenticationSessionEntity entity) { - return entity==null ? null : new AuthenticationSessionAdapter(session, this, cache, realm, entity); - } - - @Override - public AuthenticationSessionModel getAuthenticationSession(RealmModel realm, String authenticationSessionId) { - AuthenticationSessionEntity entity = getAuthenticationSessionEntity(realm, authenticationSessionId); return wrap(realm, entity); } - private AuthenticationSessionEntity getAuthenticationSessionEntity(RealmModel realm, String authSessionId) { + + private RootAuthenticationSessionAdapter wrap(RealmModel realm, RootAuthenticationSessionEntity entity) { + return entity==null ? null : new RootAuthenticationSessionAdapter(session, this, cache, realm, entity); + } + + + private RootAuthenticationSessionEntity getRootAuthenticationSessionEntity(RealmModel realm, String authSessionId) { // Chance created in this transaction - AuthenticationSessionEntity entity = tx.get(cache, authSessionId); + RootAuthenticationSessionEntity entity = tx.get(cache, authSessionId); if (entity == null) { entity = cache.get(authSessionId); @@ -102,10 +96,6 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe return entity; } - @Override - public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authenticationSession) { - tx.remove(cache, authenticationSession.getId()); - } @Override public void removeExpired(RealmModel realm) { @@ -115,16 +105,16 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe // Each cluster node cleanups just local sessions, which are those owned by himself (+ few more taking l1 cache into account) - Iterator> itr = CacheDecorators.localCache(cache) + Iterator> itr = CacheDecorators.localCache(cache) .entrySet() .stream() - .filter(AuthenticationSessionPredicate.create(realm.getId()).expired(expired)) + .filter(RootAuthenticationSessionPredicate.create(realm.getId()).expired(expired)) .iterator(); int counter = 0; while (itr.hasNext()) { counter++; - AuthenticationSessionEntity entity = itr.next().getValue(); + RootAuthenticationSessionEntity entity = itr.next().getValue(); tx.remove(CacheDecorators.localCache(cache), entity.getId()); } @@ -141,10 +131,10 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe } protected void onRealmRemovedEvent(String realmId) { - Iterator> itr = CacheDecorators.localCache(cache) + Iterator> itr = CacheDecorators.localCache(cache) .entrySet() .stream() - .filter(AuthenticationSessionPredicate.create(realmId)) + .filter(RootAuthenticationSessionPredicate.create(realmId)) .iterator(); while (itr.hasNext()) { @@ -156,28 +146,20 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe @Override public void onClientRemoved(RealmModel realm, ClientModel client) { - // Send message to all DCs. The remoteCache will notify client listeners on all DCs for remove authentication sessions of this client - clusterEventsSenderTx.addEvent( - ClientRemovedSessionEvent.create(session, InfinispanAuthenticationSessionProviderFactory.CLIENT_REMOVED_AUTHSESSION_EVENT, realm.getId(), false, client.getId()), - ClusterProvider.DCNotify.ALL_DCS); + // No update anything on clientRemove for now. AuthenticationSessions of removed client will be handled at runtime if needed. + +// clusterEventsSenderTx.addEvent( +// ClientRemovedSessionEvent.create(session, InfinispanAuthenticationSessionProviderFactory.CLIENT_REMOVED_AUTHSESSION_EVENT, realm.getId(), false, client.getId()), +// ClusterProvider.DCNotify.ALL_DCS); } protected void onClientRemovedEvent(String realmId, String clientUuid) { - Iterator> itr = CacheDecorators.localCache(cache) - .entrySet() - .stream() - .filter(AuthenticationSessionPredicate.create(realmId).client(clientUuid)) - .iterator(); - while (itr.hasNext()) { - CacheDecorators.localCache(cache) - .remove(itr.next().getKey()); - } } @Override - public void updateNonlocalSessionAuthNotes(String authSessionId, Map authNotesFragment) { + public void updateNonlocalSessionAuthNotes(String authSessionId, ClientModel client, Map authNotesFragment) { if (authSessionId == null) { return; } @@ -185,18 +167,31 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe ClusterProvider cluster = session.getProvider(ClusterProvider.class); cluster.notify( InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS, - AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, authNotesFragment), + AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, client.getId(), authNotesFragment), true, ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC ); } + + @Override + public RootAuthenticationSessionModel getRootAuthenticationSession(RealmModel realm, String authenticationSessionId) { + RootAuthenticationSessionEntity entity = getRootAuthenticationSessionEntity(realm, authenticationSessionId); + return wrap(realm, entity); + } + + + @Override + public void removeRootAuthenticationSession(RealmModel realm, RootAuthenticationSessionModel authenticationSession) { + tx.remove(cache, authenticationSession.getId()); + } + @Override public void close() { } - public Cache getCache() { + public Cache getCache() { return cache; } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java index 04e1dc8c18..405588558f 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java @@ -26,6 +26,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent; import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity; import org.keycloak.models.sessions.infinispan.events.AbstractAuthSessionClusterListener; import org.keycloak.models.sessions.infinispan.events.ClientRemovedSessionEvent; import org.keycloak.models.sessions.infinispan.events.RealmRemovedSessionEvent; @@ -46,7 +47,7 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProviderFactory.class); - private volatile Cache authSessionsCache; + private volatile Cache authSessionsCache; public static final String PROVIDER_ID = "infinispan"; @@ -113,11 +114,18 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic } AuthenticationSessionAuthNoteUpdateEvent event = (AuthenticationSessionAuthNoteUpdateEvent) clEvent; - AuthenticationSessionEntity authSession = this.authSessionsCache.get(event.getAuthSessionId()); - updateAuthSession(authSession, event.getAuthNotesFragment()); + RootAuthenticationSessionEntity authSession = this.authSessionsCache.get(event.getAuthSessionId()); + updateAuthSession(authSession, event.getClientUUID(), event.getAuthNotesFragment()); } - private static void updateAuthSession(AuthenticationSessionEntity authSession, Map authNotesFragment) { + + private static void updateAuthSession(RootAuthenticationSessionEntity rootAuthSession, String clientUUID, Map authNotesFragment) { + if (rootAuthSession == null) { + return; + } + + AuthenticationSessionEntity authSession = rootAuthSession.getAuthenticationSessions().get(clientUUID); + if (authSession != null) { if (authSession.getAuthNotes() == null) { authSession.setAuthNotes(new ConcurrentHashMap<>()); diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/RootAuthenticationSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/RootAuthenticationSessionAdapter.java new file mode 100644 index 0000000000..e7cd72795e --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/RootAuthenticationSessionAdapter.java @@ -0,0 +1,112 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan; + +import java.util.HashMap; +import java.util.Map; + +import org.infinispan.Cache; +import org.keycloak.common.util.Time; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; +import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.RootAuthenticationSessionModel; + +/** + * @author Marek Posolda + */ +public class RootAuthenticationSessionAdapter implements RootAuthenticationSessionModel { + + private KeycloakSession session; + private InfinispanAuthenticationSessionProvider provider; + private Cache cache; + private RealmModel realm; + private RootAuthenticationSessionEntity entity; + + public RootAuthenticationSessionAdapter(KeycloakSession session, InfinispanAuthenticationSessionProvider provider, + Cache cache, RealmModel realm, + RootAuthenticationSessionEntity entity) { + this.session = session; + this.provider = provider; + this.cache = cache; + this.realm = realm; + this.entity = entity; + } + + void update() { + provider.tx.replace(cache, entity.getId(), entity); + } + + + @Override + public String getId() { + return entity.getId(); + } + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public int getTimestamp() { + return entity.getTimestamp(); + } + + @Override + public void setTimestamp(int timestamp) { + entity.setTimestamp(timestamp); + update(); + } + + @Override + public Map getAuthenticationSessions() { + Map result = new HashMap<>(); + + for (Map.Entry entry : entity.getAuthenticationSessions().entrySet()) { + String clientUUID = entry.getKey(); + result.put(clientUUID , new AuthenticationSessionAdapter(session, this, clientUUID, entry.getValue())); + } + + return result; + } + + @Override + public AuthenticationSessionModel getAuthenticationSession(ClientModel client) { + return client==null ? null : getAuthenticationSessions().get(client.getId()); + } + + @Override + public AuthenticationSessionModel createAuthenticationSession(ClientModel client) { + AuthenticationSessionEntity authSessionEntity = new AuthenticationSessionEntity(); + entity.getAuthenticationSessions().put(client.getId(), authSessionEntity); + update(); + + return new AuthenticationSessionAdapter(session, this, client.getId(), authSessionEntity); + } + + @Override + public void restartSession(RealmModel realm) { + entity.getAuthenticationSessions().clear(); + entity.setTimestamp(Time.currentTime()); + update(); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java index 3d7d9af3d5..710f17a7b5 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/AuthenticationSessionEntity.java @@ -17,53 +17,34 @@ package org.keycloak.models.sessions.infinispan.entities; -import java.util.HashMap; -import java.util.HashSet; +import java.io.Serializable; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.infinispan.util.concurrent.ConcurrentHashSet; import org.keycloak.sessions.AuthenticationSessionModel; /** * @author Marek Posolda */ -public class AuthenticationSessionEntity extends SessionEntity { +public class AuthenticationSessionEntity implements Serializable { - private String id; - - private String clientUuid; private String authUserId; private String redirectUri; - private int timestamp; private String action; private Set roles; private Set protocolMappers; - private Map executionStatus = new HashMap<>(); + private Map executionStatus = new ConcurrentHashMap<>(); private String protocol; private Map clientNotes; private Map authNotes; - private Set requiredActions = new HashSet<>(); + private Set requiredActions = new ConcurrentHashSet<>(); private Map userSessionNotes; - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getClientUuid() { - return clientUuid; - } - - public void setClientUuid(String clientUuid) { - this.clientUuid = clientUuid; - } - public String getAuthUserId() { return authUserId; } @@ -80,14 +61,6 @@ public class AuthenticationSessionEntity extends SessionEntity { this.redirectUri = redirectUri; } - public int getTimestamp() { - return timestamp; - } - - public void setTimestamp(int timestamp) { - this.timestamp = timestamp; - } - public String getAction() { return action; } @@ -160,25 +133,4 @@ public class AuthenticationSessionEntity extends SessionEntity { this.authNotes = authNotes; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof AuthenticationSessionEntity)) return false; - - AuthenticationSessionEntity that = (AuthenticationSessionEntity) o; - - if (id != null ? !id.equals(that.id) : that.id != null) return false; - - return true; - } - - @Override - public int hashCode() { - return id != null ? id.hashCode() : 0; - } - - @Override - public String toString() { - return String.format("AuthenticationSessionEntity [id=%s, realm=%s, clientUuid=%s ]", getId(), getRealmId(), getClientUuid()); - } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/RootAuthenticationSessionEntity.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/RootAuthenticationSessionEntity.java new file mode 100644 index 0000000000..751b6f0d58 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/RootAuthenticationSessionEntity.java @@ -0,0 +1,77 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.entities; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author Marek Posolda + */ +public class RootAuthenticationSessionEntity extends SessionEntity { + + private String id; + private int timestamp; + private Map authenticationSessions = new ConcurrentHashMap<>(); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public int getTimestamp() { + return timestamp; + } + + public void setTimestamp(int timestamp) { + this.timestamp = timestamp; + } + + public Map getAuthenticationSessions() { + return authenticationSessions; + } + + public void setAuthenticationSessions(Map authenticationSessions) { + this.authenticationSessions = authenticationSessions; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RootAuthenticationSessionEntity)) return false; + + RootAuthenticationSessionEntity that = (RootAuthenticationSessionEntity) o; + + if (id != null ? !id.equals(that.id) : that.id != null) return false; + + return true; + } + + @Override + public int hashCode() { + return id != null ? id.hashCode() : 0; + } + + @Override + public String toString() { + return String.format("RootAuthenticationSessionEntity [ id=%s, realm=%s ]", getId(), getRealmId()); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/AuthenticationSessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/AuthenticationSessionPredicate.java deleted file mode 100644 index 30747daea8..0000000000 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/AuthenticationSessionPredicate.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2016 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.models.sessions.infinispan.stream; - -import java.util.Map; -import java.util.function.Predicate; - -import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity; -import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil; -import java.io.IOException; -import java.io.ObjectInput; -import java.io.ObjectOutput; -import org.infinispan.commons.marshall.Externalizer; -import org.infinispan.commons.marshall.MarshallUtil; -import org.infinispan.commons.marshall.SerializeWith; - -/** - * @author Marek Posolda - */ -@SerializeWith(AuthenticationSessionPredicate.ExternalizerImpl.class) -public class AuthenticationSessionPredicate implements Predicate> { - - private final String realm; - - private String client; - - private String user; - - private Integer expired; - - //private String brokerSessionId; - //private String brokerUserId; - - private AuthenticationSessionPredicate(String realm) { - this.realm = realm; - } - - public static AuthenticationSessionPredicate create(String realm) { - return new AuthenticationSessionPredicate(realm); - } - - public AuthenticationSessionPredicate user(String user) { - this.user = user; - return this; - } - - public AuthenticationSessionPredicate client(String client) { - this.client = client; - return this; - } - - public AuthenticationSessionPredicate expired(Integer expired) { - this.expired = expired; - return this; - } - -// public UserSessionPredicate brokerSessionId(String id) { -// this.brokerSessionId = id; -// return this; -// } - -// public UserSessionPredicate brokerUserId(String id) { -// this.brokerUserId = id; -// return this; -// } - - @Override - public boolean test(Map.Entry entry) { - AuthenticationSessionEntity entity = entry.getValue(); - - if (!realm.equals(entity.getRealmId())) { - return false; - } - - if (user != null && !entity.getAuthUserId().equals(user)) { - return false; - } - - if (client != null && !entity.getClientUuid().equals(client)) { - return false; - } - -// if (brokerSessionId != null && !brokerSessionId.equals(entity.getBrokerSessionId())) { -// return false; -// } -// -// if (brokerUserId != null && !brokerUserId.equals(entity.getBrokerUserId())) { -// return false; -// } - - if (expired != null && entity.getTimestamp() > expired) { - return false; - } - - return true; - } - - public static class ExternalizerImpl implements Externalizer { - - private static final int VERSION_1 = 1; - - @Override - public void writeObject(ObjectOutput output, AuthenticationSessionPredicate obj) throws IOException { - output.writeByte(VERSION_1); - - MarshallUtil.marshallString(obj.realm, output); - MarshallUtil.marshallString(obj.user, output); - MarshallUtil.marshallString(obj.client, output); - KeycloakMarshallUtil.marshall(obj.expired, output); - - } - - @Override - public AuthenticationSessionPredicate readObject(ObjectInput input) throws IOException, ClassNotFoundException { - switch (input.readByte()) { - case VERSION_1: - return readObjectVersion1(input); - default: - throw new IOException("Unknown version"); - } - } - - public AuthenticationSessionPredicate readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException { - AuthenticationSessionPredicate res = new AuthenticationSessionPredicate(MarshallUtil.unmarshallString(input)); - res.user(MarshallUtil.unmarshallString(input)); - res.client(MarshallUtil.unmarshallString(input)); - res.expired(KeycloakMarshallUtil.unmarshallInteger(input)); - return res; - } - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/RootAuthenticationSessionPredicate.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/RootAuthenticationSessionPredicate.java new file mode 100644 index 0000000000..8f3ae5d006 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/stream/RootAuthenticationSessionPredicate.java @@ -0,0 +1,100 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.sessions.infinispan.stream; + +import java.util.Map; +import java.util.function.Predicate; + +import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity; +import org.keycloak.models.sessions.infinispan.util.KeycloakMarshallUtil; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import org.infinispan.commons.marshall.Externalizer; +import org.infinispan.commons.marshall.MarshallUtil; +import org.infinispan.commons.marshall.SerializeWith; + +/** + * @author Marek Posolda + */ +@SerializeWith(RootAuthenticationSessionPredicate.ExternalizerImpl.class) +public class RootAuthenticationSessionPredicate implements Predicate> { + + private final String realm; + + private Integer expired; + + private RootAuthenticationSessionPredicate(String realm) { + this.realm = realm; + } + + public static RootAuthenticationSessionPredicate create(String realm) { + return new RootAuthenticationSessionPredicate(realm); + } + + public RootAuthenticationSessionPredicate expired(Integer expired) { + this.expired = expired; + return this; + } + + + @Override + public boolean test(Map.Entry entry) { + RootAuthenticationSessionEntity entity = entry.getValue(); + + if (!realm.equals(entity.getRealmId())) { + return false; + } + + if (expired != null && entity.getTimestamp() > expired) { + return false; + } + + return true; + } + + public static class ExternalizerImpl implements Externalizer { + + private static final int VERSION_1 = 1; + + @Override + public void writeObject(ObjectOutput output, RootAuthenticationSessionPredicate obj) throws IOException { + output.writeByte(VERSION_1); + + MarshallUtil.marshallString(obj.realm, output); + KeycloakMarshallUtil.marshall(obj.expired, output); + + } + + @Override + public RootAuthenticationSessionPredicate readObject(ObjectInput input) throws IOException, ClassNotFoundException { + switch (input.readByte()) { + case VERSION_1: + return readObjectVersion1(input); + default: + throw new IOException("Unknown version"); + } + } + + public RootAuthenticationSessionPredicate readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException { + RootAuthenticationSessionPredicate res = new RootAuthenticationSessionPredicate(MarshallUtil.unmarshallString(input)); + res.expired(KeycloakMarshallUtil.unmarshallInteger(input)); + return res; + } + } +} diff --git a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java index c54533c606..55628cb766 100644 --- a/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/models/AuthenticatedClientSessionModel.java @@ -27,6 +27,11 @@ import org.keycloak.sessions.CommonClientSessionModel; */ public interface AuthenticatedClientSessionModel extends CommonClientSessionModel { + String getId(); + + int getTimestamp(); + void setTimestamp(int timestamp); + /** * Detaches the client session from its user session. */ 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 8e84f1a445..56b83ac442 100644 --- a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionModel.java @@ -21,7 +21,6 @@ import java.util.Map; import java.util.Set; import org.keycloak.models.ClientModel; -import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; /** @@ -31,10 +30,8 @@ import org.keycloak.models.UserModel; */ public interface AuthenticationSessionModel extends CommonClientSessionModel { -// -// public UserSessionModel getUserSession(); -// public void setUserSession(UserSessionModel userSession); + RootAuthenticationSessionModel getParentSession(); Map getExecutionStatus(); void setExecutionStatus(String authenticator, ExecutionStatus status); @@ -125,8 +122,4 @@ public interface AuthenticationSessionModel extends CommonClientSessionModel { */ void clearClientNotes(); - void updateClient(ClientModel client); - - // Will completely restart whole state of authentication session. It will just keep same ID. It will setup it with provided realm and client. - void restartSession(RealmModel realm, ClientModel client); } 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 8cc40350d0..9a01446a93 100644 --- a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java +++ b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java @@ -31,26 +31,26 @@ 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. */ - AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client); + RootAuthenticationSessionModel createRootAuthenticationSession(RealmModel realm); - AuthenticationSessionModel createAuthenticationSession(String id, RealmModel realm, ClientModel client); + RootAuthenticationSessionModel createRootAuthenticationSession(String id, RealmModel realm); - AuthenticationSessionModel getAuthenticationSession(RealmModel realm, String authenticationSessionId); + RootAuthenticationSessionModel getRootAuthenticationSession(RealmModel realm, String authenticationSessionId); - void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authenticationSession); + void removeRootAuthenticationSession(RealmModel realm, RootAuthenticationSessionModel authenticationSession); void removeExpired(RealmModel realm); void onRealmRemoved(RealmModel realm); void onClientRemoved(RealmModel realm, ClientModel client); /** - * Requests update of authNotes of an authentication session that is not owned + * Requests update of authNotes of a root authentication session that is not owned * by this instance but might exist somewhere in the cluster. * * @param authSessionId * @param authNotesFragment Map with authNote values. Auth note is removed if the corresponding value in the map is {@code null}. */ - void updateNonlocalSessionAuthNotes(String authSessionId, Map authNotesFragment); + void updateNonlocalSessionAuthNotes(String authSessionId, ClientModel client, Map authNotesFragment); } diff --git a/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java index a87309c7c2..b59ffa16ca 100644 --- a/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java +++ b/server-spi/src/main/java/org/keycloak/sessions/CommonClientSessionModel.java @@ -33,13 +33,9 @@ public interface CommonClientSessionModel { public String getRedirectUri(); public void setRedirectUri(String uri); - public String getId(); public RealmModel getRealm(); public ClientModel getClient(); - public int getTimestamp(); - public void setTimestamp(int timestamp); - public String getAction(); public void setAction(String action); diff --git a/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java b/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java new file mode 100644 index 0000000000..fb656cf448 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/sessions/RootAuthenticationSessionModel.java @@ -0,0 +1,67 @@ +/* + * Copyright 2017 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.sessions; + +import java.util.Map; + +import org.keycloak.models.ClientModel; +import org.keycloak.models.RealmModel; + +/** + * Represents usually one browser session with potentially many browser tabs. Every browser tab is represented by {@link AuthenticationSessionModel} + * of different client. + * + * @author Marek Posolda + */ +public interface RootAuthenticationSessionModel { + + String getId(); + RealmModel getRealm(); + + int getTimestamp(); + void setTimestamp(int timestamp); + + + /** + * Key is client UUID, Value is AuthenticationSessionModel for particular client + * @return authentication sessions or empty map if no authenticationSessions presents. Never return null. + */ + Map getAuthenticationSessions(); + + + /** + * @return authentication session for particular client or null if it doesn't yet exists. + */ + AuthenticationSessionModel getAuthenticationSession(ClientModel client); + + + /** + * Create new authentication session and returns it. Overwrites existing session for particular client if already exists. + * + * @param client + * @return non-null fresh authentication session + */ + AuthenticationSessionModel createAuthenticationSession(ClientModel client); + + + /** + * Will completely restart whole state of authentication session. It will just keep same ID. It will setup it with provided realm. + */ + void restartSession(RealmModel realm); + +} diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index d033143795..4c0db68b11 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -56,6 +56,7 @@ import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.AuthenticationFlowURLHelper; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.CommonClientSessionModel; +import org.keycloak.sessions.RootAuthenticationSessionModel; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; @@ -222,7 +223,7 @@ public class AuthenticationProcessor { public String generateCode() { ClientSessionCode accessCode = new ClientSessionCode(session, getRealm(), getAuthenticationSession()); - authenticationSession.setTimestamp(Time.currentTime()); + authenticationSession.getParentSession().setTimestamp(Time.currentTime()); return accessCode.getOrGenerateCode(); } @@ -632,7 +633,10 @@ public class AuthenticationProcessor { } else if (e.getError() == AuthenticationFlowError.FORK_FLOW) { ForkFlowException reset = (ForkFlowException)e; - AuthenticationSessionModel clone = clone(session, authenticationSession); + + RootAuthenticationSessionModel rootClone = clone(session, authenticationSession.getClient(), authenticationSession.getParentSession()); + AuthenticationSessionModel clone = rootClone.getAuthenticationSession(authenticationSession.getClient()); + clone.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); setAuthenticationSession(clone); @@ -748,7 +752,7 @@ public class AuthenticationProcessor { public static void resetFlow(AuthenticationSessionModel authSession, String flowPath) { logger.debug("RESET FLOW"); - authSession.setTimestamp(Time.currentTime()); + authSession.getParentSession().setTimestamp(Time.currentTime()); authSession.setAuthenticatedUser(null); authSession.clearExecutionStatus(); authSession.clearUserSessionNotes(); @@ -759,20 +763,26 @@ public class AuthenticationProcessor { authSession.setAuthNote(CURRENT_FLOW_PATH, flowPath); } - public static AuthenticationSessionModel clone(KeycloakSession session, AuthenticationSessionModel authSession) { - AuthenticationSessionModel clone = new AuthenticationSessionManager(session).createAuthenticationSession(authSession.getRealm(), authSession.getClient(), true); + public static RootAuthenticationSessionModel clone(KeycloakSession session, ClientModel client, RootAuthenticationSessionModel authSession) { + RootAuthenticationSessionModel clone = new AuthenticationSessionManager(session).createAuthenticationSession(authSession.getRealm(), true); // Transfer just the client "notes", but not "authNotes" - for (Map.Entry entry : authSession.getClientNotes().entrySet()) { - clone.setClientNote(entry.getKey(), entry.getValue()); + for (Map.Entry entry : authSession.getAuthenticationSessions().entrySet()) { + AuthenticationSessionModel asmOrig = entry.getValue(); + AuthenticationSessionModel asmClone = clone.createAuthenticationSession(asmOrig.getClient()); + + asmClone.setRedirectUri(asmOrig.getRedirectUri()); + asmClone.setProtocol(asmOrig.getProtocol()); + + for (Map.Entry clientNote : asmOrig.getClientNotes().entrySet()) { + asmClone.setClientNote(clientNote.getKey(), clientNote.getValue()); + } } - clone.setRedirectUri(authSession.getRedirectUri()); - clone.setProtocol(authSession.getProtocol()); clone.setTimestamp(Time.currentTime()); - clone.setAuthNote(FORKED_FROM, authSession.getId()); - logger.debugf("Forked authSession %s from authSession %s", clone.getId(), authSession.getId()); + clone.getAuthenticationSession(client).setAuthNote(FORKED_FROM, authSession.getId()); + logger.debugf("Forked authSession %s from authSession %s . Client: '%s'", clone.getId(), authSession.getId(), client.getClientId()); return clone; @@ -825,7 +835,8 @@ public class AuthenticationProcessor { if (!code.isActionActive(ClientSessionCode.ActionType.LOGIN)) { throw new AuthenticationFlowException(AuthenticationFlowError.EXPIRED_CODE); } - authenticationSession.setTimestamp(Time.currentTime()); + + authenticationSession.getParentSession().setTimestamp(Time.currentTime()); } public Response authenticateOnly() throws AuthenticationFlowException { @@ -872,9 +883,9 @@ public class AuthenticationProcessor { if (userSession == null) { // if no authenticator attached a usersession - userSession = session.sessions().getUserSession(realm, authSession.getId()); + userSession = session.sessions().getUserSession(realm, authSession.getParentSession().getId()); if (userSession == null) { - userSession = session.sessions().createUserSession(authSession.getId(), realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol() + userSession = session.sessions().createUserSession(authSession.getParentSession().getId(), realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol() , remember, brokerSessionId, brokerUserId); } else if (userSession.getUser() == null || !AuthenticationManager.isSessionValid(realm, userSession)) { userSession.restartSession(realm, authSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), authSession.getProtocol() @@ -936,7 +947,7 @@ public class AuthenticationProcessor { if (nextRequiredAction != null) { return AuthenticationManager.redirectToRequiredActions(session, realm, authenticationSession, uriInfo, nextRequiredAction); } else { - event.detail(Details.CODE_ID, authenticationSession.getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set + event.detail(Details.CODE_ID, authenticationSession.getParentSession().getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set // the user has successfully logged in and we can clear his/her previous login failure attempts. logSuccess(); return AuthenticationManager.finishedRequiredActions(session, authenticationSession, userSession, connection, request, uriInfo, event); diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java index e87652a513..4b001dfbee 100755 --- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java +++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java @@ -149,7 +149,7 @@ public class RequiredActionContextResult implements RequiredActionContext { @Override public String generateCode() { ClientSessionCode accessCode = new ClientSessionCode<>(session, getRealm(), getAuthenticationSession()); - authenticationSession.setTimestamp(Time.currentTime()); + authenticationSession.getParentSession().setTimestamp(Time.currentTime()); return accessCode.getOrGenerateCode(); } diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java index 1550a8d374..29ce6c4f08 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java @@ -30,6 +30,7 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilderException; import javax.ws.rs.core.UriInfo; import org.jboss.resteasy.spi.HttpRequest; +import org.keycloak.sessions.RootAuthenticationSessionModel; /** * @@ -111,7 +112,9 @@ public class ActionTokenContext { // set up the account service as the endpoint to call. ClientModel client = realm.getClientByClientId(clientId == null ? Constants.ACCOUNT_MANAGEMENT_CLIENT_ID : clientId); - authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true); + RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, true); + authSession = rootAuthSession.createAuthenticationSession(client); + authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); String redirectUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString(); diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java index 43e4f902b6..f21cf6848b 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java @@ -75,8 +75,8 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander< final KeycloakSession session = tokenContext.getSession(); if (tokenContext.isAuthenticationSessionFresh()) { // Update the authentication session in the token - token.setAuthenticationSessionId(authSession.getId()); - UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo)); + token.setAuthenticationSessionId(authSession.getParentSession().getId()); + UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId()); String confirmUri = builder.build(realm.getName()).toString(); return session.getProvider(LoginFormsProvider.class) diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java index 39c6f9ae34..64c61dd51d 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java @@ -31,6 +31,7 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken { private static final String JSON_FIELD_IDENTITY_PROVIDER_USERNAME = "idpu"; private static final String JSON_FIELD_IDENTITY_PROVIDER_ALIAS = "idpa"; private static final String JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID = "oasid"; + private static final String JSON_FIELD_ORIGINAL_CLIENT_UUID = "ocid"; @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME) private String identityProviderUsername; @@ -41,9 +42,13 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken { @JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID) private String originalAuthenticationSessionId; - public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, + @JsonProperty(value = JSON_FIELD_ORIGINAL_CLIENT_UUID) + private String originalClientUUID; + + public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String clientUUID, String identityProviderUsername, String identityProviderAlias) { super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId); + this.originalClientUUID = clientUUID; this.identityProviderUsername = identityProviderUsername; this.identityProviderAlias = identityProviderAlias; } @@ -74,4 +79,12 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken { public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) { this.originalAuthenticationSessionId = originalAuthenticationSessionId; } + + public String getOriginalClientUUID() { + return originalClientUUID; + } + + public void setOriginalClientUUID(String originalClientUUID) { + this.originalClientUUID = originalClientUUID; + } } diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java index 7f9d58c3bc..514ca1a0b5 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java @@ -23,6 +23,7 @@ import org.keycloak.authentication.actiontoken.*; import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticator; import org.keycloak.events.*; import org.keycloak.forms.login.LoginFormsProvider; +import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -31,7 +32,7 @@ import org.keycloak.services.Urls; import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.messages.Messages; import org.keycloak.sessions.AuthenticationSessionModel; -import org.keycloak.sessions.AuthenticationSessionProvider; + import java.util.Collections; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; @@ -76,8 +77,8 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession(); if (tokenContext.isAuthenticationSessionFresh()) { token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId()); - token.setAuthenticationSessionId(authSession.getId()); - UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo)); + token.setAuthenticationSessionId(authSession.getParentSession().getId()); + UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId()); String confirmUri = builder.build(realm.getName()).toString(); return session.getProvider(LoginFormsProvider.class) @@ -94,14 +95,16 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH AuthenticationSessionManager asm = new AuthenticationSessionManager(session); asm.removeAuthenticationSession(realm, authSession, true); - AuthenticationSessionProvider authSessProvider = session.authenticationSessions(); - authSession = authSessProvider.getAuthenticationSession(realm, token.getOriginalAuthenticationSessionId()); + ClientModel originalClient = realm.getClientById(token.getOriginalClientUUID()); + authSession = asm.getAuthenticationSessionByIdAndClient(realm, token.getOriginalAuthenticationSessionId(), originalClient); if (authSession != null) { authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername()); } else { - authSessProvider.updateNonlocalSessionAuthNotes( + + session.authenticationSessions().updateNonlocalSessionAuthNotes( token.getAuthenticationSessionId(), + originalClient, Collections.singletonMap(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername()) ); } diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java index 98ad8b4b98..91ceb2a724 100644 --- a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java +++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java @@ -77,8 +77,8 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander check = new ClientSessionCode<>(session, realm, authSession); - if (!check.isActionActive(ClientSessionCode.ActionType.LOGIN)) { + authSession = rootAuthSession.getAuthenticationSession(client); - logger.debugf("Authentication session '%s' exists, but is expired. Restart existing authentication session", authSession.getId()); - authSession.restartSession(realm, client); - return new AuthorizationEndpointChecks(authSession); + if (authSession != null) { + ClientSessionCode check = new ClientSessionCode<>(session, realm, authSession); + if (!check.isActionActive(ClientSessionCode.ActionType.LOGIN)) { - } else if (isNewRequest(authSession, client, requestState)) { - // Check if we have lastProcessedExecution note or if some request parameter beside state (eg. prompt, kc_idp_hint) changed. Restart the session just if yes. - // Otherwise update just client information from the AuthorizationEndpoint request. - // This difference is needed, because of logout from JS applications in multiple browser tabs. - if (shouldRestartAuthSession(authSession)) { - logger.debug("New request from application received, but authentication session already exists. Restart existing authentication session"); - authSession.restartSession(realm, client); - } else { - logger.debug("New request from application received, but authentication session already exists. Update client information in existing authentication session"); - authSession.clearClientNotes(); // update client data - authSession.updateClient(client); - } + logger.debugf("Authentication session '%s' exists, but is expired. Restart existing authentication session", rootAuthSession.getId()); + rootAuthSession.restartSession(realm); + authSession = rootAuthSession.createAuthenticationSession(client); - return new AuthorizationEndpointChecks(authSession); - - } else { - logger.debug("Re-sent some previous request to Authorization endpoint. Likely browser 'back' or 'refresh' button."); - - // See if we have lastProcessedExecution note. If yes, we are expired. Also if we are in different flow than initial one. Otherwise it is browser refresh of initial username/password form - if (!shouldShowExpirePage(authSession)) { return new AuthorizationEndpointChecks(authSession); - } else { - CacheControlUtil.noBackButtonCacheControlHeader(); - Response response = new AuthenticationFlowURLHelper(session, realm, uriInfo) - .showPageExpired(authSession); - return new AuthorizationEndpointChecks(response); + } else if (isNewRequest(authSession, client, requestState)) { + // Check if we have lastProcessedExecution note or if some request parameter beside state (eg. prompt, kc_idp_hint) changed. Restart the session just if yes. + // Otherwise update just client information from the AuthorizationEndpoint request. + // This difference is needed, because of logout from JS applications in multiple browser tabs. + if (shouldRestartAuthSession(authSession)) { + logger.debugf("New request from application received, but authentication session '%s' already exists and has client '%s'. Restart child authentication session for client.", + rootAuthSession.getId(), client.getClientId()); + + authSession = rootAuthSession.createAuthenticationSession(client); + + } else { + logger.debugf("New request from application received, but authentication session '%s' already exists and has client '%s'. Update client information in existing authentication session.", + rootAuthSession.getId(), client.getClientId()); + authSession.clearClientNotes(); + } + + return new AuthorizationEndpointChecks(authSession); + + } else { + logger.debug("Re-sent some previous request to Authorization endpoint. Likely browser 'back' or 'refresh' button."); + + // See if we have lastProcessedExecution note. If yes, we are expired. Also if we are in different flow than initial one. Otherwise it is browser refresh of initial username/password form + if (!shouldShowExpirePage(authSession)) { + return new AuthorizationEndpointChecks(authSession); + } else { + CacheControlUtil.noBackButtonCacheControlHeader(); + + Response response = new AuthenticationFlowURLHelper(session, realm, uriInfo) + .showPageExpired(authSession); + return new AuthorizationEndpointChecks(response); + } } + } else { + logger.debugf("Sent request to authz endpoint. Authentication session with ID '%s' exists, but doesn't have client: '%s' . Adding client to authentication session", + rootAuthSession.getId(), client.getClientId()); + + authSession = rootAuthSession.createAuthenticationSession(client); + return new AuthorizationEndpointChecks(authSession); } } @@ -214,10 +231,12 @@ public abstract class AuthorizationEndpointBase { if (userSession != null) { logger.debugf("Sent request to authz endpoint. We don't have authentication session with ID '%s' but we have userSession. Will re-create authentication session with same ID", authSessionId); - authSession = session.authenticationSessions().createAuthenticationSession(authSessionId, realm, client); + rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(authSessionId, realm); + authSession = rootAuthSession.createAuthenticationSession(client); } else { - authSession = manager.createAuthenticationSession(realm, client, true); - logger.debugf("Sent request to authz endpoint. Created new authentication session with ID '%s'", authSession.getId()); + rootAuthSession = manager.createAuthenticationSession(realm, true); + authSession = rootAuthSession.createAuthenticationSession(client); + logger.debugf("Sent request to authz endpoint. Created new authentication session with ID '%s'", rootAuthSession.getId()); } return new AuthorizationEndpointChecks(authSession); diff --git a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java index b4e037b337..59cd0b9f78 100644 --- a/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java +++ b/services/src/main/java/org/keycloak/protocol/RestartLoginCookie.java @@ -31,6 +31,7 @@ import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.util.CookieHelper; import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.RootAuthenticationSessionModel; import javax.crypto.SecretKey; import javax.ws.rs.core.Cookie; @@ -143,7 +144,8 @@ public class RestartLoginCookie { } - public static AuthenticationSessionModel restartSession(KeycloakSession session, RealmModel realm) throws Exception { + public static AuthenticationSessionModel restartSession(KeycloakSession session, RealmModel realm, + RootAuthenticationSessionModel rootSession, String expectedClientId) throws Exception { Cookie cook = session.getContext().getRequestHeaders().getCookies().get(KC_RESTART); if (cook == null) { logger.debug("KC_RESTART cookie doesn't exist"); @@ -161,7 +163,18 @@ public class RestartLoginCookie { ClientModel client = realm.getClientByClientId(cookie.getClientId()); if (client == null) return null; - AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true); + // Restart just if client from cookie matches client from the URL. + if (!client.getClientId().equals(expectedClientId)) { + logger.debugf("Skip restarting from the KC_RESTART. Clients doesn't match: Cookie client: %s, Requested client: %s", client.getClientId(), expectedClientId); + return null; + } + + // Need to create brand new session and setup cookie + if (rootSession == null) { + rootSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, true); + } + + AuthenticationSessionModel authSession = rootSession.createAuthenticationSession(client); authSession.setProtocol(cookie.getAuthMethod()); authSession.setRedirectUri(cookie.getRedirectUri()); authSession.setAction(cookie.getAction()); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index 94c4d24fb0..0e8d36e881 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -80,6 +80,7 @@ import org.keycloak.services.resources.admin.AdminAuth; import org.keycloak.services.resources.admin.permissions.AdminPermissions; import org.keycloak.services.validation.Validation; import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.util.TokenUtil; import org.keycloak.utils.ProfileHelper; @@ -255,7 +256,7 @@ public class TokenEndpoint { throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST); } - ClientSessionCode.ParseResult parseResult = ClientSessionCode.parseResult(code, session, realm, event, AuthenticatedClientSessionModel.class); + ClientSessionCode.ParseResult parseResult = ClientSessionCode.parseResult(code, session, realm, client, event, AuthenticatedClientSessionModel.class); if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) { AuthenticatedClientSessionModel clientSession = parseResult.getClientSession(); @@ -469,7 +470,9 @@ public class TokenEndpoint { } String scope = formParams.getFirst(OAuth2Constants.SCOPE); - AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, false); + RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false); + AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client); + authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); authSession.setAction(AuthenticatedClientSessionModel.Action.AUTHENTICATE.name()); authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); @@ -553,13 +556,16 @@ public class TokenEndpoint { String scope = formParams.getFirst(OAuth2Constants.SCOPE); - AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, false); + RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false); + AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client); + authSession.setAuthenticatedUser(clientUser); authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); authSession.setClientNote(OIDCLoginProtocol.SCOPE_PARAM, scope); - UserSessionModel userSession = session.sessions().createUserSession(authSession.getId(), realm, clientUser, clientUsername, clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null); + UserSessionModel userSession = session.sessions().createUserSession(authSession.getParentSession().getId(), realm, clientUser, clientUsername, + clientConnection.getRemoteAddr(), ServiceAccountConstants.CLIENT_AUTH, false, null, null); event.session(userSession); AuthenticationManager.setRolesAndMappersInSession(authSession); @@ -763,7 +769,9 @@ public class TokenEndpoint { String scope = formParams.getFirst(OAuth2Constants.SCOPE); - AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, targetClient, false); + RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, false); + AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(targetClient); + authSession.setAuthenticatedUser(targetUser); authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); authSession.setClientNote(OIDCLoginProtocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName())); diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java index 6e954f9baf..b14ef060d3 100755 --- a/services/src/main/java/org/keycloak/services/Urls.java +++ b/services/src/main/java/org/keycloak/services/Urls.java @@ -182,9 +182,11 @@ public class Urls { return loginResetCredentialsBuilder(baseUri).build(realmName); } - public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString) { + public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString, String clientId) { return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken") - .queryParam("key", tokenString); + .queryParam("key", tokenString) + .queryParam(Constants.CLIENT_ID, clientId); + } public static UriBuilder loginResetCredentialsBuilder(URI baseUri) { 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 8791010931..649c77d7d6 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -56,6 +56,7 @@ import org.keycloak.services.util.CookieHelper; import org.keycloak.services.util.P3PHelper; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.CommonClientSessionModel; +import org.keycloak.sessions.RootAuthenticationSessionModel; import javax.crypto.SecretKey; import javax.ws.rs.core.Cookie; @@ -215,16 +216,20 @@ public class AuthenticationManager { } private static AuthenticationSessionModel createOrJoinLogoutSession(RealmModel realm, final AuthenticationSessionManager asm, boolean browserCookie) { + // Account management client is used as a placeholder ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); - AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm); + + AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm, client); // Try to join existing logout session if it exists and browser session is required if (browserCookie && logoutAuthSession != null) { if (Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), logoutAuthSession.getAction())) { return logoutAuthSession; } - logoutAuthSession.restartSession(realm, client); + // Re-create the authentication session for logout + logoutAuthSession = logoutAuthSession.getParentSession().createAuthenticationSession(client); } else { - logoutAuthSession = asm.createAuthenticationSession(realm, client, browserCookie); + RootAuthenticationSessionModel rootLogoutSession = asm.createAuthenticationSession(realm, browserCookie); + logoutAuthSession = rootLogoutSession.createAuthenticationSession(client); } logoutAuthSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name()); return logoutAuthSession; @@ -381,8 +386,8 @@ public class AuthenticationManager { /** * Sets logout state of the particular client into the {@code logoutAuthSession} * @param logoutAuthSession logoutAuthSession. May be {@code null} in which case this is a no-op. - * @param client Client. Must not be {@code null} - * @param state + * @param clientUuid Client. Must not be {@code null} + * @param action */ public static void setClientLogoutAction(AuthenticationSessionModel logoutAuthSession, String clientUuid, AuthenticationSessionModel.Action action) { if (logoutAuthSession != null && clientUuid != null) { @@ -479,8 +484,11 @@ public class AuthenticationManager { } public static Response finishBrowserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) { + // Account management client is used as a placeholder + ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); + final AuthenticationSessionManager asm = new AuthenticationSessionManager(session); - AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm); + AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm, client); checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession); expireIdentityCookie(realm, uriInfo, connection); @@ -832,7 +840,7 @@ public class AuthenticationManager { logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired()); - event.detail(Details.CODE_ID, authSession.getId()); + event.detail(Details.CODE_ID, authSession.getParentSession().getId()); Set requiredActions = user.getRequiredActions(); Response action = executionActions(session, authSession, request, event, realm, user, requiredActions); diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java index ac6259eede..8a625b390c 100644 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationSessionManager.java @@ -28,6 +28,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.RestartLoginCookie; import org.keycloak.services.util.CookieHelper; import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.sessions.StickySessionEncoderProvider; /** @@ -45,22 +46,22 @@ public class AuthenticationSessionManager { this.session = session; } + /** - * Creates a fresh authentication session for the given realm and client. Optionally sets the browser + * Creates a fresh authentication session for the given realm . Optionally sets the browser * authentication session cookie {@link #AUTH_SESSION_ID} with the ID of the new session. * @param realm - * @param client * @param browserCookie Set the cookie in the browser for the * @return */ - public AuthenticationSessionModel createAuthenticationSession(RealmModel realm, ClientModel client, boolean browserCookie) { - AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client); + public RootAuthenticationSessionModel createAuthenticationSession(RealmModel realm, boolean browserCookie) { + RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(realm); if (browserCookie) { - setAuthSessionCookie(authSession.getId(), realm); + setAuthSessionCookie(rootAuthSession.getId(), realm); } - return authSession; + return rootAuthSession; } @@ -73,14 +74,20 @@ public class AuthenticationSessionManager { return getAuthSessionCookieDecoded(realm); } + /** * Returns current authentication session if it exists, otherwise returns {@code null}. * @param realm * @return */ - public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm) { + public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm, ClientModel client) { String authSessionId = getAuthSessionCookieDecoded(realm); - return authSessionId==null ? null : session.authenticationSessions().getAuthenticationSession(realm, authSessionId); + + if (authSessionId == null) { + return null; + } + + return getAuthenticationSessionByIdAndClient(realm, authSessionId, client); } @@ -124,8 +131,10 @@ public class AuthenticationSessionManager { public void removeAuthenticationSession(RealmModel realm, AuthenticationSessionModel authSession, boolean expireRestartCookie) { - log.debugf("Removing authSession '%s'. Expire restart cookie: %b", authSession.getId(), expireRestartCookie); - session.authenticationSessions().removeAuthenticationSession(realm, authSession); + RootAuthenticationSessionModel rootAuthSession = authSession.getParentSession(); + + log.debugf("Removing authSession '%s'. Expire restart cookie: %b", rootAuthSession.getId(), expireRestartCookie); + session.authenticationSessions().removeRootAuthenticationSession(realm, rootAuthSession); // expire restart cookie if (expireRestartCookie) { @@ -138,7 +147,14 @@ public class AuthenticationSessionManager { // Check to see if we already have authenticationSession with same ID public UserSessionModel getUserSession(AuthenticationSessionModel authSession) { - return session.sessions().getUserSession(authSession.getRealm(), authSession.getId()); + return session.sessions().getUserSession(authSession.getRealm(), authSession.getParentSession().getId()); + } + + + // Don't look at cookie. Just lookup authentication session based on the ID and client. Return null if not found + public AuthenticationSessionModel getAuthenticationSessionByIdAndClient(RealmModel realm, String authSessionId, ClientModel client) { + RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId); + return rootAuthSession==null ? null : rootAuthSession.getAuthenticationSession(client); } } diff --git a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java index 503973ee73..2acf683b9f 100755 --- a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java @@ -80,7 +80,8 @@ public class ClientSessionCode } } - public static ParseResult parseResult(String code, KeycloakSession session, RealmModel realm, EventBuilder event, Class sessionClass) { + public static ParseResult parseResult(String code, KeycloakSession session, RealmModel realm, ClientModel client, + EventBuilder event, Class sessionClass) { ParseResult result = new ParseResult<>(); if (code == null) { result.illegalHash = true; @@ -88,7 +89,7 @@ public class ClientSessionCode } try { CodeGenerateUtil.ClientSessionParser clientSessionParser = CodeGenerateUtil.getParser(sessionClass); - result.clientSession = getClientSession(code, session, realm, event, clientSessionParser); + result.clientSession = getClientSession(code, session, realm, client, event, clientSessionParser); if (result.clientSession == null) { result.authSessionNotFound = true; return result; @@ -113,15 +114,16 @@ public class ClientSessionCode } - public static CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event, Class sessionClass) { + public static CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, + EventBuilder event, Class sessionClass) { CodeGenerateUtil.ClientSessionParser clientSessionParser = CodeGenerateUtil.getParser(sessionClass); - return getClientSession(code, session, realm, event, clientSessionParser); + return getClientSession(code, session, realm, client, event, clientSessionParser); } - private static CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event, + private static CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event, CodeGenerateUtil.ClientSessionParser clientSessionParser) { - return clientSessionParser.parseSession(code, session, realm, event); + return clientSessionParser.parseSession(code, session, realm, client, event); } @@ -135,7 +137,8 @@ public class ClientSessionCode } public boolean isActionActive(ActionType actionType) { - int timestamp = commonLoginSession.getTimestamp(); + CodeGenerateUtil.ClientSessionParser clientSessionParser = (CodeGenerateUtil.ClientSessionParser) CodeGenerateUtil.getParser(commonLoginSession.getClass()); + int timestamp = clientSessionParser.getTimestamp(commonLoginSession); int lifespan; switch (actionType) { @@ -210,7 +213,9 @@ public class ClientSessionCode public void setAction(String action) { commonLoginSession.setAction(action); - commonLoginSession.setTimestamp(Time.currentTime()); + + CodeGenerateUtil.ClientSessionParser clientSessionParser = (CodeGenerateUtil.ClientSessionParser) CodeGenerateUtil.getParser(commonLoginSession.getClass()); + clientSessionParser.setTimestamp(commonLoginSession, Time.currentTime()); } public String getOrGenerateCode() { 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 de141ff0b0..8ea2450d29 100644 --- a/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java +++ b/services/src/main/java/org/keycloak/services/managers/CodeGenerateUtil.java @@ -32,6 +32,7 @@ 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; @@ -78,7 +79,7 @@ class CodeGenerateUtil { interface ClientSessionParser { - CS parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event); + CS parseSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event); String retrieveCode(KeycloakSession session, CS clientSession); @@ -88,6 +89,9 @@ class CodeGenerateUtil { boolean isExpired(KeycloakSession session, String code, CS clientSession); + int getTimestamp(CS clientSession); + void setTimestamp(CS clientSession, int timestamp); + } @@ -97,9 +101,9 @@ class CodeGenerateUtil { private static class AuthenticationSessionModelParser implements ClientSessionParser { @Override - public AuthenticationSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event) { + public AuthenticationSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event) { // Read authSessionID from cookie. Code is ignored for now - return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm); + return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client); } @Override @@ -141,6 +145,16 @@ class CodeGenerateUtil { public boolean isExpired(KeycloakSession session, String code, AuthenticationSessionModel clientSession) { return false; } + + @Override + public int getTimestamp(AuthenticationSessionModel clientSession) { + return clientSession.getParentSession().getTimestamp(); + } + + @Override + public void setTimestamp(AuthenticationSessionModel clientSession, int timestamp) { + clientSession.getParentSession().setTimestamp(timestamp); + } } @@ -149,7 +163,7 @@ class CodeGenerateUtil { private CodeJWT codeJWT; @Override - public AuthenticatedClientSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, EventBuilder event) { + public AuthenticatedClientSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event) { SecretKey aesKey = session.keys().getActiveAesKey(realm).getSecretKey(); SecretKey hmacKey = session.keys().getActiveHmacKey(realm).getSecretKey(); @@ -241,6 +255,16 @@ class CodeGenerateUtil { public boolean isExpired(KeycloakSession session, String code, AuthenticatedClientSessionModel clientSession) { return !codeJWT.isActive(); } + + @Override + public int getTimestamp(AuthenticatedClientSessionModel clientSession) { + return clientSession.getTimestamp(); + } + + @Override + public void setTimestamp(AuthenticatedClientSessionModel clientSession, int timestamp) { + clientSession.setTimestamp(timestamp); + } } 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 0f3d30774f..611f05d07b 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -292,7 +292,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal // Create AuthenticationSessionModel with same ID like userSession and refresh cookie UserSessionModel userSession = cookieResult.getSession(); - AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(userSession.getId(), realmModel, client); + AuthenticationSessionModel authSession = session.authenticationSessions().createRootAuthenticationSession(userSession.getId(), realmModel).createAuthenticationSession(client); new AuthenticationSessionManager(session).setAuthSessionCookie(userSession.getId(), realmModel); ClientSessionCode clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession); @@ -588,7 +588,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal AuthenticationSessionModel authSession = clientSessionCode.getClientSession(); try { - this.event.detail(Details.CODE_ID, authSession.getId()) + this.event.detail(Details.CODE_ID, authSession.getParentSession().getId()) .removeDetail("auth_method"); SerializedBrokeredIdentityContext serializedCtx = SerializedBrokeredIdentityContext.readFromAuthenticationSession(authSession, AbstractIdpAuthenticator.BROKERED_CONTEXT_NOTE); @@ -685,7 +685,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal logger.debugf("Redirect to postBrokerLogin flow after authentication with identityProvider '%s'.", context.getIdpConfig().getAlias()); - authSession.setTimestamp(Time.currentTime()); + authSession.getParentSession().setTimestamp(Time.currentTime()); SerializedBrokeredIdentityContext ctx = SerializedBrokeredIdentityContext.serialize(context); ctx.saveToAuthenticationSession(authSession, PostBrokerLoginConstants.PBL_BROKERED_IDENTITY_CONTEXT); @@ -789,7 +789,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal if (nextRequiredAction != null) { return AuthenticationManager.redirectToRequiredActions(session, realmModel, authSession, uriInfo, nextRequiredAction); } else { - event.detail(Details.CODE_ID, authSession.getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set + event.detail(Details.CODE_ID, authSession.getParentSession().getId()); // todo This should be set elsewhere. find out why tests fail. Don't know where this is supposed to be set return AuthenticationManager.finishedRequiredActions(session, authSession, null, clientConnection, request, uriInfo, event); } } diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index 8aaca57891..d707e03fd8 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -72,6 +72,7 @@ import org.keycloak.services.util.CacheControlUtil; import org.keycloak.services.util.AuthenticationFlowURLHelper; import org.keycloak.services.util.BrowserHistoryHelper; import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.RootAuthenticationSessionModel; import javax.ws.rs.Consumes; import javax.ws.rs.GET; @@ -333,7 +334,8 @@ public class LoginActionsService { public Response resetCredentialsGET(@QueryParam("code") String code, @QueryParam("execution") String execution, @QueryParam("client_id") String clientId) { - AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm); + ClientModel client = realm.getClientByClientId(clientId); + AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client); // we allow applications to link to reset credentials without going through OAuth or SAML handshakes if (authSession == null && code == null) { @@ -357,7 +359,10 @@ public class LoginActionsService { // set up the account service as the endpoint to call. ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID); - authSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, client, true); + + RootAuthenticationSessionModel rootAuthSession = new AuthenticationSessionManager(session).createAuthenticationSession(realm, true); + authSession = rootAuthSession.createAuthenticationSession(client); + authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name()); //authSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true"); authSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); @@ -413,9 +418,8 @@ public class LoginActionsService { ActionTokenContext tokenContext; String eventError = null; String defaultErrorMessage = null; - AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm); - event.event(EventType.EXECUTE_ACTION_TOKEN); + AuthenticationSessionModel authSession = null; // Setup client, so error page will contain "back to application" link ClientModel client = null; @@ -424,8 +428,11 @@ public class LoginActionsService { } if (client != null) { session.getContext().setClient(client); + authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client); } + event.event(EventType.EXECUTE_ACTION_TOKEN); + // First resolve action token handler try { if (tokenString == null) { @@ -500,7 +507,7 @@ public class LoginActionsService { authSession = handler.startFreshAuthenticationSession(token, tokenContext); tokenContext.setAuthenticationSession(authSession, true); } else if (tokenAuthSessionId == null || - ! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, tokenAuthSessionId)) { + ! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, tokenAuthSessionId, client)) { // There exists an authentication session but no auth session ID was received in the action token logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId()); new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false); @@ -737,7 +744,7 @@ public class LoginActionsService { public static Response redirectToAfterBrokerLoginEndpoint(KeycloakSession session, RealmModel realm, UriInfo uriInfo, AuthenticationSessionModel authSession, boolean firstBrokerLogin) { ClientSessionCode accessCode = new ClientSessionCode<>(session, realm, authSession); - authSession.setTimestamp(Time.currentTime()); + authSession.getParentSession().setTimestamp(Time.currentTime()); String clientId = authSession.getClient().getClientId(); URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId) : @@ -816,7 +823,7 @@ public class LoginActionsService { OIDCResponseMode responseMode = OIDCResponseMode.parse(respMode, OIDCResponseType.parse(responseType)); event.event(EventType.LOGIN).client(authSession.getClient()) - .detail(Details.CODE_ID, authSession.getId()) + .detail(Details.CODE_ID, authSession.getParentSession().getId()) .detail(Details.REDIRECT_URI, authSession.getRedirectUri()) .detail(Details.AUTH_METHOD, authSession.getProtocol()) .detail(Details.RESPONSE_TYPE, responseType) diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java index 345f64a137..f0e7ef0dc3 100644 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java @@ -35,6 +35,8 @@ import org.keycloak.sessions.CommonClientSessionModel.Action; import java.util.Objects; import java.util.function.Consumer; import org.jboss.logging.Logger; +import org.keycloak.sessions.RootAuthenticationSessionModel; + /** * * @author hmlnarik @@ -75,7 +77,6 @@ public class LoginActionsServiceChecks { * If there is an action required in the session, furthermore it is not the expected one, and the required * action is redirection to "required actions", it throws with response performing the redirect to required * actions. - * @param */ public static class IsActionRequired implements Predicate { @@ -250,7 +251,7 @@ public class LoginActionsServiceChecks { * * @param */ - public static boolean doesAuthenticationSessionFromCookieMatchOneFromToken(ActionTokenContext context, String authSessionIdFromToken) throws VerificationException { + public static boolean doesAuthenticationSessionFromCookieMatchOneFromToken(ActionTokenContext context, String authSessionIdFromToken, ClientModel client) throws VerificationException { if (authSessionIdFromToken == null) { return false; } @@ -262,9 +263,8 @@ public class LoginActionsServiceChecks { return false; } - AuthenticationSessionModel authSessionFromCookie = context.getSession() - .authenticationSessions().getAuthenticationSession(context.getRealm(), authSessionIdFromCookie); - if (authSessionFromCookie == null) { // Cookie contains ID of expired auth session + AuthenticationSessionModel authSessionFromCookie = asm.getAuthenticationSessionByIdAndClient(context.getRealm(), authSessionIdFromCookie, client); + if (authSessionFromCookie == null) { // Not our client in root session return false; } @@ -278,14 +278,16 @@ public class LoginActionsServiceChecks { return false; } - AuthenticationSessionModel authSessionFromParent = context.getSession() - .authenticationSessions().getAuthenticationSession(context.getRealm(), parentSessionId); + AuthenticationSessionModel authSessionFromParent = asm.getAuthenticationSessionByIdAndClient(context.getRealm(), parentSessionId, client); + if (authSessionFromParent == null) { + return false; + } // It's the correct browser. Let's remove forked session as we won't continue // from the login form (browser flow) but from the token's flow // Don't expire KC_RESTART cookie at this point asm.removeAuthenticationSession(context.getRealm(), authSessionFromCookie, false); - LOG.debugf("Removed forked session: %s", authSessionFromCookie.getId()); + LOG.debugf("Removed forked session: %s", authSessionFromCookie.getParentSession().getId()); // Refresh browser cookie asm.setAuthSessionCookie(parentSessionId, context.getRealm()); diff --git a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java index 3624b53d9c..90dd18670f 100644 --- a/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java +++ b/services/src/main/java/org/keycloak/services/resources/SessionCodeChecks.java @@ -47,6 +47,7 @@ import org.keycloak.services.messages.Messages; import org.keycloak.services.util.BrowserHistoryHelper; import org.keycloak.services.util.AuthenticationFlowURLHelper; import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.RootAuthenticationSessionModel; public class SessionCodeChecks { @@ -132,12 +133,6 @@ public class SessionCodeChecks { return null; } - // object retrieve - AuthenticationSessionModel authSession = ClientSessionCode.getClientSession(code, session, realm, event, AuthenticationSessionModel.class); - if (authSession != null) { - return authSession; - } - // Setup client to be shown on error/info page based on "client_id" parameter logger.debugf("Will use client '%s' in back-to-application link", clientId); ClientModel client = null; @@ -148,8 +143,16 @@ public class SessionCodeChecks { session.getContext().setClient(client); } + // object retrieve + AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session); + AuthenticationSessionModel authSession = authSessionManager.getCurrentAuthenticationSession(realm, client); + if (authSession != null) { + return authSession; + } + // See if we are already authenticated and userSession with same ID exists. - String sessionId = new AuthenticationSessionManager(session).getCurrentAuthenticationSessionId(realm); + String sessionId = authSessionManager.getCurrentAuthenticationSessionId(realm); + RootAuthenticationSessionModel existingRootAuthSession = null; if (sessionId != null) { UserSessionModel userSession = session.sessions().getUserSession(realm, sessionId); if (userSession != null) { @@ -164,10 +167,13 @@ public class SessionCodeChecks { response = loginForm.createInfoPage(); return null; } + + + existingRootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, sessionId); } // Otherwise just try to restart from the cookie - response = restartAuthenticationSessionFromCookie(); + response = restartAuthenticationSessionFromCookie(existingRootAuthSession); return null; } @@ -186,7 +192,7 @@ public class SessionCodeChecks { } // Client checks - event.detail(Details.CODE_ID, authSession.getId()); + event.detail(Details.CODE_ID, authSession.getParentSession().getId()); ClientModel client = authSession.getClient(); if (client == null) { event.error(Errors.CLIENT_NOT_FOUND); @@ -240,7 +246,7 @@ public class SessionCodeChecks { return false; } } else { - ClientSessionCode.ParseResult result = ClientSessionCode.parseResult(code, session, realm, event, AuthenticationSessionModel.class); + ClientSessionCode.ParseResult result = ClientSessionCode.parseResult(code, session, realm, client, event, AuthenticationSessionModel.class); clientCode = result.getCode(); if (clientCode == null) { @@ -341,11 +347,12 @@ public class SessionCodeChecks { } - private Response restartAuthenticationSessionFromCookie() { + private Response restartAuthenticationSessionFromCookie(RootAuthenticationSessionModel existingRootSession) { logger.debug("Authentication session not found. Trying to restart from cookie."); AuthenticationSessionModel authSession = null; + try { - authSession = RestartLoginCookie.restartSession(session, realm); + authSession = RestartLoginCookie.restartSession(session, realm, existingRootSession, clientId); } catch (Exception e) { ServicesLogger.LOGGER.failedToParseRestartLoginCookie(e); } diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java index 99e6ea3bf2..d40dc61794 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java @@ -52,6 +52,7 @@ import org.keycloak.services.Urls; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AuthenticationSessionManager; import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.AbstractSecuredLocalService; @@ -60,6 +61,7 @@ import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.util.ResolveRelative; import org.keycloak.services.validation.Validation; import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.storage.ReadOnlyException; import org.keycloak.util.JsonSerialization; @@ -179,7 +181,7 @@ public class AccountFormService extends AbstractSecuredLocalService { setReferrerOnPage(); UserSessionModel userSession = auth.getSession(); - AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, userSession.getId()); + AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getAuthenticationSessionByIdAndClient(realm, userSession.getId(), client); if (authSession != null) { String forwardedError = authSession.getAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE); if (forwardedError != null) { diff --git a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java index d4c67a7a90..432bc7ba94 100755 --- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java @@ -25,6 +25,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken; import org.keycloak.broker.provider.IdentityProvider; +import org.keycloak.broker.provider.util.IdentityBrokerState; import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.common.ClientConnection; import org.keycloak.events.Details; @@ -197,7 +198,9 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider { try { String cookieVal = OLD_RESTART_COOKIE_JSON.replace("\n", "").replace(" ", ""); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/MailAssert.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/MailAssert.java index 463c32fa85..0575317458 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/MailAssert.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/MailAssert.java @@ -50,6 +50,7 @@ public class MailAssert { if (message.getContent() instanceof MimeMultipart) { MimeMultipart mimeMultipart = (MimeMultipart) message.getContent(); + // TEXT content is on index 0 messageContent = String.valueOf(mimeMultipart.getBodyPart(0).getContent()); } else { messageContent = String.valueOf(message.getContent()); @@ -61,6 +62,9 @@ public class MailAssert { assertTrue(errorMessage, messageContent.contains(content)); for (String string : messageContent.split("\n")) { if (string.contains("http://")) { + + // Ampersand escaped in the text version. Needs to be replaced to have correct URL + string = string.replace("&", "&"); return string; } } diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java index e977aefd5e..49b037bf87 100755 --- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java @@ -356,15 +356,16 @@ public abstract class AbstractIdentityProviderTest { assertEquals("text/html; charset=UTF-8", htmlContentType); final String htmlBody = (String) multipart.getBodyPart(1).getContent(); - + + final String htmlChangePwdUrl = MailUtil.getLink(htmlBody); // .replace() accounts for escaping the ampersand // It's not escaped in the html version because html retrieved from a // message bundle is considered safe and it must be unescaped to display // properly. - final String htmlVerificationUrl = MailUtil.getLink(htmlBody).replace("&", "&"); + final String htmlChangePwdUrlToCompare = htmlChangePwdUrl.replace("&", "&"); - assertEquals(htmlVerificationUrl, textVerificationUrl); + assertEquals(htmlChangePwdUrlToCompare, textVerificationUrl); - return htmlVerificationUrl; + return htmlChangePwdUrl; } } diff --git a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java index db08e810b9..e28a976236 100644 --- a/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java +++ b/testsuite/integration-deprecated/src/test/java/org/keycloak/testsuite/model/AuthenticationSessionProviderTest.java @@ -32,6 +32,7 @@ import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.sessions.CommonClientSessionModel; +import org.keycloak.sessions.RootAuthenticationSessionModel; import org.keycloak.testsuite.rule.KeycloakRule; import static org.junit.Assert.assertNotNull; @@ -83,37 +84,40 @@ public class AuthenticationSessionProviderTest { ClientModel client1 = realm.getClientByClientId("test-app"); UserModel user1 = session.users().getUserByUsername("user1", realm); - AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client1); + RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(realm); + AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client1); authSession.setAction("foo"); - authSession.setTimestamp(100); + rootAuthSession.setTimestamp(100); resetSession(); // Ensure session is here - authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId()); + rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId()); testAuthenticationSession(authSession, client1.getId(), null, "foo"); - Assert.assertEquals(100, authSession.getTimestamp()); + Assert.assertEquals(100, rootAuthSession.getTimestamp()); // Update and commit authSession.setAction("foo-updated"); - authSession.setTimestamp(200); + rootAuthSession.setTimestamp(200); authSession.setAuthenticatedUser(session.users().getUserByUsername("user1", realm)); resetSession(); // Ensure session was updated - authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId()); + rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId()); + client1 = realm.getClientByClientId("test-app"); + authSession = rootAuthSession.getAuthenticationSession(client1); testAuthenticationSession(authSession, client1.getId(), user1.getId(), "foo-updated"); - Assert.assertEquals(200, authSession.getTimestamp()); + Assert.assertEquals(200, rootAuthSession.getTimestamp()); // Remove and commit - session.authenticationSessions().removeAuthenticationSession(realm, authSession); + session.authenticationSessions().removeRootAuthenticationSession(realm, rootAuthSession); resetSession(); // Ensure session was removed - Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSession.getId())); + Assert.assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId())); } @@ -122,10 +126,10 @@ public class AuthenticationSessionProviderTest { ClientModel client1 = realm.getClientByClientId("test-app"); UserModel user1 = session.users().getUserByUsername("user1", realm); - AuthenticationSessionModel authSession = session.authenticationSessions().createAuthenticationSession(realm, client1); + AuthenticationSessionModel authSession = session.authenticationSessions().createRootAuthenticationSession(realm).createAuthenticationSession(client1); authSession.setAction("foo"); - authSession.setTimestamp(100); + authSession.getParentSession().setTimestamp(100); authSession.setAuthenticatedUser(user1); authSession.setAuthNote("foo", "bar"); @@ -134,20 +138,17 @@ public class AuthenticationSessionProviderTest { resetSession(); + // Test restart root authentication session client1 = realm.getClientByClientId("test-app"); - authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId()); - authSession.restartSession(realm, client1); + authSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSession.getParentSession().getId()) + .getAuthenticationSession(client1); + authSession.getParentSession().restartSession(realm); resetSession(); - authSession = session.authenticationSessions().getAuthenticationSession(realm, authSession.getId()); - testAuthenticationSession(authSession, client1.getId(), null, null); - Assert.assertTrue(authSession.getTimestamp() > 0); - - Assert.assertTrue(authSession.getClientNotes().isEmpty()); - Assert.assertNull(authSession.getAuthNote("foo2")); - Assert.assertTrue(authSession.getExecutionStatus().isEmpty()); - + RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSession.getParentSession().getId()); + Assert.assertNull(rootAuthSession.getAuthenticationSession(client1)); + Assert.assertTrue(rootAuthSession.getTimestamp() > 0); } @@ -159,58 +160,59 @@ public class AuthenticationSessionProviderTest { realm.setAccessCodeLifespanLogin(30); // Login lifespan is largest - String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId(); + String authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId(); + resetSession(); Time.setOffset(25); session.authenticationSessions().removeExpired(realm); resetSession(); - assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId)); + assertNotNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId)); Time.setOffset(35); session.authenticationSessions().removeExpired(realm); resetSession(); - assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId)); + assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId)); // User action is largest realm.setAccessCodeLifespanUserAction(40); Time.setOffset(0); - authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId(); + authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId(); resetSession(); Time.setOffset(35); session.authenticationSessions().removeExpired(realm); resetSession(); - assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId)); + assertNotNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId)); Time.setOffset(45); session.authenticationSessions().removeExpired(realm); resetSession(); - assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId)); + assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId)); // Access code is largest realm.setAccessCodeLifespan(50); Time.setOffset(0); - authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId(); + authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId(); resetSession(); Time.setOffset(45); session.authenticationSessions().removeExpired(realm); resetSession(); - assertNotNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId)); + assertNotNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId)); Time.setOffset(55); session.authenticationSessions().removeExpired(realm); resetSession(); - assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId)); + assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId)); } finally { Time.setOffset(0); @@ -227,8 +229,8 @@ public class AuthenticationSessionProviderTest { RealmModel fooRealm = session.realms().createRealm("foo-realm"); ClientModel fooClient = fooRealm.addClient("foo-client"); - String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId(); - String authSessionId2 = session.authenticationSessions().createAuthenticationSession(fooRealm, fooClient).getId(); + String authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId(); + String authSessionId2 = session.authenticationSessions().createRootAuthenticationSession(fooRealm).getId(); resetSession(); @@ -236,27 +238,36 @@ public class AuthenticationSessionProviderTest { resetSession(); - AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, authSessionId); - testAuthenticationSession(authSession, realm.getClientByClientId("test-app").getId(), null, null); - Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId2)); + RootAuthenticationSessionModel authSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId); + Assert.assertNotNull(authSession); + Assert.assertNull(session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId2)); } @Test public void testOnClientRemoved() { - String authSessionId = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("test-app")).getId(); - String authSessionId2 = session.authenticationSessions().createAuthenticationSession(realm, realm.getClientByClientId("third-party")).getId(); + String authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId(); + AuthenticationSessionModel authSession1 = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId).createAuthenticationSession(realm.getClientByClientId("test-app")); + AuthenticationSessionModel authSession2 = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId).createAuthenticationSession(realm.getClientByClientId("third-party")); + + authSession1.setAuthNote("foo", "bar"); + authSession2.setAuthNote("foo", "baz"); String testAppClientUUID = realm.getClientByClientId("test-app").getId(); resetSession(); + RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId); + Assert.assertEquals(2, rootAuthSession.getAuthenticationSessions().size()); + Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app")).getAuthNote("foo")); + Assert.assertEquals("baz", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("third-party")).getAuthNote("foo")); + new ClientManager(new RealmManager(session)).removeClient(realm, realm.getClientByClientId("third-party")); resetSession(); - AuthenticationSessionModel authSession = session.authenticationSessions().getAuthenticationSession(realm, authSessionId); - testAuthenticationSession(authSession, testAppClientUUID, null, null); - Assert.assertNull(session.authenticationSessions().getAuthenticationSession(realm, authSessionId2)); + rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId); + Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app")).getAuthNote("foo")); + Assert.assertNull(rootAuthSession.getAuthenticationSession(realm.getClientByClientId("third-party"))); // Revert client realm.addClient("third-party");