KEYCLOAK-5938 Authentication sessions: Support for logins of multiple tabs of same client
This commit is contained in:
parent
c3855510ef
commit
63efee6e15
57 changed files with 905 additions and 567 deletions
|
@ -35,7 +35,7 @@ import org.infinispan.commons.marshall.SerializeWith;
|
||||||
public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
||||||
|
|
||||||
private String authSessionId;
|
private String authSessionId;
|
||||||
|
private String tabId;
|
||||||
private String clientUUID;
|
private String clientUUID;
|
||||||
|
|
||||||
private Map<String, String> authNotesFragment;
|
private Map<String, String> authNotesFragment;
|
||||||
|
@ -46,9 +46,10 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
||||||
* @param authNotesFragment
|
* @param authNotesFragment
|
||||||
* @return Event. Note that {@code authNotesFragment} property is not thread safe which is fine for now.
|
* @return Event. Note that {@code authNotesFragment} property is not thread safe which is fine for now.
|
||||||
*/
|
*/
|
||||||
public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, String clientUUID, Map<String, String> authNotesFragment) {
|
public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, String tabId, String clientUUID, Map<String, String> authNotesFragment) {
|
||||||
AuthenticationSessionAuthNoteUpdateEvent event = new AuthenticationSessionAuthNoteUpdateEvent();
|
AuthenticationSessionAuthNoteUpdateEvent event = new AuthenticationSessionAuthNoteUpdateEvent();
|
||||||
event.authSessionId = authSessionId;
|
event.authSessionId = authSessionId;
|
||||||
|
event.tabId = tabId;
|
||||||
event.clientUUID = clientUUID;
|
event.clientUUID = clientUUID;
|
||||||
event.authNotesFragment = new LinkedHashMap<>(authNotesFragment);
|
event.authNotesFragment = new LinkedHashMap<>(authNotesFragment);
|
||||||
return event;
|
return event;
|
||||||
|
@ -58,6 +59,10 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
||||||
return authSessionId;
|
return authSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getTabId() {
|
||||||
|
return tabId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getClientUUID() {
|
public String getClientUUID() {
|
||||||
return clientUUID;
|
return clientUUID;
|
||||||
}
|
}
|
||||||
|
@ -68,7 +73,7 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, clientUUID=%s, authNotesFragment=%s ]",
|
return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, tabId=%s, clientUUID=%s, authNotesFragment=%s ]",
|
||||||
authSessionId, clientUUID, authNotesFragment);
|
authSessionId, clientUUID, authNotesFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +86,7 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
||||||
output.writeByte(VERSION_1);
|
output.writeByte(VERSION_1);
|
||||||
|
|
||||||
MarshallUtil.marshallString(value.authSessionId, output);
|
MarshallUtil.marshallString(value.authSessionId, output);
|
||||||
|
MarshallUtil.marshallString(value.tabId, output);
|
||||||
MarshallUtil.marshallString(value.clientUUID, output);
|
MarshallUtil.marshallString(value.clientUUID, output);
|
||||||
MarshallUtil.marshallMap(value.authNotesFragment, output);
|
MarshallUtil.marshallMap(value.authNotesFragment, output);
|
||||||
}
|
}
|
||||||
|
@ -97,6 +103,7 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
||||||
|
|
||||||
public AuthenticationSessionAuthNoteUpdateEvent readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
|
public AuthenticationSessionAuthNoteUpdateEvent readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
|
||||||
return create(
|
return create(
|
||||||
|
MarshallUtil.unmarshallString(input),
|
||||||
MarshallUtil.unmarshallString(input),
|
MarshallUtil.unmarshallString(input),
|
||||||
MarshallUtil.unmarshallString(input),
|
MarshallUtil.unmarshallString(input),
|
||||||
MarshallUtil.unmarshallMap(input, HashMap::new)
|
MarshallUtil.unmarshallMap(input, HashMap::new)
|
||||||
|
|
|
@ -40,13 +40,13 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
||||||
|
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
private final RootAuthenticationSessionAdapter parent;
|
private final RootAuthenticationSessionAdapter parent;
|
||||||
private final String clientUUID;
|
private final String tabId;
|
||||||
private AuthenticationSessionEntity entity;
|
private AuthenticationSessionEntity entity;
|
||||||
|
|
||||||
public AuthenticationSessionAdapter(KeycloakSession session, RootAuthenticationSessionAdapter parent, String clientUUID, AuthenticationSessionEntity entity) {
|
public AuthenticationSessionAdapter(KeycloakSession session, RootAuthenticationSessionAdapter parent, String tabId, AuthenticationSessionEntity entity) {
|
||||||
this.session = session;
|
this.session = session;
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.clientUUID = clientUUID;
|
this.tabId = tabId;
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +54,10 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
||||||
parent.update();
|
parent.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTabId() {
|
||||||
|
return tabId;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RootAuthenticationSessionModel getParentSession() {
|
public RootAuthenticationSessionModel getParentSession() {
|
||||||
|
@ -68,7 +72,7 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ClientModel getClient() {
|
public ClientModel getClient() {
|
||||||
return getRealm().getClientById(clientUUID);
|
return getRealm().getClientById(entity.getClientUUID());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -23,6 +23,7 @@ import java.util.Map;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -35,6 +36,7 @@ import org.keycloak.models.sessions.infinispan.stream.RootAuthenticationSessionP
|
||||||
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
|
import org.keycloak.models.sessions.infinispan.util.InfinispanKeyGenerator;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.models.utils.RealmInfoUtil;
|
import org.keycloak.models.utils.RealmInfoUtil;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||||
import org.keycloak.sessions.AuthenticationSessionProvider;
|
import org.keycloak.sessions.AuthenticationSessionProvider;
|
||||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
|
@ -162,15 +164,15 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateNonlocalSessionAuthNotes(String authSessionId, ClientModel client, Map<String, String> authNotesFragment) {
|
public void updateNonlocalSessionAuthNotes(AuthenticationSessionCompoundId compoundId, Map<String, String> authNotesFragment) {
|
||||||
if (authSessionId == null) {
|
if (compoundId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||||
cluster.notify(
|
cluster.notify(
|
||||||
InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS,
|
InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS,
|
||||||
AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, client.getId(), authNotesFragment),
|
AuthenticationSessionAuthNoteUpdateEvent.create(compoundId.getRootSessionId(), compoundId.getTabId(), compoundId.getClientUUID(), authNotesFragment),
|
||||||
true,
|
true,
|
||||||
ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC
|
ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC
|
||||||
);
|
);
|
||||||
|
@ -197,4 +199,9 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
||||||
public Cache<String, RootAuthenticationSessionEntity> getCache() {
|
public Cache<String, RootAuthenticationSessionEntity> getCache() {
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected String generateTabId() {
|
||||||
|
return Base64Url.encode(KeycloakModelUtils.generateSecret(8));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,16 +118,16 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
|
||||||
|
|
||||||
AuthenticationSessionAuthNoteUpdateEvent event = (AuthenticationSessionAuthNoteUpdateEvent) clEvent;
|
AuthenticationSessionAuthNoteUpdateEvent event = (AuthenticationSessionAuthNoteUpdateEvent) clEvent;
|
||||||
RootAuthenticationSessionEntity authSession = this.authSessionsCache.get(event.getAuthSessionId());
|
RootAuthenticationSessionEntity authSession = this.authSessionsCache.get(event.getAuthSessionId());
|
||||||
updateAuthSession(authSession, event.getClientUUID(), event.getAuthNotesFragment());
|
updateAuthSession(authSession, event.getTabId(), event.getAuthNotesFragment());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static void updateAuthSession(RootAuthenticationSessionEntity rootAuthSession, String clientUUID, Map<String, String> authNotesFragment) {
|
private static void updateAuthSession(RootAuthenticationSessionEntity rootAuthSession, String tabId, Map<String, String> authNotesFragment) {
|
||||||
if (rootAuthSession == null) {
|
if (rootAuthSession == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationSessionEntity authSession = rootAuthSession.getAuthenticationSessions().get(clientUUID);
|
AuthenticationSessionEntity authSession = rootAuthSession.getAuthenticationSessions().get(tabId);
|
||||||
|
|
||||||
if (authSession != null) {
|
if (authSession != null) {
|
||||||
if (authSession.getAuthNotes() == null) {
|
if (authSession.getAuthNotes() == null) {
|
||||||
|
|
|
@ -21,12 +21,14 @@ import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
|
||||||
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
|
@ -82,25 +84,41 @@ public class RootAuthenticationSessionAdapter implements RootAuthenticationSessi
|
||||||
Map<String, AuthenticationSessionModel> result = new HashMap<>();
|
Map<String, AuthenticationSessionModel> result = new HashMap<>();
|
||||||
|
|
||||||
for (Map.Entry<String, AuthenticationSessionEntity> entry : entity.getAuthenticationSessions().entrySet()) {
|
for (Map.Entry<String, AuthenticationSessionEntity> entry : entity.getAuthenticationSessions().entrySet()) {
|
||||||
String clientUUID = entry.getKey();
|
String tabId = entry.getKey();
|
||||||
result.put(clientUUID , new AuthenticationSessionAdapter(session, this, clientUUID, entry.getValue()));
|
result.put(tabId , new AuthenticationSessionAdapter(session, this, tabId, entry.getValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationSessionModel getAuthenticationSession(ClientModel client) {
|
public AuthenticationSessionModel getAuthenticationSession(ClientModel client, String tabId) {
|
||||||
return client==null ? null : getAuthenticationSessions().get(client.getId());
|
if (client == null || tabId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationSessionModel authSession = getAuthenticationSessions().get(tabId);
|
||||||
|
if (authSession != null && client.equals(authSession.getClient())) {
|
||||||
|
return authSession;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationSessionModel createAuthenticationSession(ClientModel client) {
|
public AuthenticationSessionModel createAuthenticationSession(ClientModel client) {
|
||||||
AuthenticationSessionEntity authSessionEntity = new AuthenticationSessionEntity();
|
AuthenticationSessionEntity authSessionEntity = new AuthenticationSessionEntity();
|
||||||
entity.getAuthenticationSessions().put(client.getId(), authSessionEntity);
|
authSessionEntity.setClientUUID(client.getId());
|
||||||
|
|
||||||
|
String tabId = provider.generateTabId();
|
||||||
|
entity.getAuthenticationSessions().put(tabId, authSessionEntity);
|
||||||
|
|
||||||
|
// Update our timestamp when adding new authenticationSession
|
||||||
|
entity.setTimestamp(Time.currentTime());
|
||||||
|
|
||||||
update();
|
update();
|
||||||
|
|
||||||
return new AuthenticationSessionAdapter(session, this, client.getId(), authSessionEntity);
|
return new AuthenticationSessionAdapter(session, this, tabId, authSessionEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -30,6 +30,8 @@ import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
*/
|
*/
|
||||||
public class AuthenticationSessionEntity implements Serializable {
|
public class AuthenticationSessionEntity implements Serializable {
|
||||||
|
|
||||||
|
private String clientUUID;
|
||||||
|
|
||||||
private String authUserId;
|
private String authUserId;
|
||||||
|
|
||||||
private String redirectUri;
|
private String redirectUri;
|
||||||
|
@ -45,6 +47,14 @@ public class AuthenticationSessionEntity implements Serializable {
|
||||||
private Set<String> requiredActions = new ConcurrentHashSet<>();
|
private Set<String> requiredActions = new ConcurrentHashSet<>();
|
||||||
private Map<String, String> userSessionNotes;
|
private Map<String, String> userSessionNotes;
|
||||||
|
|
||||||
|
public String getClientUUID() {
|
||||||
|
return clientUUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClientUUID(String clientUUID) {
|
||||||
|
this.clientUUID = clientUUID;
|
||||||
|
}
|
||||||
|
|
||||||
public String getAuthUserId() {
|
public String getAuthUserId() {
|
||||||
return authUserId;
|
return authUserId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,69 +22,61 @@ import java.util.regex.Pattern;
|
||||||
/**
|
/**
|
||||||
* Encapsulates parsing logic related to state passed to identity provider in "state" (or RelayState) parameter
|
* Encapsulates parsing logic related to state passed to identity provider in "state" (or RelayState) parameter
|
||||||
*
|
*
|
||||||
* Not Thread-safe
|
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class IdentityBrokerState {
|
public class IdentityBrokerState {
|
||||||
|
|
||||||
private String decodedState;
|
private static final Pattern DOT = Pattern.compile("\\.");
|
||||||
private String clientId;
|
|
||||||
private String encodedState;
|
|
||||||
|
|
||||||
private IdentityBrokerState() {
|
|
||||||
|
public static IdentityBrokerState decoded(String state, String clientId, String tabId) {
|
||||||
|
String encodedState = state + "." + clientId + "." + tabId;
|
||||||
|
|
||||||
|
return new IdentityBrokerState(state, clientId, tabId, encodedState);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IdentityBrokerState decoded(String decodedState, String clientId) {
|
|
||||||
IdentityBrokerState state = new IdentityBrokerState();
|
|
||||||
state.decodedState = decodedState;
|
|
||||||
state.clientId = clientId;
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IdentityBrokerState encoded(String encodedState) {
|
public static IdentityBrokerState encoded(String encodedState) {
|
||||||
IdentityBrokerState state = new IdentityBrokerState();
|
String[] decoded = DOT.split(encodedState, 3);
|
||||||
state.encodedState = encodedState;
|
|
||||||
return state;
|
String state =(decoded.length > 0) ? decoded[0] : null;
|
||||||
|
String clientId = (decoded.length > 1) ? decoded[1] : null;
|
||||||
|
String tabId = (decoded.length > 2) ? decoded[2] : null;
|
||||||
|
|
||||||
|
return new IdentityBrokerState(state, clientId, tabId, encodedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private final String decodedState;
|
||||||
|
private final String clientId;
|
||||||
|
private final String tabId;
|
||||||
|
|
||||||
|
// Encoded form of whole state
|
||||||
|
private final String encoded;
|
||||||
|
|
||||||
|
private IdentityBrokerState(String decodedStateParam, String clientId, String tabId, String encoded) {
|
||||||
|
this.decodedState = decodedStateParam;
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.tabId = tabId;
|
||||||
|
this.encoded = encoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public String getDecodedState() {
|
public String getDecodedState() {
|
||||||
if (decodedState == null) {
|
|
||||||
decode();
|
|
||||||
}
|
|
||||||
return decodedState;
|
return decodedState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getClientId() {
|
public String getClientId() {
|
||||||
if (decodedState == null) {
|
|
||||||
decode();
|
|
||||||
}
|
|
||||||
return clientId;
|
return clientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getEncodedState() {
|
public String getTabId() {
|
||||||
if (encodedState == null) {
|
return tabId;
|
||||||
encode();
|
|
||||||
}
|
|
||||||
return encodedState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getEncoded() {
|
||||||
private void decode() {
|
return encoded;
|
||||||
String[] decoded = DOT.split(encodedState, 0);
|
|
||||||
decodedState = decoded[0];
|
|
||||||
if (decoded.length > 0) {
|
|
||||||
clientId = decoded[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void encode() {
|
|
||||||
encodedState = decodedState + "." + clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Pattern DOT = Pattern.compile("\\.");
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,7 @@ public interface Constants {
|
||||||
String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
|
String VERIFY_EMAIL_KEY = "VERIFY_EMAIL_KEY";
|
||||||
String EXECUTION = "execution";
|
String EXECUTION = "execution";
|
||||||
String CLIENT_ID = "client_id";
|
String CLIENT_ID = "client_id";
|
||||||
|
String TAB_ID = "tab_id";
|
||||||
String KEY = "key";
|
String KEY = "key";
|
||||||
|
|
||||||
String SKIP_LINK = "skipLink";
|
String SKIP_LINK = "skipLink";
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* 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.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow to encode compound string to fully lookup authenticationSessionModel
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class AuthenticationSessionCompoundId {
|
||||||
|
|
||||||
|
private static final Pattern DOT = Pattern.compile("\\.");
|
||||||
|
|
||||||
|
public static AuthenticationSessionCompoundId fromAuthSession(AuthenticationSessionModel authSession) {
|
||||||
|
return decoded(authSession.getParentSession().getId(), authSession.getTabId(), authSession.getClient().getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthenticationSessionCompoundId decoded(String rootAuthSessionId, String tabId, String clientUUID) {
|
||||||
|
String encodedId = rootAuthSessionId + "." + tabId + "." + clientUUID;
|
||||||
|
return new AuthenticationSessionCompoundId(rootAuthSessionId, tabId, clientUUID, encodedId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthenticationSessionCompoundId encoded(String encodedId) {
|
||||||
|
String[] decoded = DOT.split(encodedId, 3);
|
||||||
|
|
||||||
|
String rootAuthSessionId =(decoded.length > 0) ? decoded[0] : null;
|
||||||
|
String tabId = (decoded.length > 1) ? decoded[1] : null;
|
||||||
|
String clientUUID = (decoded.length > 2) ? decoded[2] : null;
|
||||||
|
|
||||||
|
return new AuthenticationSessionCompoundId(rootAuthSessionId, tabId, clientUUID, encodedId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private final String rootSessionId;
|
||||||
|
private final String tabId;
|
||||||
|
private final String clientUUID;
|
||||||
|
private final String encodedId;
|
||||||
|
|
||||||
|
public AuthenticationSessionCompoundId(String rootSessionId, String tabId, String clientUUID, String encodedId) {
|
||||||
|
this.rootSessionId = rootSessionId;
|
||||||
|
this.tabId = tabId;
|
||||||
|
this.clientUUID = clientUUID;
|
||||||
|
this.encodedId = encodedId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRootSessionId() {
|
||||||
|
return rootSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTabId() {
|
||||||
|
return tabId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientUUID() {
|
||||||
|
return clientUUID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEncodedId() {
|
||||||
|
return encodedId;
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,12 +24,19 @@ import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Using class for now to avoid many updates among implementations
|
* Represents the state of the authentication. If the login is requested from different tabs of same browser, every browser
|
||||||
|
* tab has it's own state of the authentication. So there is separate AuthenticationSessionModel for every tab. Whole browser
|
||||||
|
* is represented by {@link RootAuthenticationSessionModel}
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public interface AuthenticationSessionModel extends CommonClientSessionModel {
|
public interface AuthenticationSessionModel extends CommonClientSessionModel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ID of this subsession (in other words, usually browser tab). For lookup the AuthenticationSessionModel, you need:
|
||||||
|
* ID of rootSession (parent), client UUID and tabId. For lookup the ID of the parent, use {@link #getParentSession().getId()}
|
||||||
|
*/
|
||||||
|
String getTabId();
|
||||||
|
|
||||||
RootAuthenticationSessionModel getParentSession();
|
RootAuthenticationSessionModel getParentSession();
|
||||||
|
|
||||||
|
|
|
@ -47,10 +47,10 @@ public interface AuthenticationSessionProvider extends Provider {
|
||||||
* Requests update of authNotes of a root authentication session that is not owned
|
* Requests update of authNotes of a root authentication session that is not owned
|
||||||
* by this instance but might exist somewhere in the cluster.
|
* by this instance but might exist somewhere in the cluster.
|
||||||
*
|
*
|
||||||
* @param authSessionId
|
* @param compoundId
|
||||||
* @param authNotesFragment Map with authNote values. Auth note is removed if the corresponding value in the map is {@code null}.
|
* @param authNotesFragment Map with authNote values. Auth note is removed if the corresponding value in the map is {@code null}.
|
||||||
*/
|
*/
|
||||||
void updateNonlocalSessionAuthNotes(String authSessionId, ClientModel client, Map<String, String> authNotesFragment);
|
void updateNonlocalSessionAuthNotes(AuthenticationSessionCompoundId compoundId, Map<String, String> authNotesFragment);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,16 +38,16 @@ public interface RootAuthenticationSessionModel {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key is client UUID, Value is AuthenticationSessionModel for particular client
|
* Key is tabId, Value is AuthenticationSessionModel.
|
||||||
* @return authentication sessions or empty map if no authenticationSessions presents. Never return null.
|
* @return authentication sessions or empty map if no authenticationSessions presents. Never return null.
|
||||||
*/
|
*/
|
||||||
Map<String, AuthenticationSessionModel> getAuthenticationSessions();
|
Map<String, AuthenticationSessionModel> getAuthenticationSessions();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return authentication session for particular client or null if it doesn't yet exists.
|
* @return authentication session for particular client and tab or null if it doesn't yet exists.
|
||||||
*/
|
*/
|
||||||
AuthenticationSessionModel getAuthenticationSession(ClientModel client);
|
AuthenticationSessionModel getAuthenticationSession(ClientModel client, String tabId);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -491,6 +491,7 @@ public class AuthenticationProcessor {
|
||||||
.queryParam(OAuth2Constants.CODE, code)
|
.queryParam(OAuth2Constants.CODE, code)
|
||||||
.queryParam(Constants.EXECUTION, getExecution().getId())
|
.queryParam(Constants.EXECUTION, getExecution().getId())
|
||||||
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
||||||
|
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
|
||||||
.build(getRealm().getName());
|
.build(getRealm().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -500,6 +501,7 @@ public class AuthenticationProcessor {
|
||||||
.queryParam(Constants.KEY, tokenString)
|
.queryParam(Constants.KEY, tokenString)
|
||||||
.queryParam(Constants.EXECUTION, getExecution().getId())
|
.queryParam(Constants.EXECUTION, getExecution().getId())
|
||||||
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
||||||
|
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
|
||||||
.build(getRealm().getName());
|
.build(getRealm().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -509,6 +511,7 @@ public class AuthenticationProcessor {
|
||||||
.path(AuthenticationProcessor.this.flowPath)
|
.path(AuthenticationProcessor.this.flowPath)
|
||||||
.queryParam(Constants.EXECUTION, getExecution().getId())
|
.queryParam(Constants.EXECUTION, getExecution().getId())
|
||||||
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
||||||
|
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
|
||||||
.build(getRealm().getName());
|
.build(getRealm().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -634,11 +637,11 @@ public class AuthenticationProcessor {
|
||||||
} else if (e.getError() == AuthenticationFlowError.FORK_FLOW) {
|
} else if (e.getError() == AuthenticationFlowError.FORK_FLOW) {
|
||||||
ForkFlowException reset = (ForkFlowException)e;
|
ForkFlowException reset = (ForkFlowException)e;
|
||||||
|
|
||||||
RootAuthenticationSessionModel rootClone = clone(session, authenticationSession.getClient(), authenticationSession.getParentSession());
|
AuthenticationSessionModel clone = clone(session, authenticationSession);
|
||||||
AuthenticationSessionModel clone = rootClone.getAuthenticationSession(authenticationSession.getClient());
|
|
||||||
|
|
||||||
clone.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
clone.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||||
setAuthenticationSession(clone);
|
setAuthenticationSession(clone);
|
||||||
|
session.getProvider(LoginFormsProvider.class).setAuthenticationSession(clone);
|
||||||
|
|
||||||
AuthenticationProcessor processor = new AuthenticationProcessor();
|
AuthenticationProcessor processor = new AuthenticationProcessor();
|
||||||
processor.setAuthenticationSession(clone)
|
processor.setAuthenticationSession(clone)
|
||||||
|
@ -763,29 +766,24 @@ public class AuthenticationProcessor {
|
||||||
authSession.setAuthNote(CURRENT_FLOW_PATH, flowPath);
|
authSession.setAuthNote(CURRENT_FLOW_PATH, flowPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
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"
|
// Clone new authentication session from the given authSession. New authenticationSession will have same parent (rootSession) and will use same client
|
||||||
for (Map.Entry<String, AuthenticationSessionModel> entry : authSession.getAuthenticationSessions().entrySet()) {
|
public static AuthenticationSessionModel clone(KeycloakSession session, AuthenticationSessionModel authSession) {
|
||||||
AuthenticationSessionModel asmOrig = entry.getValue();
|
AuthenticationSessionModel clone = authSession.getParentSession().createAuthenticationSession(authSession.getClient());
|
||||||
AuthenticationSessionModel asmClone = clone.createAuthenticationSession(asmOrig.getClient());
|
|
||||||
|
|
||||||
asmClone.setRedirectUri(asmOrig.getRedirectUri());
|
clone.setRedirectUri(authSession.getRedirectUri());
|
||||||
asmClone.setProtocol(asmOrig.getProtocol());
|
clone.setProtocol(authSession.getProtocol());
|
||||||
|
|
||||||
for (Map.Entry<String, String> clientNote : asmOrig.getClientNotes().entrySet()) {
|
for (Map.Entry<String, String> clientNote : authSession.getClientNotes().entrySet()) {
|
||||||
asmClone.setClientNote(clientNote.getKey(), clientNote.getValue());
|
clone.setClientNote(clientNote.getKey(), clientNote.getValue());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clone.setTimestamp(Time.currentTime());
|
clone.setAuthNote(FORKED_FROM, authSession.getTabId());
|
||||||
|
|
||||||
clone.getAuthenticationSession(client).setAuthNote(FORKED_FROM, authSession.getId());
|
logger.debugf("Forked authSession %s from authSession %s . Client: %s, Root session: %s",
|
||||||
logger.debugf("Forked authSession %s from authSession %s . Client: '%s'", clone.getId(), authSession.getId(), client.getClientId());
|
clone.getTabId(), authSession.getTabId(), authSession.getClient().getClientId(), authSession.getParentSession().getId());
|
||||||
|
|
||||||
return clone;
|
return clone;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -269,6 +269,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
|
||||||
.queryParam(OAuth2Constants.CODE, code)
|
.queryParam(OAuth2Constants.CODE, code)
|
||||||
.queryParam(Constants.EXECUTION, executionId)
|
.queryParam(Constants.EXECUTION, executionId)
|
||||||
.queryParam(Constants.CLIENT_ID, client.getClientId())
|
.queryParam(Constants.CLIENT_ID, client.getClientId())
|
||||||
|
.queryParam(Constants.TAB_ID, processor.getAuthenticationSession().getTabId())
|
||||||
.build(processor.getRealm().getName());
|
.build(processor.getRealm().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -139,6 +139,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
|
||||||
.queryParam(OAuth2Constants.CODE, code)
|
.queryParam(OAuth2Constants.CODE, code)
|
||||||
.queryParam(Constants.EXECUTION, getExecution())
|
.queryParam(Constants.EXECUTION, getExecution())
|
||||||
.queryParam(Constants.CLIENT_ID, client.getClientId())
|
.queryParam(Constants.CLIENT_ID, client.getClientId())
|
||||||
|
.queryParam(Constants.TAB_ID, authenticationSession.getTabId())
|
||||||
.build(getRealm().getName());
|
.build(getRealm().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -88,7 +88,7 @@ public abstract class AbstractActionTokenHander<T extends JsonWebToken> implemen
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getAuthenticationSessionIdFromToken(T token, ActionTokenContext<T> tokenContext) {
|
public String getAuthenticationSessionIdFromToken(T token, ActionTokenContext<T> tokenContext) {
|
||||||
return token instanceof DefaultActionToken ? ((DefaultActionToken) token).getAuthenticationSessionId() : null;
|
return token instanceof DefaultActionToken ? ((DefaultActionToken) token).getCompoundAuthenticationSessionId() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -45,7 +45,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface ProcessBrokerFlow {
|
public interface ProcessBrokerFlow {
|
||||||
Response brokerLoginFlow(String code, String execution, String clientId, String flowPath);
|
Response brokerLoginFlow(String code, String execution, String clientId, String tabId, String flowPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
|
@ -161,6 +161,6 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
||||||
|
|
||||||
public Response brokerFlow(String code, String flowPath) {
|
public Response brokerFlow(String code, String flowPath) {
|
||||||
ClientModel client = authenticationSession.getClient();
|
ClientModel client = authenticationSession.getClient();
|
||||||
return processBrokerFlow.brokerLoginFlow(code, getExecutionId(), client.getClientId(), flowPath);
|
return processBrokerFlow.brokerLoginFlow(code, getExecutionId(), client.getClientId(), authenticationSession.getTabId(), flowPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,18 +72,18 @@ public class DefaultActionToken extends DefaultActionTokenKey implements ActionT
|
||||||
* @param absoluteExpirationInSecs Absolute expiration time in seconds in timezone of Keycloak.
|
* @param absoluteExpirationInSecs Absolute expiration time in seconds in timezone of Keycloak.
|
||||||
* @param actionVerificationNonce
|
* @param actionVerificationNonce
|
||||||
*/
|
*/
|
||||||
protected DefaultActionToken(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce, String authenticationSessionId) {
|
protected DefaultActionToken(String userId, String actionId, int absoluteExpirationInSecs, UUID actionVerificationNonce, String compoundAuthenticationSessionId) {
|
||||||
super(userId, actionId, absoluteExpirationInSecs, actionVerificationNonce);
|
super(userId, actionId, absoluteExpirationInSecs, actionVerificationNonce);
|
||||||
setAuthenticationSessionId(authenticationSessionId);
|
setCompoundAuthenticationSessionId(compoundAuthenticationSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonProperty(value = JSON_FIELD_AUTHENTICATION_SESSION_ID)
|
@JsonProperty(value = JSON_FIELD_AUTHENTICATION_SESSION_ID)
|
||||||
public String getAuthenticationSessionId() {
|
public String getCompoundAuthenticationSessionId() {
|
||||||
return (String) getOtherClaims().get(JSON_FIELD_AUTHENTICATION_SESSION_ID);
|
return (String) getOtherClaims().get(JSON_FIELD_AUTHENTICATION_SESSION_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonProperty(value = JSON_FIELD_AUTHENTICATION_SESSION_ID)
|
@JsonProperty(value = JSON_FIELD_AUTHENTICATION_SESSION_ID)
|
||||||
public final void setAuthenticationSessionId(String authenticationSessionId) {
|
public final void setCompoundAuthenticationSessionId(String authenticationSessionId) {
|
||||||
setOtherClaims(JSON_FIELD_AUTHENTICATION_SESSION_ID, authenticationSessionId);
|
setOtherClaims(JSON_FIELD_AUTHENTICATION_SESSION_ID, authenticationSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,8 +91,8 @@ public class DefaultActionToken extends DefaultActionTokenKey implements ActionT
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> getNotes() {
|
public Map<String, String> getNotes() {
|
||||||
Map<String, String> res = new HashMap<>();
|
Map<String, String> res = new HashMap<>();
|
||||||
if (getAuthenticationSessionId() != null) {
|
if (getCompoundAuthenticationSessionId() != null) {
|
||||||
res.put(JSON_FIELD_AUTHENTICATION_SESSION_ID, getAuthenticationSessionId());
|
res.put(JSON_FIELD_AUTHENTICATION_SESSION_ID, getCompoundAuthenticationSessionId());
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,13 +24,12 @@ import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.*;
|
import org.keycloak.models.*;
|
||||||
import org.keycloak.models.UserModel.RequiredAction;
|
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||||
import org.keycloak.provider.ProviderFactory;
|
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
@ -75,8 +74,10 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander<
|
||||||
final KeycloakSession session = tokenContext.getSession();
|
final KeycloakSession session = tokenContext.getSession();
|
||||||
if (tokenContext.isAuthenticationSessionFresh()) {
|
if (tokenContext.isAuthenticationSessionFresh()) {
|
||||||
// Update the authentication session in the token
|
// Update the authentication session in the token
|
||||||
token.setAuthenticationSessionId(authSession.getParentSession().getId());
|
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
|
token.setCompoundAuthenticationSessionId(authSessionEncodedId);
|
||||||
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
||||||
|
authSession.getClient().getClientId(), authSession.getTabId());
|
||||||
String confirmUri = builder.build(realm.getName()).toString();
|
String confirmUri = builder.build(realm.getName()).toString();
|
||||||
|
|
||||||
return session.getProvider(LoginFormsProvider.class)
|
return session.getProvider(LoginFormsProvider.class)
|
||||||
|
|
|
@ -31,7 +31,6 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
|
||||||
private static final String JSON_FIELD_IDENTITY_PROVIDER_USERNAME = "idpu";
|
private static final String JSON_FIELD_IDENTITY_PROVIDER_USERNAME = "idpu";
|
||||||
private static final String JSON_FIELD_IDENTITY_PROVIDER_ALIAS = "idpa";
|
private static final String JSON_FIELD_IDENTITY_PROVIDER_ALIAS = "idpa";
|
||||||
private static final String JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID = "oasid";
|
private static final String JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID = "oasid";
|
||||||
private static final String JSON_FIELD_ORIGINAL_CLIENT_UUID = "ocid";
|
|
||||||
|
|
||||||
@JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME)
|
@JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME)
|
||||||
private String identityProviderUsername;
|
private String identityProviderUsername;
|
||||||
|
@ -42,13 +41,10 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
|
||||||
@JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
|
@JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
|
||||||
private String originalAuthenticationSessionId;
|
private String originalAuthenticationSessionId;
|
||||||
|
|
||||||
@JsonProperty(value = JSON_FIELD_ORIGINAL_CLIENT_UUID)
|
|
||||||
private String originalClientUUID;
|
|
||||||
|
|
||||||
public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String clientUUID,
|
public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId,
|
||||||
String identityProviderUsername, String identityProviderAlias) {
|
String identityProviderUsername, String identityProviderAlias) {
|
||||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
|
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
|
||||||
this.originalClientUUID = clientUUID;
|
|
||||||
this.identityProviderUsername = identityProviderUsername;
|
this.identityProviderUsername = identityProviderUsername;
|
||||||
this.identityProviderAlias = identityProviderAlias;
|
this.identityProviderAlias = identityProviderAlias;
|
||||||
}
|
}
|
||||||
|
@ -72,19 +68,12 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
|
||||||
this.identityProviderAlias = identityProviderAlias;
|
this.identityProviderAlias = identityProviderAlias;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getOriginalAuthenticationSessionId() {
|
public String getOriginalCompoundAuthenticationSessionId() {
|
||||||
return originalAuthenticationSessionId;
|
return originalAuthenticationSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) {
|
public void setOriginalCompoundAuthenticationSessionId(String originalCompoundAuthenticationSessionId) {
|
||||||
this.originalAuthenticationSessionId = originalAuthenticationSessionId;
|
this.originalAuthenticationSessionId = originalCompoundAuthenticationSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getOriginalClientUUID() {
|
|
||||||
return originalClientUUID;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOriginalClientUUID(String originalClientUUID) {
|
|
||||||
this.originalClientUUID = originalClientUUID;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -76,9 +77,12 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
|
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
|
||||||
if (tokenContext.isAuthenticationSessionFresh()) {
|
if (tokenContext.isAuthenticationSessionFresh()) {
|
||||||
token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
|
token.setOriginalCompoundAuthenticationSessionId(token.getCompoundAuthenticationSessionId());
|
||||||
token.setAuthenticationSessionId(authSession.getParentSession().getId());
|
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
|
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
||||||
|
token.setCompoundAuthenticationSessionId(authSessionEncodedId);
|
||||||
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
||||||
|
authSession.getClient().getClientId(), authSession.getTabId());
|
||||||
String confirmUri = builder.build(realm.getName()).toString();
|
String confirmUri = builder.build(realm.getName()).toString();
|
||||||
|
|
||||||
return session.getProvider(LoginFormsProvider.class)
|
return session.getProvider(LoginFormsProvider.class)
|
||||||
|
@ -91,20 +95,20 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
|
||||||
// verify user email as we know it is valid as this entry point would never have gotten here.
|
// verify user email as we know it is valid as this entry point would never have gotten here.
|
||||||
user.setEmailVerified(true);
|
user.setEmailVerified(true);
|
||||||
|
|
||||||
if (token.getOriginalAuthenticationSessionId() != null) {
|
if (token.getOriginalCompoundAuthenticationSessionId() != null) {
|
||||||
AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
||||||
asm.removeAuthenticationSession(realm, authSession, true);
|
asm.removeAuthenticationSession(realm, authSession, true);
|
||||||
|
|
||||||
ClientModel originalClient = realm.getClientById(token.getOriginalClientUUID());
|
AuthenticationSessionCompoundId compoundId = AuthenticationSessionCompoundId.encoded(token.getOriginalCompoundAuthenticationSessionId());
|
||||||
authSession = asm.getAuthenticationSessionByIdAndClient(realm, token.getOriginalAuthenticationSessionId(), originalClient);
|
ClientModel originalClient = realm.getClientById(compoundId.getClientUUID());
|
||||||
|
authSession = asm.getAuthenticationSessionByIdAndClient(realm, compoundId.getRootSessionId(), originalClient, compoundId.getTabId());
|
||||||
|
|
||||||
if (authSession != null) {
|
if (authSession != null) {
|
||||||
authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
|
authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
session.authenticationSessions().updateNonlocalSessionAuthNotes(
|
session.authenticationSessions().updateNonlocalSessionAuthNotes(
|
||||||
token.getAuthenticationSessionId(),
|
compoundId,
|
||||||
originalClient,
|
|
||||||
Collections.singletonMap(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername())
|
Collections.singletonMap(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,8 +27,8 @@ public class ResetCredentialsActionToken extends DefaultActionToken {
|
||||||
|
|
||||||
public static final String TOKEN_TYPE = "reset-credentials";
|
public static final String TOKEN_TYPE = "reset-credentials";
|
||||||
|
|
||||||
public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId) {
|
public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId) {
|
||||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
|
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResetCredentialsActionToken() {
|
private ResetCredentialsActionToken() {
|
||||||
|
|
|
@ -37,8 +37,8 @@ public class VerifyEmailActionToken extends DefaultActionToken {
|
||||||
@JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
|
@JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
|
||||||
private String originalAuthenticationSessionId;
|
private String originalAuthenticationSessionId;
|
||||||
|
|
||||||
public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String email) {
|
public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId, String email) {
|
||||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
|
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
|
||||||
this.email = email;
|
this.email = email;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,11 +53,11 @@ public class VerifyEmailActionToken extends DefaultActionToken {
|
||||||
this.email = email;
|
this.email = email;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getOriginalAuthenticationSessionId() {
|
public String getCompoundOriginalAuthenticationSessionId() {
|
||||||
return originalAuthenticationSessionId;
|
return originalAuthenticationSessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) {
|
public void setCompoundOriginalAuthenticationSessionId(String originalAuthenticationSessionId) {
|
||||||
this.originalAuthenticationSessionId = originalAuthenticationSessionId;
|
this.originalAuthenticationSessionId = originalAuthenticationSessionId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
@ -76,9 +77,12 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
|
||||||
|
|
||||||
if (tokenContext.isAuthenticationSessionFresh()) {
|
if (tokenContext.isAuthenticationSessionFresh()) {
|
||||||
// Update the authentication session in the token
|
// Update the authentication session in the token
|
||||||
token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
|
token.setCompoundOriginalAuthenticationSessionId(token.getCompoundAuthenticationSessionId());
|
||||||
token.setAuthenticationSessionId(authSession.getParentSession().getId());
|
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
|
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
||||||
|
token.setCompoundAuthenticationSessionId(authSessionEncodedId);
|
||||||
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
||||||
|
authSession.getClient().getClientId(), authSession.getTabId());
|
||||||
String confirmUri = builder.build(realm.getName()).toString();
|
String confirmUri = builder.build(realm.getName()).toString();
|
||||||
|
|
||||||
return session.getProvider(LoginFormsProvider.class)
|
return session.getProvider(LoginFormsProvider.class)
|
||||||
|
@ -95,7 +99,7 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
|
||||||
|
|
||||||
event.success();
|
event.success();
|
||||||
|
|
||||||
if (token.getOriginalAuthenticationSessionId() != null) {
|
if (token.getCompoundOriginalAuthenticationSessionId() != null) {
|
||||||
AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession());
|
AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession());
|
||||||
asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true);
|
asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true);
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.managers.ClientSessionCode;
|
import org.keycloak.services.managers.ClientSessionCode;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
@ -127,11 +128,13 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
|
||||||
.removeDetail(Details.AUTH_METHOD)
|
.removeDetail(Details.AUTH_METHOD)
|
||||||
.removeDetail(Details.AUTH_TYPE);
|
.removeDetail(Details.AUTH_TYPE);
|
||||||
|
|
||||||
|
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
||||||
IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
|
IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
|
||||||
existingUser.getId(), absoluteExpirationInSecs, authSession.getParentSession().getId(), authSession.getClient().getId(),
|
existingUser.getId(), absoluteExpirationInSecs, authSessionEncodedId,
|
||||||
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
|
brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
|
||||||
);
|
);
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
||||||
|
authSession.getClient().getClientId(), authSession.getTabId());
|
||||||
String link = builder
|
String link = builder
|
||||||
.queryParam(Constants.EXECUTION, context.getExecution().getId())
|
.queryParam(Constants.EXECUTION, context.getExecution().getId())
|
||||||
.build(realm.getName()).toString();
|
.build(realm.getName()).toString();
|
||||||
|
|
|
@ -65,8 +65,9 @@ public class IdentityProviderAuthenticator implements Authenticator {
|
||||||
if (identityProvider.isEnabled() && providerId.equals(identityProvider.getAlias())) {
|
if (identityProvider.isEnabled() && providerId.equals(identityProvider.getAlias())) {
|
||||||
String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode();
|
String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode();
|
||||||
String clientId = context.getAuthenticationSession().getClient().getClientId();
|
String clientId = context.getAuthenticationSession().getClient().getClientId();
|
||||||
|
String tabId = context.getAuthenticationSession().getTabId();
|
||||||
Response response = Response.seeOther(
|
Response response = Response.seeOther(
|
||||||
Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId))
|
Urls.identityProviderAuthnRequest(context.getUriInfo().getBaseUri(), providerId, context.getRealm().getName(), accessCode, clientId, tabId))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
LOG.debugf("Redirecting to %s", providerId);
|
LOG.debugf("Redirecting to %s", providerId);
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.keycloak.models.utils.FormMessage;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.services.ServicesLogger;
|
import org.keycloak.services.ServicesLogger;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
@ -89,7 +90,8 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
||||||
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
||||||
|
|
||||||
// We send the secret in the email in a link as a query param.
|
// We send the secret in the email in a link as a query param.
|
||||||
ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, authenticationSession.getParentSession().getId());
|
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authenticationSession).getEncodedId();
|
||||||
|
ResetCredentialsActionToken token = new ResetCredentialsActionToken(user.getId(), absoluteExpirationInSecs, authSessionEncodedId);
|
||||||
String link = UriBuilder
|
String link = UriBuilder
|
||||||
.fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), context.getRealm(), context.getUriInfo())))
|
.fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), context.getRealm(), context.getUriInfo())))
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.keycloak.models.*;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.services.validation.Validation;
|
||||||
|
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
@ -134,8 +135,10 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
|
||||||
int validityInSecs = realm.getActionTokenGeneratedByUserLifespan(VerifyEmailActionToken.TOKEN_TYPE);
|
int validityInSecs = realm.getActionTokenGeneratedByUserLifespan(VerifyEmailActionToken.TOKEN_TYPE);
|
||||||
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
||||||
|
|
||||||
VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSession.getParentSession().getId(), user.getEmail());
|
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
||||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
|
VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSessionEncodedId, user.getEmail());
|
||||||
|
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo),
|
||||||
|
authSession.getClient().getClientId(), authSession.getTabId());
|
||||||
String link = builder.build(realm.getName()).toString();
|
String link = builder.build(realm.getName()).toString();
|
||||||
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
||||||
|
|
||||||
|
|
|
@ -297,7 +297,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
||||||
protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
|
protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
|
||||||
final UriBuilder uriBuilder = UriBuilder.fromUri(getConfig().getAuthorizationUrl())
|
final UriBuilder uriBuilder = UriBuilder.fromUri(getConfig().getAuthorizationUrl())
|
||||||
.queryParam(OAUTH2_PARAMETER_SCOPE, getConfig().getDefaultScope())
|
.queryParam(OAUTH2_PARAMETER_SCOPE, getConfig().getDefaultScope())
|
||||||
.queryParam(OAUTH2_PARAMETER_STATE, request.getState().getEncodedState())
|
.queryParam(OAUTH2_PARAMETER_STATE, request.getState().getEncoded())
|
||||||
.queryParam(OAUTH2_PARAMETER_RESPONSE_TYPE, "code")
|
.queryParam(OAUTH2_PARAMETER_RESPONSE_TYPE, "code")
|
||||||
.queryParam(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
|
.queryParam(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
|
||||||
.queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri());
|
.queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri());
|
||||||
|
|
|
@ -101,7 +101,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
||||||
.protocolBinding(protocolBinding)
|
.protocolBinding(protocolBinding)
|
||||||
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
|
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
|
||||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
|
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
|
||||||
.relayState(request.getState().getEncodedState());
|
.relayState(request.getState().getEncoded());
|
||||||
boolean postBinding = getConfig().isPostBindingAuthnRequest();
|
boolean postBinding = getConfig().isPostBindingAuthnRequest();
|
||||||
|
|
||||||
if (getConfig().isWantAuthnRequestsSigned()) {
|
if (getConfig().isWantAuthnRequestsSigned()) {
|
||||||
|
|
|
@ -248,6 +248,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId());
|
uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId());
|
||||||
}
|
}
|
||||||
|
if (authenticationSession != null) {
|
||||||
|
uriBuilder.queryParam(Constants.TAB_ID, authenticationSession.getTabId());
|
||||||
|
}
|
||||||
return uriBuilder;
|
return uriBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.AuthenticationFlowModel;
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -166,7 +167,7 @@ public abstract class AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected AuthorizationEndpointChecks getOrCreateAuthenticationSession(ClientModel client, String requestState) {
|
protected AuthenticationSessionModel createAuthenticationSession(ClientModel client, String requestState) {
|
||||||
AuthenticationSessionManager manager = new AuthenticationSessionManager(session);
|
AuthenticationSessionManager manager = new AuthenticationSessionManager(session);
|
||||||
String authSessionId = manager.getCurrentAuthenticationSessionId(realm);
|
String authSessionId = manager.getCurrentAuthenticationSessionId(realm);
|
||||||
RootAuthenticationSessionModel rootAuthSession = authSessionId==null ? null : session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
RootAuthenticationSessionModel rootAuthSession = authSessionId==null ? null : session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||||
|
@ -174,128 +175,32 @@ public abstract class AuthorizationEndpointBase {
|
||||||
|
|
||||||
if (rootAuthSession != null) {
|
if (rootAuthSession != null) {
|
||||||
|
|
||||||
authSession = rootAuthSession.getAuthenticationSession(client);
|
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||||
|
|
||||||
if (authSession != null) {
|
logger.debugf("Sent request to authz endpoint. Root authentication session with ID '%s' exists. Client is '%s' . Created new authentication session with tab ID: %s",
|
||||||
ClientSessionCode<AuthenticationSessionModel> check = new ClientSessionCode<>(session, realm, authSession);
|
rootAuthSession.getId(), client.getClientId(), authSession.getTabId());
|
||||||
if (!check.isActionActive(ClientSessionCode.ActionType.LOGIN)) {
|
|
||||||
|
|
||||||
logger.debugf("Authentication session '%s' exists, but is expired. Restart existing authentication session", rootAuthSession.getId());
|
} else {
|
||||||
rootAuthSession.restartSession(realm);
|
|
||||||
authSession = rootAuthSession.createAuthenticationSession(client);
|
|
||||||
|
|
||||||
return new AuthorizationEndpointChecks(authSession);
|
UserSessionModel userSession = authSessionId == null ? null : new UserSessionCrossDCManager(session).getUserSessionIfExistsRemotely(realm, authSessionId);
|
||||||
|
|
||||||
} 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());
|
|
||||||
|
|
||||||
|
if (userSession != null) {
|
||||||
|
rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(authSessionId, realm);
|
||||||
authSession = rootAuthSession.createAuthenticationSession(client);
|
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||||
return new AuthorizationEndpointChecks(authSession);
|
logger.debugf("Sent request to authz endpoint. We don't have root authentication session with ID '%s' but we have userSession." +
|
||||||
|
"Re-created root authentication session with same ID. Client is: %s . New authentication session tab ID: %s", authSessionId, client.getClientId(), authSession.getTabId());
|
||||||
|
} else {
|
||||||
|
rootAuthSession = manager.createAuthenticationSession(realm, true);
|
||||||
|
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||||
|
logger.debugf("Sent request to authz endpoint. Created new root authentication session with ID '%s' . Client: %s . New authentication session tab ID: %s",
|
||||||
|
rootAuthSession.getId(), client.getClientId(), authSession.getTabId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UserSessionModel userSession = authSessionId==null ? null : new UserSessionCrossDCManager(session).getUserSessionIfExistsRemotely(realm, authSessionId);
|
session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession);
|
||||||
|
|
||||||
if (userSession != null) {
|
return authSession;
|
||||||
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);
|
|
||||||
rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(authSessionId, realm);
|
|
||||||
authSession = rootAuthSession.createAuthenticationSession(client);
|
|
||||||
} else {
|
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected boolean shouldRestartAuthSession(AuthenticationSessionModel authSession) {
|
|
||||||
return hasProcessedExecution(authSession);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean hasProcessedExecution(AuthenticationSessionModel authSession) {
|
|
||||||
String lastProcessedExecution = authSession.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION);
|
|
||||||
return (lastProcessedExecution != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 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
|
|
||||||
private boolean shouldShowExpirePage(AuthenticationSessionModel authSession) {
|
|
||||||
if (hasProcessedExecution(authSession)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String initialFlow = authSession.getClientNote(APP_INITIATED_FLOW);
|
|
||||||
if (initialFlow == null) {
|
|
||||||
initialFlow = LoginActionsService.AUTHENTICATE_PATH;
|
|
||||||
}
|
|
||||||
|
|
||||||
String lastFlow = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
|
|
||||||
// Check if we transitted between flows (eg. clicking "register" on login screen and then clicking browser 'back', which showed this page)
|
|
||||||
if (!initialFlow.equals(lastFlow) && AuthenticationSessionModel.Action.AUTHENTICATE.toString().equals(authSession.getAction())) {
|
|
||||||
logger.debugf("Transition between flows! Current flow: %s, Previous flow: %s", initialFlow, lastFlow);
|
|
||||||
|
|
||||||
authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, initialFlow);
|
|
||||||
authSession.removeAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to see if it is new request from the application, or refresh of some previous request
|
|
||||||
protected abstract boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String requestState);
|
|
||||||
|
|
||||||
|
|
||||||
protected static class AuthorizationEndpointChecks {
|
|
||||||
public final AuthenticationSessionModel authSession;
|
|
||||||
public final Response response;
|
|
||||||
|
|
||||||
private AuthorizationEndpointChecks(Response response) {
|
|
||||||
this.authSession = null;
|
|
||||||
this.response = response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private AuthorizationEndpointChecks(AuthenticationSessionModel authSession) {
|
|
||||||
this.authSession = authSession;
|
|
||||||
this.response = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -65,12 +65,8 @@ public class DockerEndpoint extends AuthorizationEndpointBase {
|
||||||
checkRealm();
|
checkRealm();
|
||||||
|
|
||||||
final AuthorizationEndpointRequest authRequest = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params);
|
final AuthorizationEndpointRequest authRequest = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params);
|
||||||
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, authRequest.getState());
|
authenticationSession = createAuthenticationSession(client, authRequest.getState());
|
||||||
if (checks.response != null) {
|
|
||||||
return checks.response;
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticationSession = checks.authSession;
|
|
||||||
updateAuthenticationSession();
|
updateAuthenticationSession();
|
||||||
|
|
||||||
// So back button doesn't work
|
// So back button doesn't work
|
||||||
|
@ -96,8 +92,4 @@ public class DockerEndpoint extends AuthorizationEndpointBase {
|
||||||
return realm.getDockerAuthenticationFlow();
|
return realm.getDockerAuthenticationFlow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isNewRequest(final AuthenticationSessionModel authSession, final ClientModel clientFromRequest, final String requestState) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,6 @@ import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.core.MultivaluedMap;
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@ -126,12 +125,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||||
return errorResponse;
|
return errorResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, request.getState());
|
authenticationSession = createAuthenticationSession(client, request.getState());
|
||||||
if (checks.response != null) {
|
|
||||||
return checks.response;
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticationSession = checks.authSession;
|
|
||||||
updateAuthenticationSession();
|
updateAuthenticationSession();
|
||||||
|
|
||||||
// So back button doesn't work
|
// So back button doesn't work
|
||||||
|
@ -359,64 +353,6 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String stateFromRequest) {
|
|
||||||
if (stateFromRequest==null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's different client
|
|
||||||
if (!clientFromRequest.equals(authSession.getClient())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If state is same, we likely have the refresh of some previous request
|
|
||||||
String stateFromSession = authSession.getClientNote(OIDCLoginProtocol.STATE_PARAM);
|
|
||||||
boolean stateChanged =!stateFromRequest.equals(stateFromSession);
|
|
||||||
if (stateChanged) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isOIDCAuthenticationRelatedParamsChanged(authSession);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean shouldRestartAuthSession(AuthenticationSessionModel authSession) {
|
|
||||||
return super.shouldRestartAuthSession(authSession) || isOIDCAuthenticationRelatedParamsChanged(authSession);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Check if some important OIDC parameters, which have impact on authentication, changed. If yes, we need to restart auth session
|
|
||||||
private boolean isOIDCAuthenticationRelatedParamsChanged(AuthenticationSessionModel authSession) {
|
|
||||||
if (isRequestParamChanged(authSession, OIDCLoginProtocol.LOGIN_HINT_PARAM, request.getLoginHint())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (isRequestParamChanged(authSession, OIDCLoginProtocol.PROMPT_PARAM, request.getPrompt())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (isRequestParamChanged(authSession, AdapterConstants.KC_IDP_HINT, request.getIdpHint())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String maxAgeValue = authSession.getClientNote(OIDCLoginProtocol.MAX_AGE_PARAM);
|
|
||||||
if (maxAgeValue == null && request.getMaxAge() == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (maxAgeValue != null && Integer.parseInt(maxAgeValue) == request.getMaxAge()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean isRequestParamChanged(AuthenticationSessionModel authSession, String noteName, String requestParamValue) {
|
|
||||||
String authSessionNoteValue = authSession.getClientNote(noteName);
|
|
||||||
return !Objects.equals(authSessionNoteValue, requestParamValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void updateAuthenticationSession() {
|
private void updateAuthenticationSession() {
|
||||||
authenticationSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
authenticationSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
authenticationSession.setRedirectUri(redirectUri);
|
authenticationSession.setRedirectUri(redirectUri);
|
||||||
|
|
|
@ -257,7 +257,7 @@ public class TokenEndpoint {
|
||||||
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
|
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Missing parameter: " + OAuth2Constants.CODE, Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, session, realm, client, event, AuthenticatedClientSessionModel.class);
|
ClientSessionCode.ParseResult<AuthenticatedClientSessionModel> parseResult = ClientSessionCode.parseResult(code, null, session, realm, client, event, AuthenticatedClientSessionModel.class);
|
||||||
if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) {
|
if (parseResult.isAuthSessionNotFound() || parseResult.isIllegalHash()) {
|
||||||
AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
|
AuthenticatedClientSessionModel clientSession = parseResult.getClientSession();
|
||||||
|
|
||||||
|
|
|
@ -300,12 +300,7 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI);
|
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI);
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, relayState);
|
AuthenticationSessionModel authSession = createAuthenticationSession(client, relayState);
|
||||||
if (checks.response != null) {
|
|
||||||
return checks.response;
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = checks.authSession;
|
|
||||||
|
|
||||||
authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
|
authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
|
||||||
authSession.setRedirectUri(redirect);
|
authSession.setRedirectUri(redirect);
|
||||||
|
@ -673,12 +668,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
redirect = client.getManagementUrl();
|
redirect = client.getManagementUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, null);
|
AuthenticationSessionModel authSession = createAuthenticationSession(client, null);
|
||||||
if (checks.response != null) {
|
|
||||||
throw new IllegalStateException("Not expected to detect re-sent request for IDP initiated SSO");
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = checks.authSession;
|
|
||||||
authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
|
authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
|
||||||
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||||
authSession.setClientNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING);
|
authSession.setClientNote(SamlProtocol.SAML_BINDING, SamlProtocol.SAML_POST_BINDING);
|
||||||
|
@ -696,26 +687,6 @@ public class SamlService extends AuthorizationEndpointBase {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isNewRequest(AuthenticationSessionModel authSession, ClientModel clientFromRequest, String requestRelayState) {
|
|
||||||
// No support of browser "refresh" or "back" buttons for SAML IDP initiated SSO. So always treat as new request
|
|
||||||
String idpInitiated = authSession.getClientNote(SamlProtocol.SAML_IDP_INITIATED_LOGIN);
|
|
||||||
if (Boolean.parseBoolean(idpInitiated)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestRelayState == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's different client
|
|
||||||
if (!clientFromRequest.equals(authSession.getClient())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !requestRelayState.equals(authSession.getClientNote(GeneralConstants.RELAY_STATE));
|
|
||||||
}
|
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@NoCache
|
@NoCache
|
||||||
@Consumes({"application/soap+xml",MediaType.TEXT_XML})
|
@Consumes({"application/soap+xml",MediaType.TEXT_XML})
|
||||||
|
|
|
@ -74,7 +74,7 @@ public class Urls {
|
||||||
.build(realmName, providerId);
|
.build(realmName, providerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static URI identityProviderAuthnRequest(URI baseUri, String providerId, String realmName, String accessCode, String clientId) {
|
public static URI identityProviderAuthnRequest(URI baseUri, String providerId, String realmName, String accessCode, String clientId, String tabId) {
|
||||||
UriBuilder uriBuilder = realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
UriBuilder uriBuilder = realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
||||||
.path(IdentityBrokerService.class, "performLogin");
|
.path(IdentityBrokerService.class, "performLogin");
|
||||||
|
|
||||||
|
@ -84,6 +84,9 @@ public class Urls {
|
||||||
if (clientId != null) {
|
if (clientId != null) {
|
||||||
uriBuilder.replaceQueryParam(Constants.CLIENT_ID, clientId);
|
uriBuilder.replaceQueryParam(Constants.CLIENT_ID, clientId);
|
||||||
}
|
}
|
||||||
|
if (tabId != null) {
|
||||||
|
uriBuilder.replaceQueryParam(Constants.TAB_ID, tabId);
|
||||||
|
}
|
||||||
|
|
||||||
return uriBuilder.build(realmName, providerId);
|
return uriBuilder.build(realmName, providerId);
|
||||||
}
|
}
|
||||||
|
@ -103,22 +106,24 @@ public class Urls {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static URI identityProviderAuthnRequest(URI baseURI, String providerId, String realmName) {
|
public static URI identityProviderAuthnRequest(URI baseURI, String providerId, String realmName) {
|
||||||
return identityProviderAuthnRequest(baseURI, providerId, realmName, null, null);
|
return identityProviderAuthnRequest(baseURI, providerId, realmName, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static URI identityProviderAfterFirstBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId) {
|
public static URI identityProviderAfterFirstBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId) {
|
||||||
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
||||||
.path(IdentityBrokerService.class, "afterFirstBrokerLogin")
|
.path(IdentityBrokerService.class, "afterFirstBrokerLogin")
|
||||||
.replaceQueryParam(OAuth2Constants.CODE, accessCode)
|
.replaceQueryParam(OAuth2Constants.CODE, accessCode)
|
||||||
.replaceQueryParam(Constants.CLIENT_ID, clientId)
|
.replaceQueryParam(Constants.CLIENT_ID, clientId)
|
||||||
|
.replaceQueryParam(Constants.TAB_ID, tabId)
|
||||||
.build(realmName);
|
.build(realmName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static URI identityProviderAfterPostBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId) {
|
public static URI identityProviderAfterPostBrokerLogin(URI baseUri, String realmName, String accessCode, String clientId, String tabId) {
|
||||||
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
return realmBase(baseUri).path(RealmsResource.class, "getBrokerService")
|
||||||
.path(IdentityBrokerService.class, "afterPostBrokerLoginFlow")
|
.path(IdentityBrokerService.class, "afterPostBrokerLoginFlow")
|
||||||
.replaceQueryParam(OAuth2Constants.CODE, accessCode)
|
.replaceQueryParam(OAuth2Constants.CODE, accessCode)
|
||||||
.replaceQueryParam(Constants.CLIENT_ID, clientId)
|
.replaceQueryParam(Constants.CLIENT_ID, clientId)
|
||||||
|
.replaceQueryParam(Constants.TAB_ID, tabId)
|
||||||
.build(realmName);
|
.build(realmName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,10 +187,11 @@ public class Urls {
|
||||||
return loginResetCredentialsBuilder(baseUri).build(realmName);
|
return loginResetCredentialsBuilder(baseUri).build(realmName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString, String clientId) {
|
public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString, String clientId, String tabId) {
|
||||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken")
|
return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken")
|
||||||
.queryParam("key", tokenString)
|
.queryParam("key", tokenString)
|
||||||
.queryParam(Constants.CLIENT_ID, clientId);
|
.queryParam(Constants.CLIENT_ID, clientId)
|
||||||
|
.queryParam(Constants.TAB_ID, tabId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -173,7 +173,6 @@ public class AuthenticationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do not logout broker
|
|
||||||
*
|
*
|
||||||
* @param session
|
* @param session
|
||||||
* @param realm
|
* @param realm
|
||||||
|
@ -181,6 +180,8 @@ public class AuthenticationManager {
|
||||||
* @param uriInfo
|
* @param uriInfo
|
||||||
* @param connection
|
* @param connection
|
||||||
* @param headers
|
* @param headers
|
||||||
|
* @param logoutBroker
|
||||||
|
* @param offlineSession
|
||||||
*/
|
*/
|
||||||
public static void backchannelLogout(KeycloakSession session, RealmModel realm,
|
public static void backchannelLogout(KeycloakSession session, RealmModel realm,
|
||||||
UserSessionModel userSession, UriInfo uriInfo,
|
UserSessionModel userSession, UriInfo uriInfo,
|
||||||
|
@ -197,7 +198,7 @@ public class AuthenticationManager {
|
||||||
expireUserSessionCookie(session, userSession, realm, uriInfo, headers, connection);
|
expireUserSessionCookie(session, userSession, realm, uriInfo, headers, connection);
|
||||||
|
|
||||||
final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
||||||
AuthenticationSessionModel logoutAuthSession = createOrJoinLogoutSession(realm, asm, false);
|
AuthenticationSessionModel logoutAuthSession = createOrJoinLogoutSession(session, realm, asm, userSession, false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
backchannelLogoutAll(session, realm, userSession, logoutAuthSession, uriInfo, headers, logoutBroker);
|
backchannelLogoutAll(session, realm, userSession, logoutAuthSession, uriInfo, headers, logoutBroker);
|
||||||
|
@ -215,22 +216,41 @@ public class AuthenticationManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static AuthenticationSessionModel createOrJoinLogoutSession(RealmModel realm, final AuthenticationSessionManager asm, boolean browserCookie) {
|
private static AuthenticationSessionModel createOrJoinLogoutSession(KeycloakSession session, RealmModel realm, final AuthenticationSessionManager asm, UserSessionModel userSession, boolean browserCookie) {
|
||||||
// Account management client is used as a placeholder
|
// Account management client is used as a placeholder
|
||||||
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
||||||
|
|
||||||
AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm, client);
|
// Try to lookup current authSessionId from browser cookie. If doesn't exists, use the same as current userSession
|
||||||
// Try to join existing logout session if it exists and browser session is required
|
String authSessionId = null;
|
||||||
if (browserCookie && logoutAuthSession != null) {
|
boolean browserCookiePresent = false;
|
||||||
if (Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), logoutAuthSession.getAction())) {
|
if (browserCookie) {
|
||||||
return logoutAuthSession;
|
authSessionId = asm.getCurrentAuthenticationSessionId(realm);
|
||||||
}
|
|
||||||
// Re-create the authentication session for logout
|
|
||||||
logoutAuthSession = logoutAuthSession.getParentSession().createAuthenticationSession(client);
|
|
||||||
} else {
|
|
||||||
RootAuthenticationSessionModel rootLogoutSession = asm.createAuthenticationSession(realm, browserCookie);
|
|
||||||
logoutAuthSession = rootLogoutSession.createAuthenticationSession(client);
|
|
||||||
}
|
}
|
||||||
|
if (authSessionId != null) {
|
||||||
|
browserCookiePresent = true;
|
||||||
|
} else {
|
||||||
|
authSessionId = userSession.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to join existing logout session if it exists
|
||||||
|
RootAuthenticationSessionModel rootLogoutSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||||
|
if (rootLogoutSession == null) {
|
||||||
|
rootLogoutSession = session.authenticationSessions().createRootAuthenticationSession(authSessionId, realm);
|
||||||
|
}
|
||||||
|
if (browserCookie && !browserCookiePresent) {
|
||||||
|
// Update cookie if needed
|
||||||
|
asm.setAuthSessionCookie(authSessionId, realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if we have logoutAuthSession inside current rootSession. Create new if not
|
||||||
|
Optional<AuthenticationSessionModel> found = rootLogoutSession.getAuthenticationSessions().values().stream().filter((AuthenticationSessionModel authSession) -> {
|
||||||
|
|
||||||
|
return client.equals(authSession.getClient()) && Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), authSession.getAction());
|
||||||
|
|
||||||
|
}).findFirst();
|
||||||
|
|
||||||
|
AuthenticationSessionModel logoutAuthSession = found.isPresent() ? found.get() : rootLogoutSession.createAuthenticationSession(client);
|
||||||
|
|
||||||
logoutAuthSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name());
|
logoutAuthSession.setAction(AuthenticationSessionModel.Action.LOGGING_OUT.name());
|
||||||
return logoutAuthSession;
|
return logoutAuthSession;
|
||||||
}
|
}
|
||||||
|
@ -444,7 +464,7 @@ public class AuthenticationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
||||||
AuthenticationSessionModel logoutAuthSession = createOrJoinLogoutSession(realm, asm, true);
|
AuthenticationSessionModel logoutAuthSession = createOrJoinLogoutSession(session, realm, asm, userSession, true);
|
||||||
|
|
||||||
Response response = browserLogoutAllClients(userSession, session, realm, headers, uriInfo, logoutAuthSession);
|
Response response = browserLogoutAllClients(userSession, session, realm, headers, uriInfo, logoutAuthSession);
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
|
@ -484,11 +504,9 @@ public class AuthenticationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Response finishBrowserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
|
public static Response finishBrowserLogout(KeycloakSession session, RealmModel realm, UserSessionModel userSession, UriInfo uriInfo, ClientConnection connection, HttpHeaders headers) {
|
||||||
// Account management client is used as a placeholder
|
|
||||||
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
|
||||||
|
|
||||||
final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
||||||
AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm, client);
|
AuthenticationSessionModel logoutAuthSession = createOrJoinLogoutSession(session, realm, asm, userSession, true);
|
||||||
|
|
||||||
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
|
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
|
||||||
|
|
||||||
expireIdentityCookie(realm, uriInfo, connection);
|
expireIdentityCookie(realm, uriInfo, connection);
|
||||||
|
@ -503,6 +521,7 @@ public class AuthenticationManager {
|
||||||
.setEventBuilder(event);
|
.setEventBuilder(event);
|
||||||
Response response = protocol.finishLogout(userSession);
|
Response response = protocol.finishLogout(userSession);
|
||||||
session.sessions().removeUserSession(realm, userSession);
|
session.sessions().removeUserSession(realm, userSession);
|
||||||
|
session.authenticationSessions().removeRootAuthenticationSession(realm, logoutAuthSession.getParentSession());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -733,6 +752,7 @@ public class AuthenticationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId());
|
uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId());
|
||||||
|
uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId());
|
||||||
|
|
||||||
URI redirect = uriBuilder.build(realm.getName());
|
URI redirect = uriBuilder.build(realm.getName());
|
||||||
return Response.status(302).location(redirect).build();
|
return Response.status(302).location(redirect).build();
|
||||||
|
|
|
@ -80,14 +80,14 @@ public class AuthenticationSessionManager {
|
||||||
* @param realm
|
* @param realm
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm, ClientModel client) {
|
public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm, ClientModel client, String tabId) {
|
||||||
String authSessionId = getAuthSessionCookieDecoded(realm);
|
String authSessionId = getAuthSessionCookieDecoded(realm);
|
||||||
|
|
||||||
if (authSessionId == null) {
|
if (authSessionId == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getAuthenticationSessionByIdAndClient(realm, authSessionId, client);
|
return getAuthenticationSessionByIdAndClient(realm, authSessionId, client, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -152,9 +152,9 @@ public class AuthenticationSessionManager {
|
||||||
|
|
||||||
|
|
||||||
// Don't look at cookie. Just lookup authentication session based on the ID and client. Return null if not found
|
// 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) {
|
public AuthenticationSessionModel getAuthenticationSessionByIdAndClient(RealmModel realm, String authSessionId, ClientModel client, String tabId) {
|
||||||
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||||
return rootAuthSession==null ? null : rootAuthSession.getAuthenticationSession(client);
|
return rootAuthSession==null ? null : rootAuthSession.getAuthenticationSession(client, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,8 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <CLIENT_SESSION extends CommonClientSessionModel> ParseResult<CLIENT_SESSION> parseResult(String code, KeycloakSession session, RealmModel realm, ClientModel client,
|
public static <CLIENT_SESSION extends CommonClientSessionModel> ParseResult<CLIENT_SESSION> parseResult(String code, String tabId,
|
||||||
|
KeycloakSession session, RealmModel realm, ClientModel client,
|
||||||
EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
|
EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
|
||||||
ParseResult<CLIENT_SESSION> result = new ParseResult<>();
|
ParseResult<CLIENT_SESSION> result = new ParseResult<>();
|
||||||
if (code == null) {
|
if (code == null) {
|
||||||
|
@ -89,7 +90,7 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
|
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
|
||||||
result.clientSession = getClientSession(code, session, realm, client, event, clientSessionParser);
|
result.clientSession = getClientSession(code, tabId, session, realm, client, event, clientSessionParser);
|
||||||
if (result.clientSession == null) {
|
if (result.clientSession == null) {
|
||||||
result.authSessionNotFound = true;
|
result.authSessionNotFound = true;
|
||||||
return result;
|
return result;
|
||||||
|
@ -114,16 +115,16 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, ClientModel client,
|
public static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, String tabId, KeycloakSession session, RealmModel realm, ClientModel client,
|
||||||
EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
|
EventBuilder event, Class<CLIENT_SESSION> sessionClass) {
|
||||||
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
|
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser = CodeGenerateUtil.getParser(sessionClass);
|
||||||
return getClientSession(code, session, realm, client, event, clientSessionParser);
|
return getClientSession(code, tabId, session, realm, client, event, clientSessionParser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event,
|
private static <CLIENT_SESSION extends CommonClientSessionModel> CLIENT_SESSION getClientSession(String code, String tabId, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event,
|
||||||
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser) {
|
CodeGenerateUtil.ClientSessionParser<CLIENT_SESSION> clientSessionParser) {
|
||||||
return clientSessionParser.parseSession(code, session, realm, client, event);
|
return clientSessionParser.parseSession(code, tabId, session, realm, client, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.util.TokenUtil;
|
import org.keycloak.util.TokenUtil;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* TODO: Remove this and probably also ClientSessionParser. It's uneccessary genericity and abstraction, which is not needed anymore when clientSessionModel was fully removed.
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
|
@ -79,7 +80,7 @@ class CodeGenerateUtil {
|
||||||
|
|
||||||
interface ClientSessionParser<CS extends CommonClientSessionModel> {
|
interface ClientSessionParser<CS extends CommonClientSessionModel> {
|
||||||
|
|
||||||
CS parseSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event);
|
CS parseSession(String code, String tabId, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event);
|
||||||
|
|
||||||
String retrieveCode(KeycloakSession session, CS clientSession);
|
String retrieveCode(KeycloakSession session, CS clientSession);
|
||||||
|
|
||||||
|
@ -101,9 +102,9 @@ class CodeGenerateUtil {
|
||||||
private static class AuthenticationSessionModelParser implements ClientSessionParser<AuthenticationSessionModel> {
|
private static class AuthenticationSessionModelParser implements ClientSessionParser<AuthenticationSessionModel> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event) {
|
public AuthenticationSessionModel parseSession(String code, String tabId, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event) {
|
||||||
// Read authSessionID from cookie. Code is ignored for now
|
// Read authSessionID from cookie. Code is ignored for now
|
||||||
return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client);
|
return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -163,7 +164,7 @@ class CodeGenerateUtil {
|
||||||
private CodeJWT codeJWT;
|
private CodeJWT codeJWT;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticatedClientSessionModel parseSession(String code, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event) {
|
public AuthenticatedClientSessionModel parseSession(String code, String tabId, KeycloakSession session, RealmModel realm, ClientModel client, EventBuilder event) {
|
||||||
SecretKey aesKey = session.keys().getActiveAesKey(realm).getSecretKey();
|
SecretKey aesKey = session.keys().getActiveAesKey(realm).getSecretKey();
|
||||||
SecretKey hmacKey = session.keys().getActiveHmacKey(realm).getSecretKey();
|
SecretKey hmacKey = session.keys().getActiveHmacKey(realm).getSecretKey();
|
||||||
|
|
||||||
|
|
|
@ -81,6 +81,7 @@ import org.keycloak.services.util.BrowserHistoryHelper;
|
||||||
import org.keycloak.services.util.CacheControlUtil;
|
import org.keycloak.services.util.CacheControlUtil;
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.services.validation.Validation;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -292,7 +293,16 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
|
|
||||||
// Create AuthenticationSessionModel with same ID like userSession and refresh cookie
|
// Create AuthenticationSessionModel with same ID like userSession and refresh cookie
|
||||||
UserSessionModel userSession = cookieResult.getSession();
|
UserSessionModel userSession = cookieResult.getSession();
|
||||||
AuthenticationSessionModel authSession = session.authenticationSessions().createRootAuthenticationSession(userSession.getId(), realmModel).createAuthenticationSession(client);
|
|
||||||
|
// Auth session with ID corresponding to our userSession may already exists in some rare cases (EG. if some client tried to login in another browser tab with "prompt=login")
|
||||||
|
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realmModel, userSession.getId());
|
||||||
|
if (rootAuthSession == null) {
|
||||||
|
rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(userSession.getId(), realmModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client);
|
||||||
|
|
||||||
|
// Refresh the cookie
|
||||||
new AuthenticationSessionManager(session).setAuthSessionCookie(userSession.getId(), realmModel);
|
new AuthenticationSessionManager(session).setAuthSessionCookie(userSession.getId(), realmModel);
|
||||||
|
|
||||||
ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
|
ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
|
||||||
|
@ -329,14 +339,20 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("/{provider_id}/login")
|
@Path("/{provider_id}/login")
|
||||||
public Response performPostLogin(@PathParam("provider_id") String providerId, @QueryParam("code") String code, @QueryParam("client_id") String clientId) {
|
public Response performPostLogin(@PathParam("provider_id") String providerId,
|
||||||
return performLogin(providerId, code, clientId);
|
@QueryParam("code") String code,
|
||||||
|
@QueryParam("client_id") String clientId,
|
||||||
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
|
return performLogin(providerId, code, clientId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@NoCache
|
@NoCache
|
||||||
@Path("/{provider_id}/login")
|
@Path("/{provider_id}/login")
|
||||||
public Response performLogin(@PathParam("provider_id") String providerId, @QueryParam("code") String code, @QueryParam("client_id") String clientId) {
|
public Response performLogin(@PathParam("provider_id") String providerId,
|
||||||
|
@QueryParam("code") String code,
|
||||||
|
@QueryParam("client_id") String clientId,
|
||||||
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
this.event.detail(Details.IDENTITY_PROVIDER, providerId);
|
this.event.detail(Details.IDENTITY_PROVIDER, providerId);
|
||||||
|
|
||||||
if (isDebugEnabled()) {
|
if (isDebugEnabled()) {
|
||||||
|
@ -344,7 +360,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ParsedCodeContext parsedCode = parseSessionCode(code, clientId);
|
ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
|
||||||
if (parsedCode.response != null) {
|
if (parsedCode.response != null) {
|
||||||
return parsedCode.response;
|
return parsedCode.response;
|
||||||
}
|
}
|
||||||
|
@ -541,6 +557,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
|
|
||||||
URI redirect = LoginActionsService.firstBrokerLoginProcessor(uriInfo)
|
URI redirect = LoginActionsService.firstBrokerLoginProcessor(uriInfo)
|
||||||
.queryParam(Constants.CLIENT_ID, authenticationSession.getClient().getClientId())
|
.queryParam(Constants.CLIENT_ID, authenticationSession.getClient().getClientId())
|
||||||
|
.queryParam(Constants.TAB_ID, authenticationSession.getTabId())
|
||||||
.build(realmModel.getName());
|
.build(realmModel.getName());
|
||||||
return Response.status(302).location(redirect).build();
|
return Response.status(302).location(redirect).build();
|
||||||
|
|
||||||
|
@ -576,8 +593,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
@GET
|
@GET
|
||||||
@NoCache
|
@NoCache
|
||||||
@Path("/after-first-broker-login")
|
@Path("/after-first-broker-login")
|
||||||
public Response afterFirstBrokerLogin(@QueryParam("code") String code, @QueryParam("client_id") String clientId) {
|
public Response afterFirstBrokerLogin(@QueryParam("code") String code,
|
||||||
ParsedCodeContext parsedCode = parseSessionCode(code, clientId);
|
@QueryParam("client_id") String clientId,
|
||||||
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
|
ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
|
||||||
if (parsedCode.response != null) {
|
if (parsedCode.response != null) {
|
||||||
return parsedCode.response;
|
return parsedCode.response;
|
||||||
}
|
}
|
||||||
|
@ -694,6 +713,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
|
|
||||||
URI redirect = LoginActionsService.postBrokerLoginProcessor(uriInfo)
|
URI redirect = LoginActionsService.postBrokerLoginProcessor(uriInfo)
|
||||||
.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId())
|
.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId())
|
||||||
|
.queryParam(Constants.TAB_ID, authSession.getTabId())
|
||||||
.build(realmModel.getName());
|
.build(realmModel.getName());
|
||||||
return Response.status(302).location(redirect).build();
|
return Response.status(302).location(redirect).build();
|
||||||
}
|
}
|
||||||
|
@ -704,8 +724,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
@GET
|
@GET
|
||||||
@NoCache
|
@NoCache
|
||||||
@Path("/after-post-broker-login")
|
@Path("/after-post-broker-login")
|
||||||
public Response afterPostBrokerLoginFlow(@QueryParam("code") String code, @QueryParam("client_id") String clientId) {
|
public Response afterPostBrokerLoginFlow(@QueryParam("code") String code,
|
||||||
ParsedCodeContext parsedCode = parseSessionCode(code, clientId);
|
@QueryParam("client_id") String clientId,
|
||||||
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
|
ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
|
||||||
if (parsedCode.response != null) {
|
if (parsedCode.response != null) {
|
||||||
return parsedCode.response;
|
return parsedCode.response;
|
||||||
}
|
}
|
||||||
|
@ -957,17 +979,18 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
IdentityBrokerState state = IdentityBrokerState.encoded(encodedCode);
|
IdentityBrokerState state = IdentityBrokerState.encoded(encodedCode);
|
||||||
String code = state.getDecodedState();
|
String code = state.getDecodedState();
|
||||||
String clientId = state.getClientId();
|
String clientId = state.getClientId();
|
||||||
return parseSessionCode(code, clientId);
|
String tabId = state.getTabId();
|
||||||
|
return parseSessionCode(code, clientId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ParsedCodeContext parseSessionCode(String code, String clientId) {
|
private ParsedCodeContext parseSessionCode(String code, String clientId, String tabId) {
|
||||||
if (code == null || clientId == null) {
|
if (code == null || clientId == null || tabId == null) {
|
||||||
logger.debugf("Invalid request. Authorization code or clientId was null. Code=" + code + ", clientId=" + clientId);
|
logger.debugf("Invalid request. Authorization code, clientId or tabId was null. Code=%s, clientId=%s, tabID=%s", code, clientId, tabId);
|
||||||
Response staleCodeError = redirectToErrorPage(Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
Response staleCodeError = redirectToErrorPage(Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
return ParsedCodeContext.response(staleCodeError);
|
return ParsedCodeContext.response(staleCodeError);
|
||||||
}
|
}
|
||||||
|
|
||||||
SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, request, clientConnection, session, event, code, null, clientId, LoginActionsService.AUTHENTICATE_PATH);
|
SessionCodeChecks checks = new SessionCodeChecks(realmModel, uriInfo, request, clientConnection, session, event, code, null, clientId, tabId, LoginActionsService.AUTHENTICATE_PATH);
|
||||||
checks.initialVerify();
|
checks.initialVerify();
|
||||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||||
|
|
||||||
|
@ -1047,7 +1070,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
if (clientSessionCode != null) {
|
if (clientSessionCode != null) {
|
||||||
authSession = clientSessionCode.getClientSession();
|
authSession = clientSessionCode.getClientSession();
|
||||||
String relayState = clientSessionCode.getOrGenerateCode();
|
String relayState = clientSessionCode.getOrGenerateCode();
|
||||||
encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getClientId());
|
encodedState = IdentityBrokerState.decoded(relayState, authSession.getClient().getClientId(), authSession.getTabId());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AuthenticationRequest(this.session, this.realmModel, authSession, this.request, this.uriInfo, encodedState, getRedirectUri(providerId));
|
return new AuthenticationRequest(this.session, this.realmModel, authSession, this.request, this.uriInfo, encodedState, getRedirectUri(providerId));
|
||||||
|
@ -1091,7 +1114,8 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
||||||
throw new RuntimeException(ioe);
|
throw new RuntimeException(ioe);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.status(302).location(UriBuilder.fromUri(authSession.getRedirectUri()).build()).build();
|
URI accountServiceUri = UriBuilder.fromUri(authSession.getRedirectUri()).queryParam(Constants.TAB_ID, authSession.getTabId()).build();
|
||||||
|
return Response.status(302).location(accountServiceUri).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,7 @@ import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.util.CacheControlUtil;
|
import org.keycloak.services.util.CacheControlUtil;
|
||||||
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
||||||
import org.keycloak.services.util.BrowserHistoryHelper;
|
import org.keycloak.services.util.BrowserHistoryHelper;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
|
|
||||||
|
@ -181,16 +182,16 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private SessionCodeChecks checksForCode(String code, String execution, String clientId, String flowPath) {
|
private SessionCodeChecks checksForCode(String code, String execution, String clientId, String tabId, String flowPath) {
|
||||||
SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, code, execution, clientId, flowPath);
|
SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, code, execution, clientId, tabId, flowPath);
|
||||||
res.initialVerify();
|
res.initialVerify();
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected URI getLastExecutionUrl(String flowPath, String executionId, String clientId) {
|
protected URI getLastExecutionUrl(String flowPath, String executionId, String clientId, String tabId) {
|
||||||
return new AuthenticationFlowURLHelper(session, realm, uriInfo)
|
return new AuthenticationFlowURLHelper(session, realm, uriInfo)
|
||||||
.getLastExecutionUrl(flowPath, executionId, clientId);
|
.getLastExecutionUrl(flowPath, executionId, clientId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -201,9 +202,10 @@ public class LoginActionsService {
|
||||||
*/
|
*/
|
||||||
@Path(RESTART_PATH)
|
@Path(RESTART_PATH)
|
||||||
@GET
|
@GET
|
||||||
public Response restartSession(@QueryParam("client_id") String clientId) {
|
public Response restartSession(@QueryParam("client_id") String clientId,
|
||||||
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
event.event(EventType.RESTART_AUTHENTICATION);
|
event.event(EventType.RESTART_AUTHENTICATION);
|
||||||
SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, null, null, clientId, null);
|
SessionCodeChecks checks = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, null, null, clientId, tabId, null);
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = checks.initialVerifyAuthSession();
|
AuthenticationSessionModel authSession = checks.initialVerifyAuthSession();
|
||||||
if (authSession == null) {
|
if (authSession == null) {
|
||||||
|
@ -217,7 +219,7 @@ public class LoginActionsService {
|
||||||
|
|
||||||
AuthenticationProcessor.resetFlow(authSession, flowPath);
|
AuthenticationProcessor.resetFlow(authSession, flowPath);
|
||||||
|
|
||||||
URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getClient().getClientId());
|
URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getClient().getClientId(), tabId);
|
||||||
logger.debugf("Flow restart requested. Redirecting to %s", redirectUri);
|
logger.debugf("Flow restart requested. Redirecting to %s", redirectUri);
|
||||||
return Response.status(Response.Status.FOUND).location(redirectUri).build();
|
return Response.status(Response.Status.FOUND).location(redirectUri).build();
|
||||||
}
|
}
|
||||||
|
@ -233,10 +235,11 @@ public class LoginActionsService {
|
||||||
@GET
|
@GET
|
||||||
public Response authenticate(@QueryParam("code") String code,
|
public Response authenticate(@QueryParam("code") String code,
|
||||||
@QueryParam("execution") String execution,
|
@QueryParam("execution") String execution,
|
||||||
@QueryParam("client_id") String clientId) {
|
@QueryParam("client_id") String clientId,
|
||||||
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
event.event(EventType.LOGIN);
|
event.event(EventType.LOGIN);
|
||||||
|
|
||||||
SessionCodeChecks checks = checksForCode(code, execution, clientId, AUTHENTICATE_PATH);
|
SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, AUTHENTICATE_PATH);
|
||||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||||
return checks.getResponse();
|
return checks.getResponse();
|
||||||
}
|
}
|
||||||
|
@ -302,8 +305,9 @@ public class LoginActionsService {
|
||||||
@POST
|
@POST
|
||||||
public Response authenticateForm(@QueryParam("code") String code,
|
public Response authenticateForm(@QueryParam("code") String code,
|
||||||
@QueryParam("execution") String execution,
|
@QueryParam("execution") String execution,
|
||||||
@QueryParam("client_id") String clientId) {
|
@QueryParam("client_id") String clientId,
|
||||||
return authenticate(code, execution, clientId);
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
|
return authenticate(code, execution, clientId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path(RESET_CREDENTIALS_PATH)
|
@Path(RESET_CREDENTIALS_PATH)
|
||||||
|
@ -311,14 +315,15 @@ public class LoginActionsService {
|
||||||
public Response resetCredentialsPOST(@QueryParam("code") String code,
|
public Response resetCredentialsPOST(@QueryParam("code") String code,
|
||||||
@QueryParam("execution") String execution,
|
@QueryParam("execution") String execution,
|
||||||
@QueryParam("client_id") String clientId,
|
@QueryParam("client_id") String clientId,
|
||||||
|
@QueryParam(Constants.TAB_ID) String tabId,
|
||||||
@QueryParam(Constants.KEY) String key) {
|
@QueryParam(Constants.KEY) String key) {
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
return handleActionToken(key, execution, clientId);
|
return handleActionToken(key, execution, clientId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
event.event(EventType.RESET_PASSWORD);
|
event.event(EventType.RESET_PASSWORD);
|
||||||
|
|
||||||
return resetCredentials(code, execution, clientId);
|
return resetCredentials(code, execution, clientId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -333,9 +338,10 @@ public class LoginActionsService {
|
||||||
@GET
|
@GET
|
||||||
public Response resetCredentialsGET(@QueryParam("code") String code,
|
public Response resetCredentialsGET(@QueryParam("code") String code,
|
||||||
@QueryParam("execution") String execution,
|
@QueryParam("execution") String execution,
|
||||||
@QueryParam("client_id") String clientId) {
|
@QueryParam("client_id") String clientId,
|
||||||
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
ClientModel client = realm.getClientByClientId(clientId);
|
ClientModel client = realm.getClientByClientId(clientId);
|
||||||
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client);
|
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client, tabId);
|
||||||
|
|
||||||
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
|
// we allow applications to link to reset credentials without going through OAuth or SAML handshakes
|
||||||
if (authSession == null && code == null) {
|
if (authSession == null && code == null) {
|
||||||
|
@ -350,7 +356,7 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
event.event(EventType.RESET_PASSWORD);
|
event.event(EventType.RESET_PASSWORD);
|
||||||
return resetCredentials(code, execution, clientId);
|
return resetCredentials(code, execution, clientId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationSessionModel createAuthenticationSessionForClient()
|
AuthenticationSessionModel createAuthenticationSessionForClient()
|
||||||
|
@ -380,8 +386,8 @@ public class LoginActionsService {
|
||||||
* @param execution
|
* @param execution
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
protected Response resetCredentials(String code, String execution, String clientId) {
|
protected Response resetCredentials(String code, String execution, String clientId, String tabId) {
|
||||||
SessionCodeChecks checks = checksForCode(code, execution, clientId, RESET_CREDENTIALS_PATH);
|
SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, RESET_CREDENTIALS_PATH);
|
||||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
|
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
|
||||||
return checks.getResponse();
|
return checks.getResponse();
|
||||||
}
|
}
|
||||||
|
@ -408,11 +414,12 @@ public class LoginActionsService {
|
||||||
@GET
|
@GET
|
||||||
public Response executeActionToken(@QueryParam("key") String key,
|
public Response executeActionToken(@QueryParam("key") String key,
|
||||||
@QueryParam("execution") String execution,
|
@QueryParam("execution") String execution,
|
||||||
@QueryParam("client_id") String clientId) {
|
@QueryParam("client_id") String clientId,
|
||||||
return handleActionToken(key, execution, clientId);
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
|
return handleActionToken(key, execution, clientId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected <T extends JsonWebToken & ActionTokenKeyModel> Response handleActionToken(String tokenString, String execution, String clientId) {
|
protected <T extends JsonWebToken & ActionTokenKeyModel> Response handleActionToken(String tokenString, String execution, String clientId, String tabId) {
|
||||||
T token;
|
T token;
|
||||||
ActionTokenHandler<T> handler;
|
ActionTokenHandler<T> handler;
|
||||||
ActionTokenContext<T> tokenContext;
|
ActionTokenContext<T> tokenContext;
|
||||||
|
@ -428,7 +435,7 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
if (client != null) {
|
if (client != null) {
|
||||||
session.getContext().setClient(client);
|
session.getContext().setClient(client);
|
||||||
authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client);
|
authSession = new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
event.event(EventType.EXECUTE_ACTION_TOKEN);
|
event.event(EventType.EXECUTE_ACTION_TOKEN);
|
||||||
|
@ -496,18 +503,19 @@ public class LoginActionsService {
|
||||||
tokenContext = new ActionTokenContext(session, realm, uriInfo, clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow);
|
tokenContext = new ActionTokenContext(session, realm, uriInfo, clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String tokenAuthSessionId = handler.getAuthenticationSessionIdFromToken(token, tokenContext);
|
String tokenAuthSessionCompoundId = handler.getAuthenticationSessionIdFromToken(token, tokenContext);
|
||||||
|
|
||||||
if (tokenAuthSessionId != null) {
|
if (tokenAuthSessionCompoundId != null) {
|
||||||
// This can happen if the token contains ID but user opens the link in a new browser
|
// This can happen if the token contains ID but user opens the link in a new browser
|
||||||
LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, tokenAuthSessionId);
|
String sessionId = AuthenticationSessionCompoundId.encoded(tokenAuthSessionCompoundId).getRootSessionId();
|
||||||
|
LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authSession == null) {
|
if (authSession == null) {
|
||||||
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
|
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
|
||||||
tokenContext.setAuthenticationSession(authSession, true);
|
tokenContext.setAuthenticationSession(authSession, true);
|
||||||
} else if (tokenAuthSessionId == null ||
|
} else if (tokenAuthSessionCompoundId == null ||
|
||||||
! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, tokenAuthSessionId, client)) {
|
! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, authSession, tokenAuthSessionCompoundId)) {
|
||||||
// There exists an authentication session but no auth session ID was received in the action token
|
// There exists an authentication session but no auth session ID was received in the action token
|
||||||
logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId());
|
logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId());
|
||||||
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
|
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
|
||||||
|
@ -609,8 +617,9 @@ public class LoginActionsService {
|
||||||
@GET
|
@GET
|
||||||
public Response registerPage(@QueryParam("code") String code,
|
public Response registerPage(@QueryParam("code") String code,
|
||||||
@QueryParam("execution") String execution,
|
@QueryParam("execution") String execution,
|
||||||
@QueryParam("client_id") String clientId) {
|
@QueryParam("client_id") String clientId,
|
||||||
return registerRequest(code, execution, clientId, false);
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
|
return registerRequest(code, execution, clientId, tabId,false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -624,19 +633,20 @@ public class LoginActionsService {
|
||||||
@POST
|
@POST
|
||||||
public Response processRegister(@QueryParam("code") String code,
|
public Response processRegister(@QueryParam("code") String code,
|
||||||
@QueryParam("execution") String execution,
|
@QueryParam("execution") String execution,
|
||||||
@QueryParam("client_id") String clientId) {
|
@QueryParam("client_id") String clientId,
|
||||||
return registerRequest(code, execution, clientId, true);
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
|
return registerRequest(code, execution, clientId, tabId,true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private Response registerRequest(String code, String execution, String clientId, boolean isPostRequest) {
|
private Response registerRequest(String code, String execution, String clientId, String tabId, boolean isPostRequest) {
|
||||||
event.event(EventType.REGISTER);
|
event.event(EventType.REGISTER);
|
||||||
if (!realm.isRegistrationAllowed()) {
|
if (!realm.isRegistrationAllowed()) {
|
||||||
event.error(Errors.REGISTRATION_DISABLED);
|
event.error(Errors.REGISTRATION_DISABLED);
|
||||||
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REGISTRATION_NOT_ALLOWED);
|
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.REGISTRATION_NOT_ALLOWED);
|
||||||
}
|
}
|
||||||
|
|
||||||
SessionCodeChecks checks = checksForCode(code, execution, clientId, REGISTRATION_PATH);
|
SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, REGISTRATION_PATH);
|
||||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||||
return checks.getResponse();
|
return checks.getResponse();
|
||||||
}
|
}
|
||||||
|
@ -653,42 +663,46 @@ public class LoginActionsService {
|
||||||
@GET
|
@GET
|
||||||
public Response firstBrokerLoginGet(@QueryParam("code") String code,
|
public Response firstBrokerLoginGet(@QueryParam("code") String code,
|
||||||
@QueryParam("execution") String execution,
|
@QueryParam("execution") String execution,
|
||||||
@QueryParam("client_id") String clientId) {
|
@QueryParam("client_id") String clientId,
|
||||||
return brokerLoginFlow(code, execution, clientId, FIRST_BROKER_LOGIN_PATH);
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
|
return brokerLoginFlow(code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path(FIRST_BROKER_LOGIN_PATH)
|
@Path(FIRST_BROKER_LOGIN_PATH)
|
||||||
@POST
|
@POST
|
||||||
public Response firstBrokerLoginPost(@QueryParam("code") String code,
|
public Response firstBrokerLoginPost(@QueryParam("code") String code,
|
||||||
@QueryParam("execution") String execution,
|
@QueryParam("execution") String execution,
|
||||||
@QueryParam("client_id") String clientId) {
|
@QueryParam("client_id") String clientId,
|
||||||
return brokerLoginFlow(code, execution, clientId, FIRST_BROKER_LOGIN_PATH);
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
|
return brokerLoginFlow(code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path(POST_BROKER_LOGIN_PATH)
|
@Path(POST_BROKER_LOGIN_PATH)
|
||||||
@GET
|
@GET
|
||||||
public Response postBrokerLoginGet(@QueryParam("code") String code,
|
public Response postBrokerLoginGet(@QueryParam("code") String code,
|
||||||
@QueryParam("execution") String execution,
|
@QueryParam("execution") String execution,
|
||||||
@QueryParam("client_id") String clientId) {
|
@QueryParam("client_id") String clientId,
|
||||||
return brokerLoginFlow(code, execution, clientId, POST_BROKER_LOGIN_PATH);
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
|
return brokerLoginFlow(code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path(POST_BROKER_LOGIN_PATH)
|
@Path(POST_BROKER_LOGIN_PATH)
|
||||||
@POST
|
@POST
|
||||||
public Response postBrokerLoginPost(@QueryParam("code") String code,
|
public Response postBrokerLoginPost(@QueryParam("code") String code,
|
||||||
@QueryParam("execution") String execution,
|
@QueryParam("execution") String execution,
|
||||||
@QueryParam("client_id") String clientId) {
|
@QueryParam("client_id") String clientId,
|
||||||
return brokerLoginFlow(code, execution, clientId, POST_BROKER_LOGIN_PATH);
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
|
return brokerLoginFlow(code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected Response brokerLoginFlow(String code, String execution, String clientId, String flowPath) {
|
protected Response brokerLoginFlow(String code, String execution, String clientId, String tabId, String flowPath) {
|
||||||
boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH);
|
boolean firstBrokerLogin = flowPath.equals(FIRST_BROKER_LOGIN_PATH);
|
||||||
|
|
||||||
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
|
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
|
||||||
event.event(eventType);
|
event.event(eventType);
|
||||||
|
|
||||||
SessionCodeChecks checks = checksForCode(code, execution, clientId, flowPath);
|
SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, flowPath);
|
||||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||||
return checks.getResponse();
|
return checks.getResponse();
|
||||||
}
|
}
|
||||||
|
@ -747,8 +761,9 @@ public class LoginActionsService {
|
||||||
authSession.getParentSession().setTimestamp(Time.currentTime());
|
authSession.getParentSession().setTimestamp(Time.currentTime());
|
||||||
|
|
||||||
String clientId = authSession.getClient().getClientId();
|
String clientId = authSession.getClient().getClientId();
|
||||||
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId) :
|
String tabId = authSession.getTabId();
|
||||||
Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId) ;
|
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId, tabId) :
|
||||||
|
Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId, tabId) ;
|
||||||
logger.debugf("Redirecting to '%s' ", redirect);
|
logger.debugf("Redirecting to '%s' ", redirect);
|
||||||
|
|
||||||
return Response.status(302).location(redirect).build();
|
return Response.status(302).location(redirect).build();
|
||||||
|
@ -768,7 +783,8 @@ public class LoginActionsService {
|
||||||
event.event(EventType.LOGIN);
|
event.event(EventType.LOGIN);
|
||||||
String code = formData.getFirst("code");
|
String code = formData.getFirst("code");
|
||||||
String clientId = uriInfo.getQueryParameters().getFirst(Constants.CLIENT_ID);
|
String clientId = uriInfo.getQueryParameters().getFirst(Constants.CLIENT_ID);
|
||||||
SessionCodeChecks checks = checksForCode(code, null, clientId, REQUIRED_ACTION);
|
String tabId = uriInfo.getQueryParameters().getFirst(Constants.TAB_ID);
|
||||||
|
SessionCodeChecks checks = checksForCode(code, null, clientId, tabId, REQUIRED_ACTION);
|
||||||
if (!checks.verifyRequiredAction(AuthenticationSessionModel.Action.OAUTH_GRANT.name())) {
|
if (!checks.verifyRequiredAction(AuthenticationSessionModel.Action.OAUTH_GRANT.name())) {
|
||||||
return checks.getResponse();
|
return checks.getResponse();
|
||||||
}
|
}
|
||||||
|
@ -858,22 +874,24 @@ public class LoginActionsService {
|
||||||
@POST
|
@POST
|
||||||
public Response requiredActionPOST(@QueryParam("code") final String code,
|
public Response requiredActionPOST(@QueryParam("code") final String code,
|
||||||
@QueryParam("execution") String action,
|
@QueryParam("execution") String action,
|
||||||
@QueryParam("client_id") String clientId) {
|
@QueryParam("client_id") String clientId,
|
||||||
return processRequireAction(code, action, clientId);
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
|
return processRequireAction(code, action, clientId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path(REQUIRED_ACTION)
|
@Path(REQUIRED_ACTION)
|
||||||
@GET
|
@GET
|
||||||
public Response requiredActionGET(@QueryParam("code") final String code,
|
public Response requiredActionGET(@QueryParam("code") final String code,
|
||||||
@QueryParam("execution") String action,
|
@QueryParam("execution") String action,
|
||||||
@QueryParam("client_id") String clientId) {
|
@QueryParam("client_id") String clientId,
|
||||||
return processRequireAction(code, action, clientId);
|
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||||
|
return processRequireAction(code, action, clientId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response processRequireAction(final String code, String action, String clientId) {
|
private Response processRequireAction(final String code, String action, String clientId, String tabId) {
|
||||||
event.event(EventType.CUSTOM_REQUIRED_ACTION);
|
event.event(EventType.CUSTOM_REQUIRED_ACTION);
|
||||||
|
|
||||||
SessionCodeChecks checks = checksForCode(code, action, clientId, REQUIRED_ACTION);
|
SessionCodeChecks checks = checksForCode(code, action, clientId, tabId, REQUIRED_ACTION);
|
||||||
if (!checks.verifyRequiredAction(action)) {
|
if (!checks.verifyRequiredAction(action)) {
|
||||||
return checks.getResponse();
|
return checks.getResponse();
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.keycloak.representations.JsonWebToken;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.CommonClientSessionModel.Action;
|
import org.keycloak.sessions.CommonClientSessionModel.Action;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -251,46 +252,34 @@ public class LoginActionsServiceChecks {
|
||||||
*
|
*
|
||||||
* @param <T>
|
* @param <T>
|
||||||
*/
|
*/
|
||||||
public static <T extends JsonWebToken> boolean doesAuthenticationSessionFromCookieMatchOneFromToken(ActionTokenContext<T> context, String authSessionIdFromToken, ClientModel client) throws VerificationException {
|
public static <T extends JsonWebToken> boolean doesAuthenticationSessionFromCookieMatchOneFromToken(
|
||||||
if (authSessionIdFromToken == null) {
|
ActionTokenContext<T> context, AuthenticationSessionModel authSessionFromCookie, String authSessionCompoundIdFromToken) throws VerificationException {
|
||||||
|
if (authSessionCompoundIdFromToken == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationSessionManager asm = new AuthenticationSessionManager(context.getSession());
|
|
||||||
String authSessionIdFromCookie = asm.getCurrentAuthenticationSessionId(context.getRealm());
|
|
||||||
|
|
||||||
if (authSessionIdFromCookie == null) {
|
if (Objects.equals(AuthenticationSessionCompoundId.fromAuthSession(authSessionFromCookie).getEncodedId(), authSessionCompoundIdFromToken)) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthenticationSessionModel authSessionFromCookie = asm.getAuthenticationSessionByIdAndClient(context.getRealm(), authSessionIdFromCookie, client);
|
|
||||||
if (authSessionFromCookie == null) { // Not our client in root session
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Objects.equals(authSessionIdFromCookie, authSessionIdFromToken)) {
|
|
||||||
context.setAuthenticationSession(authSessionFromCookie, false);
|
context.setAuthenticationSession(authSessionFromCookie, false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String parentSessionId = authSessionFromCookie.getAuthNote(AuthenticationProcessor.FORKED_FROM);
|
// Check if it's forked session. It would have same parent (rootSession) as our browser authenticationSession
|
||||||
if (parentSessionId == null || ! Objects.equals(authSessionIdFromToken, parentSessionId)) {
|
String parentTabId = authSessionFromCookie.getAuthNote(AuthenticationProcessor.FORKED_FROM);
|
||||||
|
if (parentTabId == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationSessionModel authSessionFromParent = asm.getAuthenticationSessionByIdAndClient(context.getRealm(), parentSessionId, client);
|
|
||||||
|
AuthenticationSessionModel authSessionFromParent = authSessionFromCookie.getParentSession().getAuthenticationSession(authSessionFromCookie.getClient(), parentTabId);
|
||||||
if (authSessionFromParent == null) {
|
if (authSessionFromParent == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's the correct browser. Let's remove forked session as we won't continue
|
// It's the correct browser. We won't continue login
|
||||||
// from the login form (browser flow) but from the token's flow
|
// from the login form (browser flow) but from the token's flow
|
||||||
// Don't expire KC_RESTART cookie at this point
|
// Don't expire KC_RESTART cookie at this point
|
||||||
asm.removeAuthenticationSession(context.getRealm(), authSessionFromCookie, false);
|
LOG.debugf("Switched to forked tab: %s from: %s . Root session: %s", authSessionFromParent.getTabId(), authSessionFromCookie.getTabId(), authSessionFromCookie.getParentSession().getId());
|
||||||
LOG.debugf("Removed forked session: %s", authSessionFromCookie.getParentSession().getId());
|
|
||||||
|
|
||||||
// Refresh browser cookie
|
|
||||||
asm.setAuthSessionCookie(parentSessionId, context.getRealm());
|
|
||||||
|
|
||||||
context.setAuthenticationSession(authSessionFromParent, false);
|
context.setAuthenticationSession(authSessionFromParent, false);
|
||||||
context.setExecutionId(authSessionFromParent.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION));
|
context.setExecutionId(authSessionFromParent.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION));
|
||||||
|
|
|
@ -69,10 +69,12 @@ public class SessionCodeChecks {
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String execution;
|
private final String execution;
|
||||||
private final String clientId;
|
private final String clientId;
|
||||||
|
private final String tabId;
|
||||||
private final String flowPath;
|
private final String flowPath;
|
||||||
|
|
||||||
|
|
||||||
public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event, String code, String execution, String clientId, String flowPath) {
|
public SessionCodeChecks(RealmModel realm, UriInfo uriInfo, HttpRequest request, ClientConnection clientConnection, KeycloakSession session, EventBuilder event,
|
||||||
|
String code, String execution, String clientId, String tabId, String flowPath) {
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
this.uriInfo = uriInfo;
|
this.uriInfo = uriInfo;
|
||||||
this.request = request;
|
this.request = request;
|
||||||
|
@ -83,6 +85,7 @@ public class SessionCodeChecks {
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.execution = execution;
|
this.execution = execution;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
|
this.tabId = tabId;
|
||||||
this.flowPath = flowPath;
|
this.flowPath = flowPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,8 +148,9 @@ public class SessionCodeChecks {
|
||||||
|
|
||||||
// object retrieve
|
// object retrieve
|
||||||
AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session);
|
AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session);
|
||||||
AuthenticationSessionModel authSession = authSessionManager.getCurrentAuthenticationSession(realm, client);
|
AuthenticationSessionModel authSession = authSessionManager.getCurrentAuthenticationSession(realm, client, tabId);
|
||||||
if (authSession != null) {
|
if (authSession != null) {
|
||||||
|
session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession);
|
||||||
return authSession;
|
return authSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,14 +250,14 @@ public class SessionCodeChecks {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, session, realm, client, event, AuthenticationSessionModel.class);
|
ClientSessionCode.ParseResult<AuthenticationSessionModel> result = ClientSessionCode.parseResult(code, tabId, session, realm, client, event, AuthenticationSessionModel.class);
|
||||||
clientCode = result.getCode();
|
clientCode = result.getCode();
|
||||||
if (clientCode == null) {
|
if (clientCode == null) {
|
||||||
|
|
||||||
// In case that is replayed action, but sent to the same FORM like actual FORM, we just re-render the page
|
// In case that is replayed action, but sent to the same FORM like actual FORM, we just re-render the page
|
||||||
if (ObjectUtil.isEqualOrBothNull(execution, authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION))) {
|
if (ObjectUtil.isEqualOrBothNull(execution, authSession.getAuthNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION))) {
|
||||||
String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
|
String latestFlowPath = authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH);
|
||||||
URI redirectUri = getLastExecutionUrl(latestFlowPath, execution, client.getClientId());
|
URI redirectUri = getLastExecutionUrl(latestFlowPath, execution, tabId);
|
||||||
|
|
||||||
logger.debugf("Invalid action code, but execution matches. So just redirecting to %s", redirectUri);
|
logger.debugf("Invalid action code, but execution matches. So just redirecting to %s", redirectUri);
|
||||||
authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.EXPIRED_ACTION);
|
authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.EXPIRED_ACTION);
|
||||||
|
@ -308,7 +312,7 @@ public class SessionCodeChecks {
|
||||||
|
|
||||||
authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.LOGIN_TIMEOUT);
|
authSession.setAuthNote(LoginActionsService.FORWARDED_ERROR_MESSAGE_NOTE, Messages.LOGIN_TIMEOUT);
|
||||||
|
|
||||||
URI redirectUri = getLastExecutionUrl(LoginActionsService.AUTHENTICATE_PATH, null, authSession.getClient().getClientId());
|
URI redirectUri = getLastExecutionUrl(LoginActionsService.AUTHENTICATE_PATH, null, tabId);
|
||||||
logger.debugf("Flow restart after timeout. Redirecting to %s", redirectUri);
|
logger.debugf("Flow restart after timeout. Redirecting to %s", redirectUri);
|
||||||
response = Response.status(Response.Status.FOUND).location(redirectUri).build();
|
response = Response.status(Response.Status.FOUND).location(redirectUri).build();
|
||||||
return false;
|
return false;
|
||||||
|
@ -371,7 +375,7 @@ public class SessionCodeChecks {
|
||||||
flowPath = LoginActionsService.AUTHENTICATE_PATH;
|
flowPath = LoginActionsService.AUTHENTICATE_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getClient().getClientId());
|
URI redirectUri = getLastExecutionUrl(flowPath, null, authSession.getTabId());
|
||||||
logger.debugf("Authentication session restart from cookie succeeded. Redirecting to %s", redirectUri);
|
logger.debugf("Authentication session restart from cookie succeeded. Redirecting to %s", redirectUri);
|
||||||
return Response.status(Response.Status.FOUND).location(redirectUri).build();
|
return Response.status(Response.Status.FOUND).location(redirectUri).build();
|
||||||
} else {
|
} else {
|
||||||
|
@ -392,15 +396,16 @@ public class SessionCodeChecks {
|
||||||
|
|
||||||
ClientModel client = authSession.getClient();
|
ClientModel client = authSession.getClient();
|
||||||
uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId());
|
uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId());
|
||||||
|
uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId());
|
||||||
|
|
||||||
URI redirect = uriBuilder.build(realm.getName());
|
URI redirect = uriBuilder.build(realm.getName());
|
||||||
return Response.status(302).location(redirect).build();
|
return Response.status(302).location(redirect).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private URI getLastExecutionUrl(String flowPath, String executionId, String clientId) {
|
private URI getLastExecutionUrl(String flowPath, String executionId, String tabId) {
|
||||||
return new AuthenticationFlowURLHelper(session, realm, uriInfo)
|
return new AuthenticationFlowURLHelper(session, realm, uriInfo)
|
||||||
.getLastExecutionUrl(flowPath, executionId, clientId);
|
.getLastExecutionUrl(flowPath, executionId, clientId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -181,16 +181,20 @@ public class AccountFormService extends AbstractSecuredLocalService {
|
||||||
setReferrerOnPage();
|
setReferrerOnPage();
|
||||||
|
|
||||||
UserSessionModel userSession = auth.getSession();
|
UserSessionModel userSession = auth.getSession();
|
||||||
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getAuthenticationSessionByIdAndClient(realm, userSession.getId(), client);
|
|
||||||
if (authSession != null) {
|
String tabId = request.getUri().getQueryParameters().getFirst(org.keycloak.models.Constants.TAB_ID);
|
||||||
String forwardedError = authSession.getAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
|
if (tabId != null) {
|
||||||
if (forwardedError != null) {
|
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getAuthenticationSessionByIdAndClient(realm, userSession.getId(), client, tabId);
|
||||||
try {
|
if (authSession != null) {
|
||||||
FormMessage errorMessage = JsonSerialization.readValue(forwardedError, FormMessage.class);
|
String forwardedError = authSession.getAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
|
||||||
account.setError(Response.Status.INTERNAL_SERVER_ERROR, errorMessage.getMessage(), errorMessage.getParameters());
|
if (forwardedError != null) {
|
||||||
authSession.removeAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
|
try {
|
||||||
} catch (IOException ioe) {
|
FormMessage errorMessage = JsonSerialization.readValue(forwardedError, FormMessage.class);
|
||||||
throw new RuntimeException(ioe);
|
account.setError(Response.Status.INTERNAL_SERVER_ERROR, errorMessage.getMessage(), errorMessage.getParameters());
|
||||||
|
authSession.removeAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ public class AuthenticationFlowURLHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public URI getLastExecutionUrl(String flowPath, String executionId, String clientId) {
|
public URI getLastExecutionUrl(String flowPath, String executionId, String clientId, String tabId) {
|
||||||
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo)
|
UriBuilder uriBuilder = LoginActionsService.loginActionsBaseUrl(uriInfo)
|
||||||
.path(flowPath);
|
.path(flowPath);
|
||||||
|
|
||||||
|
@ -71,6 +71,7 @@ public class AuthenticationFlowURLHelper {
|
||||||
uriBuilder.queryParam(Constants.EXECUTION, executionId);
|
uriBuilder.queryParam(Constants.EXECUTION, executionId);
|
||||||
}
|
}
|
||||||
uriBuilder.queryParam(Constants.CLIENT_ID, clientId);
|
uriBuilder.queryParam(Constants.CLIENT_ID, clientId);
|
||||||
|
uriBuilder.queryParam(Constants.TAB_ID, tabId);
|
||||||
|
|
||||||
return uriBuilder.build(realm.getName());
|
return uriBuilder.build(realm.getName());
|
||||||
}
|
}
|
||||||
|
@ -88,7 +89,7 @@ public class AuthenticationFlowURLHelper {
|
||||||
latestFlowPath = LoginActionsService.AUTHENTICATE_PATH;
|
latestFlowPath = LoginActionsService.AUTHENTICATE_PATH;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getLastExecutionUrl(latestFlowPath, executionId, authSession.getClient().getClientId());
|
return getLastExecutionUrl(latestFlowPath, executionId, authSession.getClient().getClientId(), authSession.getTabId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getExecutionId(AuthenticationSessionModel authSession) {
|
private String getExecutionId(AuthenticationSessionModel authSession) {
|
||||||
|
|
|
@ -87,7 +87,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
||||||
Twitter twitter = new TwitterFactory().getInstance();
|
Twitter twitter = new TwitterFactory().getInstance();
|
||||||
twitter.setOAuthConsumer(getConfig().getClientId(), getConfig().getClientSecret());
|
twitter.setOAuthConsumer(getConfig().getClientId(), getConfig().getClientSecret());
|
||||||
|
|
||||||
URI uri = new URI(request.getRedirectUri() + "?state=" + request.getState().getEncodedState());
|
URI uri = new URI(request.getRedirectUri() + "?state=" + request.getState().getEncoded());
|
||||||
|
|
||||||
RequestToken requestToken = twitter.getOAuthRequestToken(uri.toString());
|
RequestToken requestToken = twitter.getOAuthRequestToken(uri.toString());
|
||||||
AuthenticationSessionModel authSession = request.getAuthenticationSession();
|
AuthenticationSessionModel authSession = request.getAuthenticationSession();
|
||||||
|
@ -198,9 +198,17 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
||||||
|
|
||||||
twitter.setOAuthConsumer(getConfig().getClientId(), getConfig().getClientSecret());
|
twitter.setOAuthConsumer(getConfig().getClientId(), getConfig().getClientSecret());
|
||||||
|
|
||||||
String clientId = IdentityBrokerState.encoded(state).getClientId();
|
IdentityBrokerState idpState = IdentityBrokerState.encoded(state);
|
||||||
|
String clientId = idpState.getClientId();
|
||||||
|
String tabId = idpState.getTabId();
|
||||||
|
if (clientId == null || tabId == null) {
|
||||||
|
logger.errorf("Invalid state parameter: %s", state);
|
||||||
|
sendErrorEvent();
|
||||||
|
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
ClientModel client = realm.getClientByClientId(clientId);
|
ClientModel client = realm.getClientByClientId(clientId);
|
||||||
authSession = ClientSessionCode.getClientSession(state, session, realm, client, event, AuthenticationSessionModel.class);
|
authSession = ClientSessionCode.getClientSession(state, tabId, session, realm, client, event, AuthenticationSessionModel.class);
|
||||||
|
|
||||||
String twitterToken = authSession.getAuthNote(TWITTER_TOKEN);
|
String twitterToken = authSession.getAuthNote(TWITTER_TOKEN);
|
||||||
String twitterSecret = authSession.getAuthNote(TWITTER_TOKENSECRET);
|
String twitterSecret = authSession.getAuthNote(TWITTER_TOKENSECRET);
|
||||||
|
@ -239,7 +247,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
||||||
sendErrorEvent();
|
sendErrorEvent();
|
||||||
return e.getResponse();
|
return e.getResponse();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Could get user profile from twitter.", e);
|
logger.error("Couldn't get user profile from twitter.", e);
|
||||||
sendErrorEvent();
|
sendErrorEvent();
|
||||||
return ErrorPage.error(session, authSession, Response.Status.BAD_GATEWAY, Messages.UNEXPECTED_ERROR_HANDLING_RESPONSE);
|
return ErrorPage.error(session, authSession, Response.Status.BAD_GATEWAY, Messages.UNEXPECTED_ERROR_HANDLING_RESPONSE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package org.keycloak.testsuite.updaters;
|
package org.keycloak.testsuite.updaters;
|
||||||
|
|
||||||
import org.keycloak.admin.client.resource.UserResource;
|
import org.keycloak.admin.client.resource.UserResource;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.representations.idm.UserRepresentation;
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -52,4 +54,12 @@ public class UserAttributeUpdater {
|
||||||
|
|
||||||
return () -> userResource.update(origRep);
|
return () -> userResource.update(origRep);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserAttributeUpdater setRequiredActions(UserModel.RequiredAction... requiredAction) {
|
||||||
|
rep.setRequiredActions(Arrays.stream(requiredAction)
|
||||||
|
.map(action -> action.name())
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.actions;
|
package org.keycloak.testsuite.actions;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||||
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
|
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
@ -45,6 +46,7 @@ import org.keycloak.testsuite.pages.VerifyEmailPage;
|
||||||
import org.keycloak.testsuite.updaters.UserAttributeUpdater;
|
import org.keycloak.testsuite.updaters.UserAttributeUpdater;
|
||||||
import org.keycloak.testsuite.util.GreenMailRule;
|
import org.keycloak.testsuite.util.GreenMailRule;
|
||||||
import org.keycloak.testsuite.util.MailUtils;
|
import org.keycloak.testsuite.util.MailUtils;
|
||||||
|
import org.keycloak.testsuite.util.SecondBrowser;
|
||||||
import org.keycloak.testsuite.util.UserActionTokenBuilder;
|
import org.keycloak.testsuite.util.UserActionTokenBuilder;
|
||||||
import org.keycloak.testsuite.util.UserBuilder;
|
import org.keycloak.testsuite.util.UserBuilder;
|
||||||
|
|
||||||
|
@ -58,6 +60,12 @@ import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
|
import org.openqa.selenium.By;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
import org.openqa.selenium.WebElement;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.core.Is.is;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,6 +102,10 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
||||||
|
|
||||||
private String testUserId;
|
private String testUserId;
|
||||||
|
|
||||||
|
@Drone
|
||||||
|
@SecondBrowser
|
||||||
|
protected WebDriver driver2;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
testRealm.setVerifyEmail(Boolean.TRUE);
|
testRealm.setVerifyEmail(Boolean.TRUE);
|
||||||
|
@ -617,4 +629,172 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyEmailDuringAuthFlow() throws IOException, MessagingException {
|
||||||
|
try (Closeable u = new UserAttributeUpdater(testRealm().users().get(testUserId))
|
||||||
|
.setEmailVerified(false)
|
||||||
|
.setRequiredActions(RequiredAction.VERIFY_EMAIL)
|
||||||
|
.update()) {
|
||||||
|
accountPage.setAuthRealm(testRealm().toRepresentation().getRealm());
|
||||||
|
accountPage.navigateTo();
|
||||||
|
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
|
||||||
|
verifyEmailPage.assertCurrent();
|
||||||
|
|
||||||
|
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||||
|
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||||
|
|
||||||
|
String verificationUrl = getPasswordResetEmailLink(message);
|
||||||
|
|
||||||
|
driver.navigate().to(verificationUrl.trim());
|
||||||
|
|
||||||
|
accountPage.assertCurrent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyEmailDuringAuthFlowFirstClickLink() throws IOException, MessagingException {
|
||||||
|
try (Closeable u = new UserAttributeUpdater(testRealm().users().get(testUserId))
|
||||||
|
.setEmailVerified(false)
|
||||||
|
.setRequiredActions(RequiredAction.VERIFY_EMAIL)
|
||||||
|
.update()) {
|
||||||
|
testRealm().users().get(testUserId).executeActionsEmail(Arrays.asList(RequiredAction.VERIFY_EMAIL.name()));
|
||||||
|
|
||||||
|
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||||
|
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||||
|
|
||||||
|
String verificationUrl = getPasswordResetEmailLink(message);
|
||||||
|
|
||||||
|
driver.manage().deleteAllCookies();
|
||||||
|
|
||||||
|
driver.navigate().to(verificationUrl);
|
||||||
|
|
||||||
|
accountPage.setAuthRealm(testRealm().toRepresentation().getRealm());
|
||||||
|
accountPage.navigateTo();
|
||||||
|
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
|
||||||
|
verifyEmailPage.assertCurrent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyEmailClickLinkRequiredActionsCleared() throws IOException, MessagingException {
|
||||||
|
try (Closeable u = new UserAttributeUpdater(testRealm().users().get(testUserId))
|
||||||
|
.setEmailVerified(true)
|
||||||
|
.setRequiredActions()
|
||||||
|
.update()) {
|
||||||
|
testRealm().users().get(testUserId).executeActionsEmail(Arrays.asList(RequiredAction.VERIFY_EMAIL.name()));
|
||||||
|
|
||||||
|
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||||
|
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||||
|
|
||||||
|
String verificationUrl = getPasswordResetEmailLink(message);
|
||||||
|
|
||||||
|
driver.manage().deleteAllCookies();
|
||||||
|
|
||||||
|
driver.navigate().to(verificationUrl);
|
||||||
|
|
||||||
|
accountPage.setAuthRealm(testRealm().toRepresentation().getRealm());
|
||||||
|
accountPage.navigateTo();
|
||||||
|
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
|
||||||
|
accountPage.assertCurrent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyEmailDuringAuthFlowAfterLogout() throws IOException, MessagingException {
|
||||||
|
try (Closeable u = new UserAttributeUpdater(testRealm().users().get(testUserId))
|
||||||
|
.setEmailVerified(true)
|
||||||
|
.update()) {
|
||||||
|
accountPage.setAuthRealm(testRealm().toRepresentation().getRealm());
|
||||||
|
accountPage.navigateTo();
|
||||||
|
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
|
||||||
|
accountPage.assertCurrent();
|
||||||
|
|
||||||
|
driver.navigate().to(oauth.getLogoutUrl().redirectUri(accountPage.buildUri().toString()).build());
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
|
||||||
|
verifyEmailDuringAuthFlow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void verifyEmailDuringAuthFlowAfterRefresh() throws IOException, MessagingException {
|
||||||
|
try (Closeable u = new UserAttributeUpdater(testRealm().users().get(testUserId))
|
||||||
|
.setEmailVerified(true)
|
||||||
|
.update()) {
|
||||||
|
final String testRealmName = testRealm().toRepresentation().getRealm();
|
||||||
|
accountPage.setAuthRealm(testRealmName);
|
||||||
|
|
||||||
|
// Browser 1: Log in
|
||||||
|
accountPage.navigateTo();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
accountPage.assertCurrent();
|
||||||
|
|
||||||
|
// Browser 2: Log in
|
||||||
|
driver2.navigate().to(accountPage.buildUri().toString());
|
||||||
|
|
||||||
|
assertThat(driver2.getTitle(), is("Log in to " + testRealmName));
|
||||||
|
driver2.findElement(By.id("username")).sendKeys("test-user@localhost");
|
||||||
|
driver2.findElement(By.id("password")).sendKeys("password");
|
||||||
|
driver2.findElement(By.id("password")).submit();
|
||||||
|
|
||||||
|
assertThat(driver2.getCurrentUrl(), Matchers.startsWith(accountPage.buildUri().toString()));
|
||||||
|
|
||||||
|
// Admin: set required action to VERIFY_EMAIL
|
||||||
|
try (Closeable u1 = new UserAttributeUpdater(testRealm().users().get(testUserId))
|
||||||
|
.setEmailVerified(false)
|
||||||
|
.setRequiredActions(RequiredAction.VERIFY_EMAIL)
|
||||||
|
.update()) {
|
||||||
|
// Browser 2: Refresh window
|
||||||
|
driver2.navigate().refresh();
|
||||||
|
assertThat(driver2.getCurrentUrl(), Matchers.startsWith(accountPage.buildUri().toString()));
|
||||||
|
|
||||||
|
// Browser 1: Logout
|
||||||
|
driver.navigate().to(oauth.getLogoutUrl().redirectUri(accountPage.buildUri().toString()).build());
|
||||||
|
|
||||||
|
// Browser 1: Go to account page
|
||||||
|
accountPage.navigateTo();
|
||||||
|
|
||||||
|
// Browser 1: Log in
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
|
||||||
|
verifyEmailPage.assertCurrent();
|
||||||
|
|
||||||
|
// Browser 2 [still logged in]: Click the email verification link
|
||||||
|
Assert.assertEquals(1, greenMail.getReceivedMessages().length);
|
||||||
|
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||||
|
|
||||||
|
String verificationUrl = getPasswordResetEmailLink(message);
|
||||||
|
|
||||||
|
driver2.navigate().to(verificationUrl.trim());
|
||||||
|
|
||||||
|
// Browser 2: Confirm email belongs to the user
|
||||||
|
final WebElement proceedLink = driver2.findElement(By.linkText("» Click here to proceed"));
|
||||||
|
assertThat(proceedLink, Matchers.notNullValue());
|
||||||
|
proceedLink.click();
|
||||||
|
|
||||||
|
// Browser 2: Expect confirmation
|
||||||
|
assertThat(driver2.getPageSource(), Matchers.containsString("kc-info-message"));
|
||||||
|
assertThat(driver2.getPageSource(), Matchers.containsString("Your email address has been verified."));
|
||||||
|
|
||||||
|
// Browser 1: Expect land back to account after refresh
|
||||||
|
driver.navigate().refresh();
|
||||||
|
accountPage.assertCurrent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -512,6 +512,7 @@ public abstract class AbstractClientInitiatedAccountLinkTest extends AbstractSer
|
||||||
.path(uri)
|
.path(uri)
|
||||||
.queryParam(OAuth2Constants.CODE, queryParams.get(OAuth2Constants.CODE))
|
.queryParam(OAuth2Constants.CODE, queryParams.get(OAuth2Constants.CODE))
|
||||||
.queryParam(Constants.CLIENT_ID, queryParams.get(Constants.CLIENT_ID))
|
.queryParam(Constants.CLIENT_ID, queryParams.get(Constants.CLIENT_ID))
|
||||||
|
.queryParam(Constants.TAB_ID, queryParams.get(Constants.TAB_ID))
|
||||||
.build().toString();
|
.build().toString();
|
||||||
|
|
||||||
System.out.println("hack uri: " + uri);
|
System.out.println("hack uri: " + uri);
|
||||||
|
|
|
@ -312,12 +312,8 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest {
|
||||||
loginPage.login("login-test", "password");
|
loginPage.login("login-test", "password");
|
||||||
updatePasswordPage.assertCurrent();
|
updatePasswordPage.assertCurrent();
|
||||||
|
|
||||||
// Click browser back. I should be on 'page expired' . URL corresponds to OIDC AuthorizationEndpoint
|
// Click browser back. I should be on login page . URL corresponds to OIDC AuthorizationEndpoint
|
||||||
driver.navigate().back();
|
driver.navigate().back();
|
||||||
loginExpiredPage.assertCurrent();
|
|
||||||
|
|
||||||
// Click 'restart' link. I should be on login page
|
|
||||||
loginExpiredPage.clickLoginRestartLink();
|
|
||||||
loginPage.assertCurrent();
|
loginPage.assertCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -326,6 +322,7 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest {
|
||||||
public void backButtonInResetPasswordFlow() throws Exception {
|
public void backButtonInResetPasswordFlow() throws Exception {
|
||||||
// Click on "forgot password" and type username
|
// Click on "forgot password" and type username
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
|
loginPage.login("login-test", "bad-username");
|
||||||
loginPage.resetPassword();
|
loginPage.resetPassword();
|
||||||
|
|
||||||
resetPasswordPage.assertCurrent();
|
resetPasswordPage.assertCurrent();
|
||||||
|
@ -344,22 +341,19 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
updatePasswordPage.assertCurrent();
|
updatePasswordPage.assertCurrent();
|
||||||
|
|
||||||
// Click browser back. Should be on 'page expired'
|
// Click browser back. Should be on loginPage for "forked flow"
|
||||||
driver.navigate().back();
|
driver.navigate().back();
|
||||||
loginExpiredPage.assertCurrent();
|
|
||||||
|
|
||||||
// Click 'continue' should be on updatePasswordPage
|
|
||||||
loginExpiredPage.clickLoginContinueLink();
|
|
||||||
updatePasswordPage.assertCurrent();
|
|
||||||
|
|
||||||
// Click browser back. Should be on 'page expired'
|
|
||||||
driver.navigate().back();
|
|
||||||
loginExpiredPage.assertCurrent();
|
|
||||||
|
|
||||||
// Click 'restart' . Should be on login page
|
|
||||||
loginExpiredPage.clickLoginRestartLink();
|
|
||||||
loginPage.assertCurrent();
|
loginPage.assertCurrent();
|
||||||
|
|
||||||
|
// When clicking browser forward, back on updatePasswordPage
|
||||||
|
driver.navigate().forward();
|
||||||
|
updatePasswordPage.assertCurrent();
|
||||||
|
|
||||||
|
// Click browser back. And continue login. Should be on updatePasswordPage
|
||||||
|
driver.navigate().back();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("login-test", "password");
|
||||||
|
updatePasswordPage.assertCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -116,22 +116,6 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
public AssertEvents events = new AssertEvents(this);
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
|
||||||
// Test for scenario when user is logged into JS application in 2 browser tabs. Then click "logout" in tab1 and he is logged-out from both tabs (tab2 is logged-out automatically due to session iframe few seconds later)
|
|
||||||
// Now both browser tabs show the 1st login screen and we need to make sure that actionURL (code with execution) is valid on both tabs, so user won't have error page when he tries to login from tab1
|
|
||||||
@Test
|
|
||||||
public void openMultipleTabs() {
|
|
||||||
oauth.openLoginForm();
|
|
||||||
loginPage.assertCurrent();
|
|
||||||
String actionUrl1 = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource());
|
|
||||||
|
|
||||||
oauth.openLoginForm();
|
|
||||||
loginPage.assertCurrent();
|
|
||||||
String actionUrl2 = ActionURIUtils.getActionURIFromPageSource(driver.getPageSource());
|
|
||||||
|
|
||||||
Assert.assertEquals(actionUrl1, actionUrl2);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void multipleTabsParallelLoginTest() {
|
public void multipleTabsParallelLoginTest() {
|
||||||
oauth.openLoginForm();
|
oauth.openLoginForm();
|
||||||
|
@ -314,4 +298,65 @@ public class MultipleTabsLoginTest extends AbstractTestRealmKeycloakTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// KEYCLOAK-5938
|
||||||
|
@Test
|
||||||
|
public void loginWithSameClientDifferentStatesLoginInTab1() throws Exception {
|
||||||
|
// Open tab1 and start login here
|
||||||
|
oauth.stateParamHardcoded("state1");
|
||||||
|
oauth.redirectUri("http://localhost:8180/auth/realms/master/app/auth/suffix1");
|
||||||
|
oauth.openLoginForm();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("login-test", "bad-password");
|
||||||
|
String tab1Url = driver.getCurrentUrl();
|
||||||
|
|
||||||
|
// Go to tab2 and start login with different client "root-url-client"
|
||||||
|
oauth.stateParamHardcoded("state2");
|
||||||
|
oauth.redirectUri("http://localhost:8180/auth/realms/master/app/auth/suffix2");
|
||||||
|
oauth.openLoginForm();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
String tab2Url = driver.getCurrentUrl();
|
||||||
|
|
||||||
|
// Go back to tab1 and finish login here
|
||||||
|
driver.navigate().to(tab1Url);
|
||||||
|
loginPage.login("login-test", "password");
|
||||||
|
updatePasswordPage.changePassword("password", "password");
|
||||||
|
updateProfilePage.update("John", "Doe3", "john@doe3.com");
|
||||||
|
|
||||||
|
// Assert I am redirected to the appPage in tab1 and have state corresponding to tab1
|
||||||
|
appPage.assertCurrent();
|
||||||
|
String currentUrl = driver.getCurrentUrl();
|
||||||
|
Assert.assertThat(currentUrl, Matchers.startsWith("http://localhost:8180/auth/realms/master/app/auth/suffix1"));
|
||||||
|
Assert.assertTrue(currentUrl.contains("state1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// KEYCLOAK-5938
|
||||||
|
@Test
|
||||||
|
public void loginWithSameClientDifferentStatesLoginInTab2() throws Exception {
|
||||||
|
// Open tab1 and start login here
|
||||||
|
oauth.stateParamHardcoded("state1");
|
||||||
|
oauth.redirectUri("http://localhost:8180/auth/realms/master/app/auth/suffix1");
|
||||||
|
oauth.openLoginForm();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("login-test", "bad-password");
|
||||||
|
String tab1Url = driver.getCurrentUrl();
|
||||||
|
|
||||||
|
// Go to tab2 and start login with different client "root-url-client"
|
||||||
|
oauth.stateParamHardcoded("state2");
|
||||||
|
oauth.redirectUri("http://localhost:8180/auth/realms/master/app/auth/suffix2");
|
||||||
|
oauth.openLoginForm();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
String tab2Url = driver.getCurrentUrl();
|
||||||
|
|
||||||
|
// Continue in tab2 and finish login here
|
||||||
|
loginPage.login("login-test", "password");
|
||||||
|
updatePasswordPage.changePassword("password", "password");
|
||||||
|
updateProfilePage.update("John", "Doe3", "john@doe3.com");
|
||||||
|
|
||||||
|
// Assert I am redirected to the appPage in tab2 and have state corresponding to tab2
|
||||||
|
appPage.assertCurrent();
|
||||||
|
String currentUrl = driver.getCurrentUrl();
|
||||||
|
Assert.assertThat(currentUrl, Matchers.startsWith("http://localhost:8180/auth/realms/master/app/auth/suffix2"));
|
||||||
|
Assert.assertTrue(currentUrl.contains("state2"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -467,4 +467,20 @@ public class X509BrowserLoginTest extends AbstractX509AuthenticationTest {
|
||||||
.removeDetail(Details.REDIRECT_URI)
|
.removeDetail(Details.REDIRECT_URI)
|
||||||
.assertEvent();
|
.assertEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// KEYCLOAK-5466
|
||||||
|
@Test
|
||||||
|
public void loginWithCertificateAddedLater() throws Exception {
|
||||||
|
// Start with normal login form
|
||||||
|
loginConfirmationPage.open();
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
|
||||||
|
Assert.assertThat(loginPage.getInfoMessage(), containsString("X509 client authentication has not been configured yet"));
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
|
||||||
|
// Now setup certificate and login with certificate in existing authenticationSession (Not 100% same scenario as KEYCLOAK-5466, but very similar)
|
||||||
|
loginAsUserFromCertSubjectEmail();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -434,6 +434,7 @@ public class AccountLinkSpringBootTest extends AbstractSpringBootTest {
|
||||||
.path(uri)
|
.path(uri)
|
||||||
.queryParam(OAuth2Constants.CODE, queryParams.get(OAuth2Constants.CODE))
|
.queryParam(OAuth2Constants.CODE, queryParams.get(OAuth2Constants.CODE))
|
||||||
.queryParam(Constants.CLIENT_ID, queryParams.get(Constants.CLIENT_ID))
|
.queryParam(Constants.CLIENT_ID, queryParams.get(Constants.CLIENT_ID))
|
||||||
|
.queryParam(Constants.TAB_ID, queryParams.get(Constants.TAB_ID))
|
||||||
.build().toString();
|
.build().toString();
|
||||||
|
|
||||||
log.info("hack uri: " + uri);
|
log.info("hack uri: " + uri);
|
||||||
|
|
|
@ -92,19 +92,7 @@ public class OIDCFirstBrokerLoginTest extends AbstractFirstBrokerLoginTest {
|
||||||
}, APP_REALM_ID);
|
}, APP_REALM_ID);
|
||||||
|
|
||||||
// First link "pedroigor" user with SAML broker and logout
|
// First link "pedroigor" user with SAML broker and logout
|
||||||
driver.navigate().to("http://localhost:8081/test-app");
|
linkUserWithSamlBroker("pedroigor", "psilva@redhat.com");
|
||||||
this.loginPage.clickSocial("kc-saml-idp-basic");
|
|
||||||
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/"));
|
|
||||||
Assert.assertEquals("Log in to realm-with-saml-idp-basic", this.driver.getTitle());
|
|
||||||
this.loginPage.login("pedroigor", "password");
|
|
||||||
|
|
||||||
this.idpConfirmLinkPage.assertCurrent();
|
|
||||||
Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
|
|
||||||
this.idpConfirmLinkPage.clickLinkAccount();
|
|
||||||
|
|
||||||
this.loginPage.login("password");
|
|
||||||
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app"));
|
|
||||||
driver.navigate().to("http://localhost:8081/test-app/logout");
|
|
||||||
|
|
||||||
|
|
||||||
// login through OIDC broker now
|
// login through OIDC broker now
|
||||||
|
@ -159,4 +147,84 @@ public class OIDCFirstBrokerLoginTest extends AbstractFirstBrokerLoginTest {
|
||||||
}, APP_REALM_ID);
|
}, APP_REALM_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// KEYCLOAK-5936
|
||||||
|
@Test
|
||||||
|
public void testMoreIdpAndBackButtonWhenLinkingAccount() throws Exception {
|
||||||
|
brokerServerRule.update(new KeycloakRule.KeycloakSetup() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) {
|
||||||
|
setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW,
|
||||||
|
IdpEmailVerificationAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.DISABLED);
|
||||||
|
|
||||||
|
//setUpdateProfileFirstLogin(realmWithBroker, IdentityProviderRepresentation.UPFLM_ON);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, APP_REALM_ID);
|
||||||
|
|
||||||
|
|
||||||
|
// First link user with SAML broker and logout
|
||||||
|
linkUserWithSamlBroker("pedroigor", "psilva@redhat.com");
|
||||||
|
|
||||||
|
// Try to login through OIDC broker now
|
||||||
|
loginIDP("pedroigor");
|
||||||
|
this.updateProfilePage.assertCurrent();
|
||||||
|
|
||||||
|
// User doesn't want to continue linking account. He rather wants to revert and try the other broker. Cick browser "back" 2 times now
|
||||||
|
driver.navigate().back();
|
||||||
|
loginExpiredPage.assertCurrent();
|
||||||
|
driver.navigate().back();
|
||||||
|
|
||||||
|
// I am back on the base login screen. Click login with SAML now and login with SAML broker instead
|
||||||
|
Assert.assertEquals("Log in to realm-with-broker", driver.getTitle());
|
||||||
|
this.loginPage.clickSocial("kc-saml-idp-basic");
|
||||||
|
|
||||||
|
// Login inside SAML broker
|
||||||
|
this.loginPage.login("pedroigor", "password");
|
||||||
|
|
||||||
|
// Assert logged successfully
|
||||||
|
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app"));
|
||||||
|
UserModel federatedUser = getFederatedUser();
|
||||||
|
assertNotNull(federatedUser);
|
||||||
|
assertEquals("pedroigor", federatedUser.getUsername());
|
||||||
|
|
||||||
|
// Logout
|
||||||
|
driver.navigate().to("http://localhost:8081/test-app/logout");
|
||||||
|
|
||||||
|
brokerServerRule.update(new KeycloakRule.KeycloakSetup() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel realmWithBroker) {
|
||||||
|
setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW,
|
||||||
|
IdpEmailVerificationAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.ALTERNATIVE);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}, APP_REALM_ID);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void linkUserWithSamlBroker(String username, String email) {
|
||||||
|
// First link "pedroigor" user with SAML broker and logout
|
||||||
|
driver.navigate().to("http://localhost:8081/test-app");
|
||||||
|
this.loginPage.clickSocial("kc-saml-idp-basic");
|
||||||
|
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/"));
|
||||||
|
Assert.assertEquals("Log in to realm-with-saml-idp-basic", this.driver.getTitle());
|
||||||
|
this.loginPage.login(username, "password");
|
||||||
|
|
||||||
|
if (updateProfilePage.isCurrent()) {
|
||||||
|
updateProfilePage.update("Pedro", "Igor", email);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.idpConfirmLinkPage.assertCurrent();
|
||||||
|
Assert.assertEquals("User with email " + email + " already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
|
||||||
|
this.idpConfirmLinkPage.clickLinkAccount();
|
||||||
|
|
||||||
|
this.loginPage.login("password");
|
||||||
|
assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app"));
|
||||||
|
driver.navigate().to("http://localhost:8081/test-app/logout");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,13 +87,19 @@ public class AuthenticationSessionProviderTest {
|
||||||
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(realm);
|
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(realm);
|
||||||
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client1);
|
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client1);
|
||||||
|
|
||||||
|
String tabId = authSession.getTabId();
|
||||||
|
|
||||||
authSession.setAction("foo");
|
authSession.setAction("foo");
|
||||||
rootAuthSession.setTimestamp(100);
|
rootAuthSession.setTimestamp(100);
|
||||||
|
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
|
client1 = realm.getClientByClientId("test-app");
|
||||||
|
|
||||||
// Ensure session is here
|
// Ensure session is here
|
||||||
rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId());
|
rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId());
|
||||||
|
authSession = rootAuthSession.getAuthenticationSession(client1, tabId);
|
||||||
testAuthenticationSession(authSession, client1.getId(), null, "foo");
|
testAuthenticationSession(authSession, client1.getId(), null, "foo");
|
||||||
Assert.assertEquals(100, rootAuthSession.getTimestamp());
|
Assert.assertEquals(100, rootAuthSession.getTimestamp());
|
||||||
|
|
||||||
|
@ -107,7 +113,7 @@ public class AuthenticationSessionProviderTest {
|
||||||
// Ensure session was updated
|
// Ensure session was updated
|
||||||
rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId());
|
rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId());
|
||||||
client1 = realm.getClientByClientId("test-app");
|
client1 = realm.getClientByClientId("test-app");
|
||||||
authSession = rootAuthSession.getAuthenticationSession(client1);
|
authSession = rootAuthSession.getAuthenticationSession(client1, tabId);
|
||||||
testAuthenticationSession(authSession, client1.getId(), user1.getId(), "foo-updated");
|
testAuthenticationSession(authSession, client1.getId(), user1.getId(), "foo-updated");
|
||||||
Assert.assertEquals(200, rootAuthSession.getTimestamp());
|
Assert.assertEquals(200, rootAuthSession.getTimestamp());
|
||||||
|
|
||||||
|
@ -127,6 +133,7 @@ public class AuthenticationSessionProviderTest {
|
||||||
UserModel user1 = session.users().getUserByUsername("user1", realm);
|
UserModel user1 = session.users().getUserByUsername("user1", realm);
|
||||||
|
|
||||||
AuthenticationSessionModel authSession = session.authenticationSessions().createRootAuthenticationSession(realm).createAuthenticationSession(client1);
|
AuthenticationSessionModel authSession = session.authenticationSessions().createRootAuthenticationSession(realm).createAuthenticationSession(client1);
|
||||||
|
String tabId = authSession.getTabId();
|
||||||
|
|
||||||
authSession.setAction("foo");
|
authSession.setAction("foo");
|
||||||
authSession.getParentSession().setTimestamp(100);
|
authSession.getParentSession().setTimestamp(100);
|
||||||
|
@ -141,13 +148,13 @@ public class AuthenticationSessionProviderTest {
|
||||||
// Test restart root authentication session
|
// Test restart root authentication session
|
||||||
client1 = realm.getClientByClientId("test-app");
|
client1 = realm.getClientByClientId("test-app");
|
||||||
authSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSession.getParentSession().getId())
|
authSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSession.getParentSession().getId())
|
||||||
.getAuthenticationSession(client1);
|
.getAuthenticationSession(client1, tabId);
|
||||||
authSession.getParentSession().restartSession(realm);
|
authSession.getParentSession().restartSession(realm);
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSession.getParentSession().getId());
|
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSession.getParentSession().getId());
|
||||||
Assert.assertNull(rootAuthSession.getAuthenticationSession(client1));
|
Assert.assertNull(rootAuthSession.getAuthenticationSession(client1, tabId));
|
||||||
Assert.assertTrue(rootAuthSession.getTimestamp() > 0);
|
Assert.assertTrue(rootAuthSession.getTimestamp() > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,26 +255,26 @@ public class AuthenticationSessionProviderTest {
|
||||||
String authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId();
|
String authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId();
|
||||||
AuthenticationSessionModel authSession1 = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId).createAuthenticationSession(realm.getClientByClientId("test-app"));
|
AuthenticationSessionModel authSession1 = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId).createAuthenticationSession(realm.getClientByClientId("test-app"));
|
||||||
AuthenticationSessionModel authSession2 = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId).createAuthenticationSession(realm.getClientByClientId("third-party"));
|
AuthenticationSessionModel authSession2 = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId).createAuthenticationSession(realm.getClientByClientId("third-party"));
|
||||||
|
String tab1Id = authSession1.getTabId();
|
||||||
|
String tab2Id = authSession2.getTabId();
|
||||||
|
|
||||||
authSession1.setAuthNote("foo", "bar");
|
authSession1.setAuthNote("foo", "bar");
|
||||||
authSession2.setAuthNote("foo", "baz");
|
authSession2.setAuthNote("foo", "baz");
|
||||||
|
|
||||||
String testAppClientUUID = realm.getClientByClientId("test-app").getId();
|
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||||
Assert.assertEquals(2, rootAuthSession.getAuthenticationSessions().size());
|
Assert.assertEquals(2, rootAuthSession.getAuthenticationSessions().size());
|
||||||
Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app")).getAuthNote("foo"));
|
Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app"), tab1Id).getAuthNote("foo"));
|
||||||
Assert.assertEquals("baz", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("third-party")).getAuthNote("foo"));
|
Assert.assertEquals("baz", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("third-party"), tab2Id).getAuthNote("foo"));
|
||||||
|
|
||||||
new ClientManager(new RealmManager(session)).removeClient(realm, realm.getClientByClientId("third-party"));
|
new ClientManager(new RealmManager(session)).removeClient(realm, realm.getClientByClientId("third-party"));
|
||||||
|
|
||||||
resetSession();
|
resetSession();
|
||||||
|
|
||||||
rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||||
Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app")).getAuthNote("foo"));
|
Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app"), tab1Id).getAuthNote("foo"));
|
||||||
Assert.assertNull(rootAuthSession.getAuthenticationSession(realm.getClientByClientId("third-party")));
|
Assert.assertNull(rootAuthSession.getAuthenticationSession(realm.getClientByClientId("third-party"), tab2Id));
|
||||||
|
|
||||||
// Revert client
|
// Revert client
|
||||||
realm.addClient("third-party");
|
realm.addClient("third-party");
|
||||||
|
|
Loading…
Reference in a new issue