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 {
|
||||
|
||||
private String authSessionId;
|
||||
|
||||
private String tabId;
|
||||
private String clientUUID;
|
||||
|
||||
private Map<String, String> authNotesFragment;
|
||||
|
@ -46,9 +46,10 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
|||
* @param authNotesFragment
|
||||
* @return Event. Note that {@code authNotesFragment} property is not thread safe which is fine for now.
|
||||
*/
|
||||
public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, String clientUUID, Map<String, String> authNotesFragment) {
|
||||
public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, String tabId, String clientUUID, Map<String, String> authNotesFragment) {
|
||||
AuthenticationSessionAuthNoteUpdateEvent event = new AuthenticationSessionAuthNoteUpdateEvent();
|
||||
event.authSessionId = authSessionId;
|
||||
event.tabId = tabId;
|
||||
event.clientUUID = clientUUID;
|
||||
event.authNotesFragment = new LinkedHashMap<>(authNotesFragment);
|
||||
return event;
|
||||
|
@ -58,6 +59,10 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
|||
return authSessionId;
|
||||
}
|
||||
|
||||
public String getTabId() {
|
||||
return tabId;
|
||||
}
|
||||
|
||||
public String getClientUUID() {
|
||||
return clientUUID;
|
||||
}
|
||||
|
@ -68,7 +73,7 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
|||
|
||||
@Override
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -81,6 +86,7 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
|||
output.writeByte(VERSION_1);
|
||||
|
||||
MarshallUtil.marshallString(value.authSessionId, output);
|
||||
MarshallUtil.marshallString(value.tabId, output);
|
||||
MarshallUtil.marshallString(value.clientUUID, output);
|
||||
MarshallUtil.marshallMap(value.authNotesFragment, output);
|
||||
}
|
||||
|
@ -97,6 +103,7 @@ public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
|
|||
|
||||
public AuthenticationSessionAuthNoteUpdateEvent readObjectVersion1(ObjectInput input) throws IOException, ClassNotFoundException {
|
||||
return create(
|
||||
MarshallUtil.unmarshallString(input),
|
||||
MarshallUtil.unmarshallString(input),
|
||||
MarshallUtil.unmarshallString(input),
|
||||
MarshallUtil.unmarshallMap(input, HashMap::new)
|
||||
|
|
|
@ -40,13 +40,13 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
|||
|
||||
private final KeycloakSession session;
|
||||
private final RootAuthenticationSessionAdapter parent;
|
||||
private final String clientUUID;
|
||||
private final String tabId;
|
||||
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.parent = parent;
|
||||
this.clientUUID = clientUUID;
|
||||
this.tabId = tabId;
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,10 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
|||
parent.update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTabId() {
|
||||
return tabId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RootAuthenticationSessionModel getParentSession() {
|
||||
|
@ -68,7 +72,7 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
|||
|
||||
@Override
|
||||
public ClientModel getClient() {
|
||||
return getRealm().getClientById(clientUUID);
|
||||
return getRealm().getClientById(entity.getClientUUID());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -23,6 +23,7 @@ import java.util.Map;
|
|||
|
||||
import org.infinispan.Cache;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.ClientModel;
|
||||
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.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.RealmInfoUtil;
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionProvider;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
|
||||
|
@ -162,15 +164,15 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
|||
|
||||
|
||||
@Override
|
||||
public void updateNonlocalSessionAuthNotes(String authSessionId, ClientModel client, Map<String, String> authNotesFragment) {
|
||||
if (authSessionId == null) {
|
||||
public void updateNonlocalSessionAuthNotes(AuthenticationSessionCompoundId compoundId, Map<String, String> authNotesFragment) {
|
||||
if (compoundId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ClusterProvider cluster = session.getProvider(ClusterProvider.class);
|
||||
cluster.notify(
|
||||
InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS,
|
||||
AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, client.getId(), authNotesFragment),
|
||||
AuthenticationSessionAuthNoteUpdateEvent.create(compoundId.getRootSessionId(), compoundId.getTabId(), compoundId.getClientUUID(), authNotesFragment),
|
||||
true,
|
||||
ClusterProvider.DCNotify.ALL_BUT_LOCAL_DC
|
||||
);
|
||||
|
@ -197,4 +199,9 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
|
|||
public Cache<String, RootAuthenticationSessionEntity> getCache() {
|
||||
return cache;
|
||||
}
|
||||
|
||||
|
||||
protected String generateTabId() {
|
||||
return Base64Url.encode(KeycloakModelUtils.generateSecret(8));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,16 +118,16 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
|
|||
|
||||
AuthenticationSessionAuthNoteUpdateEvent event = (AuthenticationSessionAuthNoteUpdateEvent) clEvent;
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
AuthenticationSessionEntity authSession = rootAuthSession.getAuthenticationSessions().get(clientUUID);
|
||||
AuthenticationSessionEntity authSession = rootAuthSession.getAuthenticationSessions().get(tabId);
|
||||
|
||||
if (authSession != null) {
|
||||
if (authSession.getAuthNotes() == null) {
|
||||
|
|
|
@ -21,12 +21,14 @@ import java.util.HashMap;
|
|||
import java.util.Map;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.keycloak.common.util.Base64Url;
|
||||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.RootAuthenticationSessionEntity;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
|
||||
|
@ -82,25 +84,41 @@ public class RootAuthenticationSessionAdapter implements RootAuthenticationSessi
|
|||
Map<String, AuthenticationSessionModel> result = new HashMap<>();
|
||||
|
||||
for (Map.Entry<String, AuthenticationSessionEntity> entry : entity.getAuthenticationSessions().entrySet()) {
|
||||
String clientUUID = entry.getKey();
|
||||
result.put(clientUUID , new AuthenticationSessionAdapter(session, this, clientUUID, entry.getValue()));
|
||||
String tabId = entry.getKey();
|
||||
result.put(tabId , new AuthenticationSessionAdapter(session, this, tabId, entry.getValue()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationSessionModel getAuthenticationSession(ClientModel client) {
|
||||
return client==null ? null : getAuthenticationSessions().get(client.getId());
|
||||
public AuthenticationSessionModel getAuthenticationSession(ClientModel client, String tabId) {
|
||||
if (client == null || tabId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
AuthenticationSessionModel authSession = getAuthenticationSessions().get(tabId);
|
||||
if (authSession != null && client.equals(authSession.getClient())) {
|
||||
return authSession;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticationSessionModel createAuthenticationSession(ClientModel client) {
|
||||
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();
|
||||
|
||||
return new AuthenticationSessionAdapter(session, this, client.getId(), authSessionEntity);
|
||||
return new AuthenticationSessionAdapter(session, this, tabId, authSessionEntity);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -30,6 +30,8 @@ import org.keycloak.sessions.AuthenticationSessionModel;
|
|||
*/
|
||||
public class AuthenticationSessionEntity implements Serializable {
|
||||
|
||||
private String clientUUID;
|
||||
|
||||
private String authUserId;
|
||||
|
||||
private String redirectUri;
|
||||
|
@ -45,6 +47,14 @@ public class AuthenticationSessionEntity implements Serializable {
|
|||
private Set<String> requiredActions = new ConcurrentHashSet<>();
|
||||
private Map<String, String> userSessionNotes;
|
||||
|
||||
public String getClientUUID() {
|
||||
return clientUUID;
|
||||
}
|
||||
|
||||
public void setClientUUID(String clientUUID) {
|
||||
this.clientUUID = clientUUID;
|
||||
}
|
||||
|
||||
public String getAuthUserId() {
|
||||
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
|
||||
*
|
||||
* Not Thread-safe
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class IdentityBrokerState {
|
||||
|
||||
private String decodedState;
|
||||
private String clientId;
|
||||
private String encodedState;
|
||||
private static final Pattern DOT = Pattern.compile("\\.");
|
||||
|
||||
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) {
|
||||
IdentityBrokerState state = new IdentityBrokerState();
|
||||
state.encodedState = encodedState;
|
||||
return state;
|
||||
String[] decoded = DOT.split(encodedState, 3);
|
||||
|
||||
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() {
|
||||
if (decodedState == null) {
|
||||
decode();
|
||||
}
|
||||
return decodedState;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
if (decodedState == null) {
|
||||
decode();
|
||||
}
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public String getEncodedState() {
|
||||
if (encodedState == null) {
|
||||
encode();
|
||||
}
|
||||
return encodedState;
|
||||
public String getTabId() {
|
||||
return tabId;
|
||||
}
|
||||
|
||||
|
||||
private void decode() {
|
||||
String[] decoded = DOT.split(encodedState, 0);
|
||||
decodedState = decoded[0];
|
||||
if (decoded.length > 0) {
|
||||
clientId = decoded[1];
|
||||
}
|
||||
public String getEncoded() {
|
||||
return encoded;
|
||||
}
|
||||
|
||||
|
||||
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 EXECUTION = "execution";
|
||||
String CLIENT_ID = "client_id";
|
||||
String TAB_ID = "tab_id";
|
||||
String KEY = "key";
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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>
|
||||
*/
|
||||
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();
|
||||
|
||||
|
|
|
@ -47,10 +47,10 @@ public interface AuthenticationSessionProvider extends Provider {
|
|||
* Requests update of authNotes of a root authentication session that is not owned
|
||||
* by this instance but might exist somewhere in the cluster.
|
||||
*
|
||||
* @param authSessionId
|
||||
* @param compoundId
|
||||
* @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.
|
||||
*/
|
||||
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(Constants.EXECUTION, getExecution().getId())
|
||||
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
||||
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
|
||||
.build(getRealm().getName());
|
||||
}
|
||||
|
||||
|
@ -500,6 +501,7 @@ public class AuthenticationProcessor {
|
|||
.queryParam(Constants.KEY, tokenString)
|
||||
.queryParam(Constants.EXECUTION, getExecution().getId())
|
||||
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
||||
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
|
||||
.build(getRealm().getName());
|
||||
}
|
||||
|
||||
|
@ -509,6 +511,7 @@ public class AuthenticationProcessor {
|
|||
.path(AuthenticationProcessor.this.flowPath)
|
||||
.queryParam(Constants.EXECUTION, getExecution().getId())
|
||||
.queryParam(Constants.CLIENT_ID, getAuthenticationSession().getClient().getClientId())
|
||||
.queryParam(Constants.TAB_ID, getAuthenticationSession().getTabId())
|
||||
.build(getRealm().getName());
|
||||
}
|
||||
|
||||
|
@ -634,11 +637,11 @@ public class AuthenticationProcessor {
|
|||
} else if (e.getError() == AuthenticationFlowError.FORK_FLOW) {
|
||||
ForkFlowException reset = (ForkFlowException)e;
|
||||
|
||||
RootAuthenticationSessionModel rootClone = clone(session, authenticationSession.getClient(), authenticationSession.getParentSession());
|
||||
AuthenticationSessionModel clone = rootClone.getAuthenticationSession(authenticationSession.getClient());
|
||||
AuthenticationSessionModel clone = clone(session, authenticationSession);
|
||||
|
||||
clone.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||
setAuthenticationSession(clone);
|
||||
session.getProvider(LoginFormsProvider.class).setAuthenticationSession(clone);
|
||||
|
||||
AuthenticationProcessor processor = new AuthenticationProcessor();
|
||||
processor.setAuthenticationSession(clone)
|
||||
|
@ -763,29 +766,24 @@ public class AuthenticationProcessor {
|
|||
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"
|
||||
for (Map.Entry<String, AuthenticationSessionModel> entry : authSession.getAuthenticationSessions().entrySet()) {
|
||||
AuthenticationSessionModel asmOrig = entry.getValue();
|
||||
AuthenticationSessionModel asmClone = clone.createAuthenticationSession(asmOrig.getClient());
|
||||
// Clone new authentication session from the given authSession. New authenticationSession will have same parent (rootSession) and will use same client
|
||||
public static AuthenticationSessionModel clone(KeycloakSession session, AuthenticationSessionModel authSession) {
|
||||
AuthenticationSessionModel clone = authSession.getParentSession().createAuthenticationSession(authSession.getClient());
|
||||
|
||||
asmClone.setRedirectUri(asmOrig.getRedirectUri());
|
||||
asmClone.setProtocol(asmOrig.getProtocol());
|
||||
clone.setRedirectUri(authSession.getRedirectUri());
|
||||
clone.setProtocol(authSession.getProtocol());
|
||||
|
||||
for (Map.Entry<String, String> clientNote : asmOrig.getClientNotes().entrySet()) {
|
||||
asmClone.setClientNote(clientNote.getKey(), clientNote.getValue());
|
||||
}
|
||||
for (Map.Entry<String, String> clientNote : authSession.getClientNotes().entrySet()) {
|
||||
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'", clone.getId(), authSession.getId(), client.getClientId());
|
||||
logger.debugf("Forked authSession %s from authSession %s . Client: %s, Root session: %s",
|
||||
clone.getTabId(), authSession.getTabId(), authSession.getClient().getClientId(), authSession.getParentSession().getId());
|
||||
|
||||
return clone;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -269,6 +269,7 @@ public class FormAuthenticationFlow implements AuthenticationFlow {
|
|||
.queryParam(OAuth2Constants.CODE, code)
|
||||
.queryParam(Constants.EXECUTION, executionId)
|
||||
.queryParam(Constants.CLIENT_ID, client.getClientId())
|
||||
.queryParam(Constants.TAB_ID, processor.getAuthenticationSession().getTabId())
|
||||
.build(processor.getRealm().getName());
|
||||
}
|
||||
|
||||
|
|
|
@ -139,6 +139,7 @@ public class RequiredActionContextResult implements RequiredActionContext {
|
|||
.queryParam(OAuth2Constants.CODE, code)
|
||||
.queryParam(Constants.EXECUTION, getExecution())
|
||||
.queryParam(Constants.CLIENT_ID, client.getClientId())
|
||||
.queryParam(Constants.TAB_ID, authenticationSession.getTabId())
|
||||
.build(getRealm().getName());
|
||||
}
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@ public abstract class AbstractActionTokenHander<T extends JsonWebToken> implemen
|
|||
|
||||
@Override
|
||||
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
|
||||
|
|
|
@ -45,7 +45,7 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
|||
|
||||
@FunctionalInterface
|
||||
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;
|
||||
|
@ -161,6 +161,6 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
|||
|
||||
public Response brokerFlow(String code, String flowPath) {
|
||||
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 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);
|
||||
setAuthenticationSessionId(authenticationSessionId);
|
||||
setCompoundAuthenticationSessionId(compoundAuthenticationSessionId);
|
||||
}
|
||||
|
||||
@JsonProperty(value = JSON_FIELD_AUTHENTICATION_SESSION_ID)
|
||||
public String getAuthenticationSessionId() {
|
||||
public String getCompoundAuthenticationSessionId() {
|
||||
return (String) getOtherClaims().get(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);
|
||||
}
|
||||
|
||||
|
@ -91,8 +91,8 @@ public class DefaultActionToken extends DefaultActionTokenKey implements ActionT
|
|||
@Override
|
||||
public Map<String, String> getNotes() {
|
||||
Map<String, String> res = new HashMap<>();
|
||||
if (getAuthenticationSessionId() != null) {
|
||||
res.put(JSON_FIELD_AUTHENTICATION_SESSION_ID, getAuthenticationSessionId());
|
||||
if (getCompoundAuthenticationSessionId() != null) {
|
||||
res.put(JSON_FIELD_AUTHENTICATION_SESSION_ID, getCompoundAuthenticationSessionId());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -24,13 +24,12 @@ import org.keycloak.events.Errors;
|
|||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.models.UserModel.RequiredAction;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import java.util.Objects;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
@ -75,8 +74,10 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander<
|
|||
final KeycloakSession session = tokenContext.getSession();
|
||||
if (tokenContext.isAuthenticationSessionFresh()) {
|
||||
// Update the authentication session in the token
|
||||
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();
|
||||
|
||||
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_ALIAS = "idpa";
|
||||
private static final String JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID = "oasid";
|
||||
private static final String JSON_FIELD_ORIGINAL_CLIENT_UUID = "ocid";
|
||||
|
||||
@JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME)
|
||||
private String identityProviderUsername;
|
||||
|
@ -42,13 +41,10 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
|
|||
@JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
|
||||
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) {
|
||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
|
||||
this.originalClientUUID = clientUUID;
|
||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
|
||||
this.identityProviderUsername = identityProviderUsername;
|
||||
this.identityProviderAlias = identityProviderAlias;
|
||||
}
|
||||
|
@ -72,19 +68,12 @@ public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
|
|||
this.identityProviderAlias = identityProviderAlias;
|
||||
}
|
||||
|
||||
public String getOriginalAuthenticationSessionId() {
|
||||
public String getOriginalCompoundAuthenticationSessionId() {
|
||||
return originalAuthenticationSessionId;
|
||||
}
|
||||
|
||||
public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) {
|
||||
this.originalAuthenticationSessionId = originalAuthenticationSessionId;
|
||||
public void setOriginalCompoundAuthenticationSessionId(String originalCompoundAuthenticationSessionId) {
|
||||
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.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
import java.util.Collections;
|
||||
|
@ -76,9 +77,12 @@ public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenH
|
|||
|
||||
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
|
||||
if (tokenContext.isAuthenticationSessionFresh()) {
|
||||
token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
|
||||
token.setAuthenticationSessionId(authSession.getParentSession().getId());
|
||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
|
||||
token.setOriginalCompoundAuthenticationSessionId(token.getCompoundAuthenticationSessionId());
|
||||
|
||||
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();
|
||||
|
||||
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.
|
||||
user.setEmailVerified(true);
|
||||
|
||||
if (token.getOriginalAuthenticationSessionId() != null) {
|
||||
if (token.getOriginalCompoundAuthenticationSessionId() != null) {
|
||||
AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
||||
asm.removeAuthenticationSession(realm, authSession, true);
|
||||
|
||||
ClientModel originalClient = realm.getClientById(token.getOriginalClientUUID());
|
||||
authSession = asm.getAuthenticationSessionByIdAndClient(realm, token.getOriginalAuthenticationSessionId(), originalClient);
|
||||
AuthenticationSessionCompoundId compoundId = AuthenticationSessionCompoundId.encoded(token.getOriginalCompoundAuthenticationSessionId());
|
||||
ClientModel originalClient = realm.getClientById(compoundId.getClientUUID());
|
||||
authSession = asm.getAuthenticationSessionByIdAndClient(realm, compoundId.getRootSessionId(), originalClient, compoundId.getTabId());
|
||||
|
||||
if (authSession != null) {
|
||||
authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
|
||||
} else {
|
||||
|
||||
session.authenticationSessions().updateNonlocalSessionAuthNotes(
|
||||
token.getAuthenticationSessionId(),
|
||||
originalClient,
|
||||
compoundId,
|
||||
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 ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId) {
|
||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
|
||||
public ResetCredentialsActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId) {
|
||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
|
||||
}
|
||||
|
||||
private ResetCredentialsActionToken() {
|
||||
|
|
|
@ -37,8 +37,8 @@ public class VerifyEmailActionToken extends DefaultActionToken {
|
|||
@JsonProperty(value = JSON_FIELD_ORIGINAL_AUTHENTICATION_SESSION_ID)
|
||||
private String originalAuthenticationSessionId;
|
||||
|
||||
public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, String authenticationSessionId, String email) {
|
||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, authenticationSessionId);
|
||||
public VerifyEmailActionToken(String userId, int absoluteExpirationInSecs, String compoundAuthenticationSessionId, String email) {
|
||||
super(userId, TOKEN_TYPE, absoluteExpirationInSecs, null, compoundAuthenticationSessionId);
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
|
@ -53,11 +53,11 @@ public class VerifyEmailActionToken extends DefaultActionToken {
|
|||
this.email = email;
|
||||
}
|
||||
|
||||
public String getOriginalAuthenticationSessionId() {
|
||||
public String getCompoundOriginalAuthenticationSessionId() {
|
||||
return originalAuthenticationSessionId;
|
||||
}
|
||||
|
||||
public void setOriginalAuthenticationSessionId(String originalAuthenticationSessionId) {
|
||||
public void setCompoundOriginalAuthenticationSessionId(String originalAuthenticationSessionId) {
|
||||
this.originalAuthenticationSessionId = originalAuthenticationSessionId;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.keycloak.services.Urls;
|
|||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import java.util.Objects;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
@ -76,9 +77,12 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
|
|||
|
||||
if (tokenContext.isAuthenticationSessionFresh()) {
|
||||
// Update the authentication session in the token
|
||||
token.setOriginalAuthenticationSessionId(token.getAuthenticationSessionId());
|
||||
token.setAuthenticationSessionId(authSession.getParentSession().getId());
|
||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
|
||||
token.setCompoundOriginalAuthenticationSessionId(token.getCompoundAuthenticationSessionId());
|
||||
|
||||
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();
|
||||
|
||||
return session.getProvider(LoginFormsProvider.class)
|
||||
|
@ -95,7 +99,7 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
|
|||
|
||||
event.success();
|
||||
|
||||
if (token.getOriginalAuthenticationSessionId() != null) {
|
||||
if (token.getCompoundOriginalAuthenticationSessionId() != null) {
|
||||
AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession());
|
||||
asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true);
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ import org.keycloak.services.ServicesLogger;
|
|||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
import java.net.URI;
|
||||
|
@ -127,11 +128,13 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
|
|||
.removeDetail(Details.AUTH_METHOD)
|
||||
.removeDetail(Details.AUTH_TYPE);
|
||||
|
||||
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
||||
IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
|
||||
existingUser.getId(), absoluteExpirationInSecs, authSession.getParentSession().getId(), authSession.getClient().getId(),
|
||||
existingUser.getId(), absoluteExpirationInSecs, authSessionEncodedId,
|
||||
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
|
||||
.queryParam(Constants.EXECUTION, context.getExecution().getId())
|
||||
.build(realm.getName()).toString();
|
||||
|
|
|
@ -65,8 +65,9 @@ public class IdentityProviderAuthenticator implements Authenticator {
|
|||
if (identityProvider.isEnabled() && providerId.equals(identityProvider.getAlias())) {
|
||||
String accessCode = new ClientSessionCode<>(context.getSession(), context.getRealm(), context.getAuthenticationSession()).getOrGenerateCode();
|
||||
String clientId = context.getAuthenticationSession().getClient().getClientId();
|
||||
String tabId = context.getAuthenticationSession().getTabId();
|
||||
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();
|
||||
|
||||
LOG.debugf("Redirecting to %s", providerId);
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.keycloak.models.utils.FormMessage;
|
|||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
import java.util.*;
|
||||
|
@ -89,7 +90,8 @@ public class ResetCredentialEmail implements Authenticator, AuthenticatorFactory
|
|||
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
||||
|
||||
// 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
|
||||
.fromUri(context.getActionTokenUrl(token.serialize(context.getSession(), context.getRealm(), context.getUriInfo())))
|
||||
.build()
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.keycloak.models.*;
|
|||
import org.keycloak.services.Urls;
|
||||
import org.keycloak.services.validation.Validation;
|
||||
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -134,8 +135,10 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
|
|||
int validityInSecs = realm.getActionTokenGeneratedByUserLifespan(VerifyEmailActionToken.TOKEN_TYPE);
|
||||
int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
|
||||
|
||||
VerifyEmailActionToken token = new VerifyEmailActionToken(user.getId(), absoluteExpirationInSecs, authSession.getParentSession().getId(), user.getEmail());
|
||||
UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo), authSession.getClient().getClientId());
|
||||
String authSessionEncodedId = AuthenticationSessionCompoundId.fromAuthSession(authSession).getEncodedId();
|
||||
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();
|
||||
long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
|
||||
|
||||
|
|
|
@ -297,7 +297,7 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
|||
protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
|
||||
final UriBuilder uriBuilder = UriBuilder.fromUri(getConfig().getAuthorizationUrl())
|
||||
.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_CLIENT_ID, getConfig().getClientId())
|
||||
.queryParam(OAUTH2_PARAMETER_REDIRECT_URI, request.getRedirectUri());
|
||||
|
|
|
@ -101,7 +101,7 @@ public class SAMLIdentityProvider extends AbstractIdentityProvider<SAMLIdentityP
|
|||
.protocolBinding(protocolBinding)
|
||||
.nameIdPolicy(SAML2NameIDPolicyBuilder.format(nameIDPolicyFormat));
|
||||
JaxrsSAML2BindingBuilder binding = new JaxrsSAML2BindingBuilder()
|
||||
.relayState(request.getState().getEncodedState());
|
||||
.relayState(request.getState().getEncoded());
|
||||
boolean postBinding = getConfig().isPostBindingAuthnRequest();
|
||||
|
||||
if (getConfig().isWantAuthnRequestsSigned()) {
|
||||
|
|
|
@ -248,6 +248,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
|||
if (client != null) {
|
||||
uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId());
|
||||
}
|
||||
if (authenticationSession != null) {
|
||||
uriBuilder.queryParam(Constants.TAB_ID, authenticationSession.getTabId());
|
||||
}
|
||||
return uriBuilder;
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.keycloak.common.ClientConnection;
|
|||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.forms.login.LoginFormsProvider;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
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);
|
||||
String authSessionId = manager.getCurrentAuthenticationSessionId(realm);
|
||||
RootAuthenticationSessionModel rootAuthSession = authSessionId==null ? null : session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||
|
@ -174,128 +175,32 @@ public abstract class AuthorizationEndpointBase {
|
|||
|
||||
if (rootAuthSession != null) {
|
||||
|
||||
authSession = rootAuthSession.getAuthenticationSession(client);
|
||||
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||
|
||||
if (authSession != null) {
|
||||
ClientSessionCode<AuthenticationSessionModel> check = new ClientSessionCode<>(session, realm, authSession);
|
||||
if (!check.isActionActive(ClientSessionCode.ActionType.LOGIN)) {
|
||||
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",
|
||||
rootAuthSession.getId(), client.getClientId(), authSession.getTabId());
|
||||
|
||||
logger.debugf("Authentication session '%s' exists, but is expired. Restart existing authentication session", rootAuthSession.getId());
|
||||
rootAuthSession.restartSession(realm);
|
||||
authSession = rootAuthSession.createAuthenticationSession(client);
|
||||
} else {
|
||||
|
||||
return new AuthorizationEndpointChecks(authSession);
|
||||
|
||||
} 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());
|
||||
UserSessionModel userSession = authSessionId == null ? null : new UserSessionCrossDCManager(session).getUserSessionIfExistsRemotely(realm, authSessionId);
|
||||
|
||||
if (userSession != null) {
|
||||
rootAuthSession = session.authenticationSessions().createRootAuthenticationSession(authSessionId, realm);
|
||||
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) {
|
||||
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 authSession;
|
||||
|
||||
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();
|
||||
|
||||
final AuthorizationEndpointRequest authRequest = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params);
|
||||
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, authRequest.getState());
|
||||
if (checks.response != null) {
|
||||
return checks.response;
|
||||
}
|
||||
authenticationSession = createAuthenticationSession(client, authRequest.getState());
|
||||
|
||||
authenticationSession = checks.authSession;
|
||||
updateAuthenticationSession();
|
||||
|
||||
// So back button doesn't work
|
||||
|
@ -96,8 +92,4 @@ public class DockerEndpoint extends AuthorizationEndpointBase {
|
|||
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.Response;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
@ -126,12 +125,7 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
|
|||
return errorResponse;
|
||||
}
|
||||
|
||||
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, request.getState());
|
||||
if (checks.response != null) {
|
||||
return checks.response;
|
||||
}
|
||||
|
||||
authenticationSession = checks.authSession;
|
||||
authenticationSession = createAuthenticationSession(client, request.getState());
|
||||
updateAuthenticationSession();
|
||||
|
||||
// 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() {
|
||||
authenticationSession.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
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);
|
||||
}
|
||||
|
||||
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()) {
|
||||
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);
|
||||
}
|
||||
|
||||
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, relayState);
|
||||
if (checks.response != null) {
|
||||
return checks.response;
|
||||
}
|
||||
|
||||
AuthenticationSessionModel authSession = checks.authSession;
|
||||
AuthenticationSessionModel authSession = createAuthenticationSession(client, relayState);
|
||||
|
||||
authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
|
||||
authSession.setRedirectUri(redirect);
|
||||
|
@ -673,12 +668,8 @@ public class SamlService extends AuthorizationEndpointBase {
|
|||
redirect = client.getManagementUrl();
|
||||
}
|
||||
|
||||
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, null);
|
||||
if (checks.response != null) {
|
||||
throw new IllegalStateException("Not expected to detect re-sent request for IDP initiated SSO");
|
||||
}
|
||||
AuthenticationSessionModel authSession = createAuthenticationSession(client, null);
|
||||
|
||||
AuthenticationSessionModel authSession = checks.authSession;
|
||||
authSession.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
|
||||
authSession.setAction(AuthenticationSessionModel.Action.AUTHENTICATE.name());
|
||||
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
|
||||
@NoCache
|
||||
@Consumes({"application/soap+xml",MediaType.TEXT_XML})
|
||||
|
|
|
@ -74,7 +74,7 @@ public class Urls {
|
|||
.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")
|
||||
.path(IdentityBrokerService.class, "performLogin");
|
||||
|
||||
|
@ -84,6 +84,9 @@ public class Urls {
|
|||
if (clientId != null) {
|
||||
uriBuilder.replaceQueryParam(Constants.CLIENT_ID, clientId);
|
||||
}
|
||||
if (tabId != null) {
|
||||
uriBuilder.replaceQueryParam(Constants.TAB_ID, tabId);
|
||||
}
|
||||
|
||||
return uriBuilder.build(realmName, providerId);
|
||||
}
|
||||
|
@ -103,22 +106,24 @@ public class Urls {
|
|||
}
|
||||
|
||||
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")
|
||||
.path(IdentityBrokerService.class, "afterFirstBrokerLogin")
|
||||
.replaceQueryParam(OAuth2Constants.CODE, accessCode)
|
||||
.replaceQueryParam(Constants.CLIENT_ID, clientId)
|
||||
.replaceQueryParam(Constants.TAB_ID, tabId)
|
||||
.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")
|
||||
.path(IdentityBrokerService.class, "afterPostBrokerLoginFlow")
|
||||
.replaceQueryParam(OAuth2Constants.CODE, accessCode)
|
||||
.replaceQueryParam(Constants.CLIENT_ID, clientId)
|
||||
.replaceQueryParam(Constants.TAB_ID, tabId)
|
||||
.build(realmName);
|
||||
}
|
||||
|
||||
|
@ -182,10 +187,11 @@ public class Urls {
|
|||
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")
|
||||
.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 realm
|
||||
|
@ -181,6 +180,8 @@ public class AuthenticationManager {
|
|||
* @param uriInfo
|
||||
* @param connection
|
||||
* @param headers
|
||||
* @param logoutBroker
|
||||
* @param offlineSession
|
||||
*/
|
||||
public static void backchannelLogout(KeycloakSession session, RealmModel realm,
|
||||
UserSessionModel userSession, UriInfo uriInfo,
|
||||
|
@ -197,7 +198,7 @@ public class AuthenticationManager {
|
|||
expireUserSessionCookie(session, userSession, realm, uriInfo, headers, connection);
|
||||
|
||||
final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
||||
AuthenticationSessionModel logoutAuthSession = createOrJoinLogoutSession(realm, asm, false);
|
||||
AuthenticationSessionModel logoutAuthSession = createOrJoinLogoutSession(session, realm, asm, userSession, false);
|
||||
|
||||
try {
|
||||
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
|
||||
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
||||
|
||||
AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm, client);
|
||||
// Try to join existing logout session if it exists and browser session is required
|
||||
if (browserCookie && logoutAuthSession != null) {
|
||||
if (Objects.equals(AuthenticationSessionModel.Action.LOGGING_OUT.name(), logoutAuthSession.getAction())) {
|
||||
return logoutAuthSession;
|
||||
}
|
||||
// Re-create the authentication session for logout
|
||||
logoutAuthSession = logoutAuthSession.getParentSession().createAuthenticationSession(client);
|
||||
} else {
|
||||
RootAuthenticationSessionModel rootLogoutSession = asm.createAuthenticationSession(realm, browserCookie);
|
||||
logoutAuthSession = rootLogoutSession.createAuthenticationSession(client);
|
||||
// Try to lookup current authSessionId from browser cookie. If doesn't exists, use the same as current userSession
|
||||
String authSessionId = null;
|
||||
boolean browserCookiePresent = false;
|
||||
if (browserCookie) {
|
||||
authSessionId = asm.getCurrentAuthenticationSessionId(realm);
|
||||
}
|
||||
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());
|
||||
return logoutAuthSession;
|
||||
}
|
||||
|
@ -444,7 +464,7 @@ public class AuthenticationManager {
|
|||
}
|
||||
|
||||
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);
|
||||
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) {
|
||||
// Account management client is used as a placeholder
|
||||
ClientModel client = realm.getClientByClientId(Constants.ACCOUNT_MANAGEMENT_CLIENT_ID);
|
||||
|
||||
final AuthenticationSessionManager asm = new AuthenticationSessionManager(session);
|
||||
AuthenticationSessionModel logoutAuthSession = asm.getCurrentAuthenticationSession(realm, client);
|
||||
AuthenticationSessionModel logoutAuthSession = createOrJoinLogoutSession(session, realm, asm, userSession, true);
|
||||
|
||||
checkUserSessionOnlyHasLoggedOutClients(realm, userSession, logoutAuthSession);
|
||||
|
||||
expireIdentityCookie(realm, uriInfo, connection);
|
||||
|
@ -503,6 +521,7 @@ public class AuthenticationManager {
|
|||
.setEventBuilder(event);
|
||||
Response response = protocol.finishLogout(userSession);
|
||||
session.sessions().removeUserSession(realm, userSession);
|
||||
session.authenticationSessions().removeRootAuthenticationSession(realm, logoutAuthSession.getParentSession());
|
||||
return response;
|
||||
}
|
||||
|
||||
|
@ -733,6 +752,7 @@ public class AuthenticationManager {
|
|||
}
|
||||
|
||||
uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId());
|
||||
uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId());
|
||||
|
||||
URI redirect = uriBuilder.build(realm.getName());
|
||||
return Response.status(302).location(redirect).build();
|
||||
|
|
|
@ -80,14 +80,14 @@ public class AuthenticationSessionManager {
|
|||
* @param realm
|
||||
* @return
|
||||
*/
|
||||
public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm, ClientModel client) {
|
||||
public AuthenticationSessionModel getCurrentAuthenticationSession(RealmModel realm, ClientModel client, String tabId) {
|
||||
String authSessionId = getAuthSessionCookieDecoded(realm);
|
||||
|
||||
if (authSessionId == 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
|
||||
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);
|
||||
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) {
|
||||
ParseResult<CLIENT_SESSION> result = new ParseResult<>();
|
||||
if (code == null) {
|
||||
|
@ -89,7 +90,7 @@ public class ClientSessionCode<CLIENT_SESSION extends CommonClientSessionModel>
|
|||
}
|
||||
try {
|
||||
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) {
|
||||
result.authSessionNotFound = true;
|
||||
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) {
|
||||
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) {
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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>
|
||||
*/
|
||||
|
@ -79,7 +80,7 @@ class CodeGenerateUtil {
|
|||
|
||||
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);
|
||||
|
||||
|
@ -101,9 +102,9 @@ class CodeGenerateUtil {
|
|||
private static class AuthenticationSessionModelParser implements ClientSessionParser<AuthenticationSessionModel> {
|
||||
|
||||
@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
|
||||
return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client);
|
||||
return new AuthenticationSessionManager(session).getCurrentAuthenticationSession(realm, client, tabId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -163,7 +164,7 @@ class CodeGenerateUtil {
|
|||
private CodeJWT codeJWT;
|
||||
|
||||
@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 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.validation.Validation;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -292,7 +293,16 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
|
||||
// Create AuthenticationSessionModel with same ID like userSession and refresh cookie
|
||||
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);
|
||||
|
||||
ClientSessionCode<AuthenticationSessionModel> clientSessionCode = new ClientSessionCode<>(session, realmModel, authSession);
|
||||
|
@ -329,14 +339,20 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
|
||||
@POST
|
||||
@Path("/{provider_id}/login")
|
||||
public Response performPostLogin(@PathParam("provider_id") String providerId, @QueryParam("code") String code, @QueryParam("client_id") String clientId) {
|
||||
return performLogin(providerId, code, clientId);
|
||||
public Response performPostLogin(@PathParam("provider_id") String providerId,
|
||||
@QueryParam("code") String code,
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
return performLogin(providerId, code, clientId, tabId);
|
||||
}
|
||||
|
||||
@GET
|
||||
@NoCache
|
||||
@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);
|
||||
|
||||
if (isDebugEnabled()) {
|
||||
|
@ -344,7 +360,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
}
|
||||
|
||||
try {
|
||||
ParsedCodeContext parsedCode = parseSessionCode(code, clientId);
|
||||
ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
|
||||
if (parsedCode.response != null) {
|
||||
return parsedCode.response;
|
||||
}
|
||||
|
@ -541,6 +557,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
|
||||
URI redirect = LoginActionsService.firstBrokerLoginProcessor(uriInfo)
|
||||
.queryParam(Constants.CLIENT_ID, authenticationSession.getClient().getClientId())
|
||||
.queryParam(Constants.TAB_ID, authenticationSession.getTabId())
|
||||
.build(realmModel.getName());
|
||||
return Response.status(302).location(redirect).build();
|
||||
|
||||
|
@ -576,8 +593,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
@GET
|
||||
@NoCache
|
||||
@Path("/after-first-broker-login")
|
||||
public Response afterFirstBrokerLogin(@QueryParam("code") String code, @QueryParam("client_id") String clientId) {
|
||||
ParsedCodeContext parsedCode = parseSessionCode(code, clientId);
|
||||
public Response afterFirstBrokerLogin(@QueryParam("code") String code,
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
|
||||
if (parsedCode.response != null) {
|
||||
return parsedCode.response;
|
||||
}
|
||||
|
@ -694,6 +713,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
|
||||
URI redirect = LoginActionsService.postBrokerLoginProcessor(uriInfo)
|
||||
.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId())
|
||||
.queryParam(Constants.TAB_ID, authSession.getTabId())
|
||||
.build(realmModel.getName());
|
||||
return Response.status(302).location(redirect).build();
|
||||
}
|
||||
|
@ -704,8 +724,10 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
@GET
|
||||
@NoCache
|
||||
@Path("/after-post-broker-login")
|
||||
public Response afterPostBrokerLoginFlow(@QueryParam("code") String code, @QueryParam("client_id") String clientId) {
|
||||
ParsedCodeContext parsedCode = parseSessionCode(code, clientId);
|
||||
public Response afterPostBrokerLoginFlow(@QueryParam("code") String code,
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
ParsedCodeContext parsedCode = parseSessionCode(code, clientId, tabId);
|
||||
if (parsedCode.response != null) {
|
||||
return parsedCode.response;
|
||||
}
|
||||
|
@ -957,17 +979,18 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
IdentityBrokerState state = IdentityBrokerState.encoded(encodedCode);
|
||||
String code = state.getDecodedState();
|
||||
String clientId = state.getClientId();
|
||||
return parseSessionCode(code, clientId);
|
||||
String tabId = state.getTabId();
|
||||
return parseSessionCode(code, clientId, tabId);
|
||||
}
|
||||
|
||||
private ParsedCodeContext parseSessionCode(String code, String clientId) {
|
||||
if (code == null || clientId == null) {
|
||||
logger.debugf("Invalid request. Authorization code or clientId was null. Code=" + code + ", clientId=" + clientId);
|
||||
private ParsedCodeContext parseSessionCode(String code, String clientId, String tabId) {
|
||||
if (code == null || clientId == null || tabId == null) {
|
||||
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);
|
||||
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();
|
||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.LOGIN)) {
|
||||
|
||||
|
@ -1047,7 +1070,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
if (clientSessionCode != null) {
|
||||
authSession = clientSessionCode.getClientSession();
|
||||
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));
|
||||
|
@ -1091,7 +1114,8 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
|
|||
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.AuthenticationFlowURLHelper;
|
||||
import org.keycloak.services.util.BrowserHistoryHelper;
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||
|
||||
|
@ -181,16 +182,16 @@ public class LoginActionsService {
|
|||
}
|
||||
}
|
||||
|
||||
private SessionCodeChecks checksForCode(String code, String execution, String clientId, String flowPath) {
|
||||
SessionCodeChecks res = new SessionCodeChecks(realm, uriInfo, request, clientConnection, session, event, code, execution, clientId, 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, tabId, flowPath);
|
||||
res.initialVerify();
|
||||
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)
|
||||
.getLastExecutionUrl(flowPath, executionId, clientId);
|
||||
.getLastExecutionUrl(flowPath, executionId, clientId, tabId);
|
||||
}
|
||||
|
||||
|
||||
|
@ -201,9 +202,10 @@ public class LoginActionsService {
|
|||
*/
|
||||
@Path(RESTART_PATH)
|
||||
@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);
|
||||
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();
|
||||
if (authSession == null) {
|
||||
|
@ -217,7 +219,7 @@ public class LoginActionsService {
|
|||
|
||||
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);
|
||||
return Response.status(Response.Status.FOUND).location(redirectUri).build();
|
||||
}
|
||||
|
@ -233,10 +235,11 @@ public class LoginActionsService {
|
|||
@GET
|
||||
public Response authenticate(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution,
|
||||
@QueryParam("client_id") String clientId) {
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
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)) {
|
||||
return checks.getResponse();
|
||||
}
|
||||
|
@ -302,8 +305,9 @@ public class LoginActionsService {
|
|||
@POST
|
||||
public Response authenticateForm(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution,
|
||||
@QueryParam("client_id") String clientId) {
|
||||
return authenticate(code, execution, clientId);
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
return authenticate(code, execution, clientId, tabId);
|
||||
}
|
||||
|
||||
@Path(RESET_CREDENTIALS_PATH)
|
||||
|
@ -311,14 +315,15 @@ public class LoginActionsService {
|
|||
public Response resetCredentialsPOST(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution,
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId,
|
||||
@QueryParam(Constants.KEY) String key) {
|
||||
if (key != null) {
|
||||
return handleActionToken(key, execution, clientId);
|
||||
return handleActionToken(key, execution, clientId, tabId);
|
||||
}
|
||||
|
||||
event.event(EventType.RESET_PASSWORD);
|
||||
|
||||
return resetCredentials(code, execution, clientId);
|
||||
return resetCredentials(code, execution, clientId, tabId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -333,9 +338,10 @@ public class LoginActionsService {
|
|||
@GET
|
||||
public Response resetCredentialsGET(@QueryParam("code") String code,
|
||||
@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);
|
||||
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
|
||||
if (authSession == null && code == null) {
|
||||
|
@ -350,7 +356,7 @@ public class LoginActionsService {
|
|||
}
|
||||
|
||||
event.event(EventType.RESET_PASSWORD);
|
||||
return resetCredentials(code, execution, clientId);
|
||||
return resetCredentials(code, execution, clientId, tabId);
|
||||
}
|
||||
|
||||
AuthenticationSessionModel createAuthenticationSessionForClient()
|
||||
|
@ -380,8 +386,8 @@ public class LoginActionsService {
|
|||
* @param execution
|
||||
* @return
|
||||
*/
|
||||
protected Response resetCredentials(String code, String execution, String clientId) {
|
||||
SessionCodeChecks checks = checksForCode(code, execution, clientId, RESET_CREDENTIALS_PATH);
|
||||
protected Response resetCredentials(String code, String execution, String clientId, String tabId) {
|
||||
SessionCodeChecks checks = checksForCode(code, execution, clientId, tabId, RESET_CREDENTIALS_PATH);
|
||||
if (!checks.verifyActiveAndValidAction(AuthenticationSessionModel.Action.AUTHENTICATE.name(), ClientSessionCode.ActionType.USER)) {
|
||||
return checks.getResponse();
|
||||
}
|
||||
|
@ -408,11 +414,12 @@ public class LoginActionsService {
|
|||
@GET
|
||||
public Response executeActionToken(@QueryParam("key") String key,
|
||||
@QueryParam("execution") String execution,
|
||||
@QueryParam("client_id") String clientId) {
|
||||
return handleActionToken(key, execution, clientId);
|
||||
@QueryParam("client_id") String 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;
|
||||
ActionTokenHandler<T> handler;
|
||||
ActionTokenContext<T> tokenContext;
|
||||
|
@ -428,7 +435,7 @@ public class LoginActionsService {
|
|||
}
|
||||
if (client != null) {
|
||||
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);
|
||||
|
@ -496,18 +503,19 @@ public class LoginActionsService {
|
|||
tokenContext = new ActionTokenContext(session, realm, uriInfo, clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow);
|
||||
|
||||
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
|
||||
LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, tokenAuthSessionId);
|
||||
String sessionId = AuthenticationSessionCompoundId.encoded(tokenAuthSessionCompoundId).getRootSessionId();
|
||||
LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, sessionId);
|
||||
}
|
||||
|
||||
if (authSession == null) {
|
||||
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
|
||||
tokenContext.setAuthenticationSession(authSession, true);
|
||||
} else if (tokenAuthSessionId == null ||
|
||||
! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, tokenAuthSessionId, client)) {
|
||||
} else if (tokenAuthSessionCompoundId == null ||
|
||||
! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, authSession, tokenAuthSessionCompoundId)) {
|
||||
// There exists an authentication session but no auth session ID was received in the action token
|
||||
logger.debugf("Authentication session in progress but no authentication session ID was found in action token %s, restarting.", token.getId());
|
||||
new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
|
||||
|
@ -609,8 +617,9 @@ public class LoginActionsService {
|
|||
@GET
|
||||
public Response registerPage(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution,
|
||||
@QueryParam("client_id") String clientId) {
|
||||
return registerRequest(code, execution, clientId, false);
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
return registerRequest(code, execution, clientId, tabId,false);
|
||||
}
|
||||
|
||||
|
||||
|
@ -624,19 +633,20 @@ public class LoginActionsService {
|
|||
@POST
|
||||
public Response processRegister(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution,
|
||||
@QueryParam("client_id") String clientId) {
|
||||
return registerRequest(code, execution, clientId, true);
|
||||
@QueryParam("client_id") String clientId,
|
||||
@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);
|
||||
if (!realm.isRegistrationAllowed()) {
|
||||
event.error(Errors.REGISTRATION_DISABLED);
|
||||
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)) {
|
||||
return checks.getResponse();
|
||||
}
|
||||
|
@ -653,42 +663,46 @@ public class LoginActionsService {
|
|||
@GET
|
||||
public Response firstBrokerLoginGet(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution,
|
||||
@QueryParam("client_id") String clientId) {
|
||||
return brokerLoginFlow(code, execution, clientId, FIRST_BROKER_LOGIN_PATH);
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
return brokerLoginFlow(code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
|
||||
}
|
||||
|
||||
@Path(FIRST_BROKER_LOGIN_PATH)
|
||||
@POST
|
||||
public Response firstBrokerLoginPost(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution,
|
||||
@QueryParam("client_id") String clientId) {
|
||||
return brokerLoginFlow(code, execution, clientId, FIRST_BROKER_LOGIN_PATH);
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
return brokerLoginFlow(code, execution, clientId, tabId, FIRST_BROKER_LOGIN_PATH);
|
||||
}
|
||||
|
||||
@Path(POST_BROKER_LOGIN_PATH)
|
||||
@GET
|
||||
public Response postBrokerLoginGet(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution,
|
||||
@QueryParam("client_id") String clientId) {
|
||||
return brokerLoginFlow(code, execution, clientId, POST_BROKER_LOGIN_PATH);
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
return brokerLoginFlow(code, execution, clientId, tabId, POST_BROKER_LOGIN_PATH);
|
||||
}
|
||||
|
||||
@Path(POST_BROKER_LOGIN_PATH)
|
||||
@POST
|
||||
public Response postBrokerLoginPost(@QueryParam("code") String code,
|
||||
@QueryParam("execution") String execution,
|
||||
@QueryParam("client_id") String clientId) {
|
||||
return brokerLoginFlow(code, execution, clientId, POST_BROKER_LOGIN_PATH);
|
||||
@QueryParam("client_id") String clientId,
|
||||
@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);
|
||||
|
||||
EventType eventType = firstBrokerLogin ? EventType.IDENTITY_PROVIDER_FIRST_LOGIN : EventType.IDENTITY_PROVIDER_POST_LOGIN;
|
||||
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)) {
|
||||
return checks.getResponse();
|
||||
}
|
||||
|
@ -747,8 +761,9 @@ public class LoginActionsService {
|
|||
authSession.getParentSession().setTimestamp(Time.currentTime());
|
||||
|
||||
String clientId = authSession.getClient().getClientId();
|
||||
URI redirect = firstBrokerLogin ? Urls.identityProviderAfterFirstBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId) :
|
||||
Urls.identityProviderAfterPostBrokerLogin(uriInfo.getBaseUri(), realm.getName(), accessCode.getOrGenerateCode(), clientId) ;
|
||||
String tabId = authSession.getTabId();
|
||||
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);
|
||||
|
||||
return Response.status(302).location(redirect).build();
|
||||
|
@ -768,7 +783,8 @@ public class LoginActionsService {
|
|||
event.event(EventType.LOGIN);
|
||||
String code = formData.getFirst("code");
|
||||
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())) {
|
||||
return checks.getResponse();
|
||||
}
|
||||
|
@ -858,22 +874,24 @@ public class LoginActionsService {
|
|||
@POST
|
||||
public Response requiredActionPOST(@QueryParam("code") final String code,
|
||||
@QueryParam("execution") String action,
|
||||
@QueryParam("client_id") String clientId) {
|
||||
return processRequireAction(code, action, clientId);
|
||||
@QueryParam("client_id") String clientId,
|
||||
@QueryParam(Constants.TAB_ID) String tabId) {
|
||||
return processRequireAction(code, action, clientId, tabId);
|
||||
}
|
||||
|
||||
@Path(REQUIRED_ACTION)
|
||||
@GET
|
||||
public Response requiredActionGET(@QueryParam("code") final String code,
|
||||
@QueryParam("execution") String action,
|
||||
@QueryParam("client_id") String clientId) {
|
||||
return processRequireAction(code, action, clientId);
|
||||
@QueryParam("client_id") String 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);
|
||||
|
||||
SessionCodeChecks checks = checksForCode(code, action, clientId, REQUIRED_ACTION);
|
||||
SessionCodeChecks checks = checksForCode(code, action, clientId, tabId, REQUIRED_ACTION);
|
||||
if (!checks.verifyRequiredAction(action)) {
|
||||
return checks.getResponse();
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.keycloak.representations.JsonWebToken;
|
|||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||
import org.keycloak.services.messages.Messages;
|
||||
import org.keycloak.sessions.AuthenticationSessionCompoundId;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
import org.keycloak.sessions.CommonClientSessionModel.Action;
|
||||
import java.util.Objects;
|
||||
|
@ -251,46 +252,34 @@ public class LoginActionsServiceChecks {
|
|||
*
|
||||
* @param <T>
|
||||
*/
|
||||
public static <T extends JsonWebToken> boolean doesAuthenticationSessionFromCookieMatchOneFromToken(ActionTokenContext<T> context, String authSessionIdFromToken, ClientModel client) throws VerificationException {
|
||||
if (authSessionIdFromToken == null) {
|
||||
public static <T extends JsonWebToken> boolean doesAuthenticationSessionFromCookieMatchOneFromToken(
|
||||
ActionTokenContext<T> context, AuthenticationSessionModel authSessionFromCookie, String authSessionCompoundIdFromToken) throws VerificationException {
|
||||
if (authSessionCompoundIdFromToken == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AuthenticationSessionManager asm = new AuthenticationSessionManager(context.getSession());
|
||||
String authSessionIdFromCookie = asm.getCurrentAuthenticationSessionId(context.getRealm());
|
||||
|
||||
if (authSessionIdFromCookie == null) {
|
||||
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)) {
|
||||
if (Objects.equals(AuthenticationSessionCompoundId.fromAuthSession(authSessionFromCookie).getEncodedId(), authSessionCompoundIdFromToken)) {
|
||||
context.setAuthenticationSession(authSessionFromCookie, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
String parentSessionId = authSessionFromCookie.getAuthNote(AuthenticationProcessor.FORKED_FROM);
|
||||
if (parentSessionId == null || ! Objects.equals(authSessionIdFromToken, parentSessionId)) {
|
||||
// Check if it's forked session. It would have same parent (rootSession) as our browser authenticationSession
|
||||
String parentTabId = authSessionFromCookie.getAuthNote(AuthenticationProcessor.FORKED_FROM);
|
||||
if (parentTabId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AuthenticationSessionModel authSessionFromParent = asm.getAuthenticationSessionByIdAndClient(context.getRealm(), parentSessionId, client);
|
||||
|
||||
AuthenticationSessionModel authSessionFromParent = authSessionFromCookie.getParentSession().getAuthenticationSession(authSessionFromCookie.getClient(), parentTabId);
|
||||
if (authSessionFromParent == null) {
|
||||
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
|
||||
// Don't expire KC_RESTART cookie at this point
|
||||
asm.removeAuthenticationSession(context.getRealm(), authSessionFromCookie, false);
|
||||
LOG.debugf("Removed forked session: %s", authSessionFromCookie.getParentSession().getId());
|
||||
|
||||
// Refresh browser cookie
|
||||
asm.setAuthSessionCookie(parentSessionId, context.getRealm());
|
||||
LOG.debugf("Switched to forked tab: %s from: %s . Root session: %s", authSessionFromParent.getTabId(), authSessionFromCookie.getTabId(), authSessionFromCookie.getParentSession().getId());
|
||||
|
||||
context.setAuthenticationSession(authSessionFromParent, false);
|
||||
context.setExecutionId(authSessionFromParent.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION));
|
||||
|
|
|
@ -69,10 +69,12 @@ public class SessionCodeChecks {
|
|||
private final String code;
|
||||
private final String execution;
|
||||
private final String clientId;
|
||||
private final String tabId;
|
||||
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.uriInfo = uriInfo;
|
||||
this.request = request;
|
||||
|
@ -83,6 +85,7 @@ public class SessionCodeChecks {
|
|||
this.code = code;
|
||||
this.execution = execution;
|
||||
this.clientId = clientId;
|
||||
this.tabId = tabId;
|
||||
this.flowPath = flowPath;
|
||||
}
|
||||
|
||||
|
@ -145,8 +148,9 @@ public class SessionCodeChecks {
|
|||
|
||||
// object retrieve
|
||||
AuthenticationSessionManager authSessionManager = new AuthenticationSessionManager(session);
|
||||
AuthenticationSessionModel authSession = authSessionManager.getCurrentAuthenticationSession(realm, client);
|
||||
AuthenticationSessionModel authSession = authSessionManager.getCurrentAuthenticationSession(realm, client, tabId);
|
||||
if (authSession != null) {
|
||||
session.getProvider(LoginFormsProvider.class).setAuthenticationSession(authSession);
|
||||
return authSession;
|
||||
}
|
||||
|
||||
|
@ -246,14 +250,14 @@ public class SessionCodeChecks {
|
|||
return false;
|
||||
}
|
||||
} 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();
|
||||
if (clientCode == null) {
|
||||
|
||||
// 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))) {
|
||||
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);
|
||||
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);
|
||||
|
||||
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);
|
||||
response = Response.status(Response.Status.FOUND).location(redirectUri).build();
|
||||
return false;
|
||||
|
@ -371,7 +375,7 @@ public class SessionCodeChecks {
|
|||
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);
|
||||
return Response.status(Response.Status.FOUND).location(redirectUri).build();
|
||||
} else {
|
||||
|
@ -392,15 +396,16 @@ public class SessionCodeChecks {
|
|||
|
||||
ClientModel client = authSession.getClient();
|
||||
uriBuilder.queryParam(Constants.CLIENT_ID, client.getClientId());
|
||||
uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId());
|
||||
|
||||
URI redirect = uriBuilder.build(realm.getName());
|
||||
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)
|
||||
.getLastExecutionUrl(flowPath, executionId, clientId);
|
||||
.getLastExecutionUrl(flowPath, executionId, clientId, tabId);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -181,16 +181,20 @@ public class AccountFormService extends AbstractSecuredLocalService {
|
|||
setReferrerOnPage();
|
||||
|
||||
UserSessionModel userSession = auth.getSession();
|
||||
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getAuthenticationSessionByIdAndClient(realm, userSession.getId(), client);
|
||||
if (authSession != null) {
|
||||
String forwardedError = authSession.getAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
|
||||
if (forwardedError != null) {
|
||||
try {
|
||||
FormMessage errorMessage = JsonSerialization.readValue(forwardedError, FormMessage.class);
|
||||
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);
|
||||
|
||||
String tabId = request.getUri().getQueryParameters().getFirst(org.keycloak.models.Constants.TAB_ID);
|
||||
if (tabId != null) {
|
||||
AuthenticationSessionModel authSession = new AuthenticationSessionManager(session).getAuthenticationSessionByIdAndClient(realm, userSession.getId(), client, tabId);
|
||||
if (authSession != null) {
|
||||
String forwardedError = authSession.getAuthNote(ACCOUNT_MGMT_FORWARDED_ERROR_NOTE);
|
||||
if (forwardedError != null) {
|
||||
try {
|
||||
FormMessage errorMessage = JsonSerialization.readValue(forwardedError, FormMessage.class);
|
||||
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)
|
||||
.path(flowPath);
|
||||
|
||||
|
@ -71,6 +71,7 @@ public class AuthenticationFlowURLHelper {
|
|||
uriBuilder.queryParam(Constants.EXECUTION, executionId);
|
||||
}
|
||||
uriBuilder.queryParam(Constants.CLIENT_ID, clientId);
|
||||
uriBuilder.queryParam(Constants.TAB_ID, tabId);
|
||||
|
||||
return uriBuilder.build(realm.getName());
|
||||
}
|
||||
|
@ -88,7 +89,7 @@ public class AuthenticationFlowURLHelper {
|
|||
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) {
|
||||
|
|
|
@ -87,7 +87,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
|||
Twitter twitter = new TwitterFactory().getInstance();
|
||||
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());
|
||||
AuthenticationSessionModel authSession = request.getAuthenticationSession();
|
||||
|
@ -198,9 +198,17 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
|||
|
||||
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);
|
||||
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 twitterSecret = authSession.getAuthNote(TWITTER_TOKENSECRET);
|
||||
|
@ -239,7 +247,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProvider<OAuth2Iden
|
|||
sendErrorEvent();
|
||||
return e.getResponse();
|
||||
} catch (Exception e) {
|
||||
logger.error("Could get user profile from twitter.", e);
|
||||
logger.error("Couldn't get user profile from twitter.", e);
|
||||
sendErrorEvent();
|
||||
return ErrorPage.error(session, authSession, Response.Status.BAD_GATEWAY, Messages.UNEXPECTED_ERROR_HANDLING_RESPONSE);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package org.keycloak.testsuite.updaters;
|
||||
|
||||
import org.keycloak.admin.client.resource.UserResource;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import java.io.Closeable;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -52,4 +54,12 @@ public class UserAttributeUpdater {
|
|||
|
||||
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;
|
||||
|
||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||
import org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionToken;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Assert;
|
||||
|
@ -45,6 +46,7 @@ import org.keycloak.testsuite.pages.VerifyEmailPage;
|
|||
import org.keycloak.testsuite.updaters.UserAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.GreenMailRule;
|
||||
import org.keycloak.testsuite.util.MailUtils;
|
||||
import org.keycloak.testsuite.util.SecondBrowser;
|
||||
import org.keycloak.testsuite.util.UserActionTokenBuilder;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
|
||||
|
@ -58,6 +60,12 @@ import java.util.HashMap;
|
|||
import java.util.Map;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
|
@ -94,6 +102,10 @@ public class RequiredActionEmailVerificationTest extends AbstractTestRealmKeyclo
|
|||
|
||||
private String testUserId;
|
||||
|
||||
@Drone
|
||||
@SecondBrowser
|
||||
protected WebDriver driver2;
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
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)
|
||||
.queryParam(OAuth2Constants.CODE, queryParams.get(OAuth2Constants.CODE))
|
||||
.queryParam(Constants.CLIENT_ID, queryParams.get(Constants.CLIENT_ID))
|
||||
.queryParam(Constants.TAB_ID, queryParams.get(Constants.TAB_ID))
|
||||
.build().toString();
|
||||
|
||||
System.out.println("hack uri: " + uri);
|
||||
|
|
|
@ -312,12 +312,8 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest {
|
|||
loginPage.login("login-test", "password");
|
||||
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();
|
||||
loginExpiredPage.assertCurrent();
|
||||
|
||||
// Click 'restart' link. I should be on login page
|
||||
loginExpiredPage.clickLoginRestartLink();
|
||||
loginPage.assertCurrent();
|
||||
}
|
||||
|
||||
|
@ -326,6 +322,7 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest {
|
|||
public void backButtonInResetPasswordFlow() throws Exception {
|
||||
// Click on "forgot password" and type username
|
||||
loginPage.open();
|
||||
loginPage.login("login-test", "bad-username");
|
||||
loginPage.resetPassword();
|
||||
|
||||
resetPasswordPage.assertCurrent();
|
||||
|
@ -344,22 +341,19 @@ public class BrowserButtonsTest extends AbstractTestRealmKeycloakTest {
|
|||
|
||||
updatePasswordPage.assertCurrent();
|
||||
|
||||
// Click browser back. Should be on 'page expired'
|
||||
// Click browser back. Should be on loginPage for "forked flow"
|
||||
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();
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
// 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
|
||||
public void multipleTabsParallelLoginTest() {
|
||||
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)
|
||||
.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)
|
||||
.queryParam(OAuth2Constants.CODE, queryParams.get(OAuth2Constants.CODE))
|
||||
.queryParam(Constants.CLIENT_ID, queryParams.get(Constants.CLIENT_ID))
|
||||
.queryParam(Constants.TAB_ID, queryParams.get(Constants.TAB_ID))
|
||||
.build().toString();
|
||||
|
||||
log.info("hack uri: " + uri);
|
||||
|
|
|
@ -92,19 +92,7 @@ public class OIDCFirstBrokerLoginTest extends AbstractFirstBrokerLoginTest {
|
|||
}, APP_REALM_ID);
|
||||
|
||||
// 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("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");
|
||||
linkUserWithSamlBroker("pedroigor", "psilva@redhat.com");
|
||||
|
||||
|
||||
// login through OIDC broker now
|
||||
|
@ -159,4 +147,84 @@ public class OIDCFirstBrokerLoginTest extends AbstractFirstBrokerLoginTest {
|
|||
}, 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);
|
||||
AuthenticationSessionModel authSession = rootAuthSession.createAuthenticationSession(client1);
|
||||
|
||||
String tabId = authSession.getTabId();
|
||||
|
||||
authSession.setAction("foo");
|
||||
rootAuthSession.setTimestamp(100);
|
||||
|
||||
|
||||
resetSession();
|
||||
|
||||
client1 = realm.getClientByClientId("test-app");
|
||||
|
||||
// Ensure session is here
|
||||
rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId());
|
||||
authSession = rootAuthSession.getAuthenticationSession(client1, tabId);
|
||||
testAuthenticationSession(authSession, client1.getId(), null, "foo");
|
||||
Assert.assertEquals(100, rootAuthSession.getTimestamp());
|
||||
|
||||
|
@ -107,7 +113,7 @@ public class AuthenticationSessionProviderTest {
|
|||
// Ensure session was updated
|
||||
rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, rootAuthSession.getId());
|
||||
client1 = realm.getClientByClientId("test-app");
|
||||
authSession = rootAuthSession.getAuthenticationSession(client1);
|
||||
authSession = rootAuthSession.getAuthenticationSession(client1, tabId);
|
||||
testAuthenticationSession(authSession, client1.getId(), user1.getId(), "foo-updated");
|
||||
Assert.assertEquals(200, rootAuthSession.getTimestamp());
|
||||
|
||||
|
@ -127,6 +133,7 @@ public class AuthenticationSessionProviderTest {
|
|||
UserModel user1 = session.users().getUserByUsername("user1", realm);
|
||||
|
||||
AuthenticationSessionModel authSession = session.authenticationSessions().createRootAuthenticationSession(realm).createAuthenticationSession(client1);
|
||||
String tabId = authSession.getTabId();
|
||||
|
||||
authSession.setAction("foo");
|
||||
authSession.getParentSession().setTimestamp(100);
|
||||
|
@ -141,13 +148,13 @@ public class AuthenticationSessionProviderTest {
|
|||
// Test restart root authentication session
|
||||
client1 = realm.getClientByClientId("test-app");
|
||||
authSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSession.getParentSession().getId())
|
||||
.getAuthenticationSession(client1);
|
||||
.getAuthenticationSession(client1, tabId);
|
||||
authSession.getParentSession().restartSession(realm);
|
||||
|
||||
resetSession();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -248,26 +255,26 @@ public class AuthenticationSessionProviderTest {
|
|||
String authSessionId = session.authenticationSessions().createRootAuthenticationSession(realm).getId();
|
||||
AuthenticationSessionModel authSession1 = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId).createAuthenticationSession(realm.getClientByClientId("test-app"));
|
||||
AuthenticationSessionModel authSession2 = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId).createAuthenticationSession(realm.getClientByClientId("third-party"));
|
||||
String tab1Id = authSession1.getTabId();
|
||||
String tab2Id = authSession2.getTabId();
|
||||
|
||||
authSession1.setAuthNote("foo", "bar");
|
||||
authSession2.setAuthNote("foo", "baz");
|
||||
|
||||
String testAppClientUUID = realm.getClientByClientId("test-app").getId();
|
||||
|
||||
resetSession();
|
||||
|
||||
RootAuthenticationSessionModel rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||
Assert.assertEquals(2, rootAuthSession.getAuthenticationSessions().size());
|
||||
Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app")).getAuthNote("foo"));
|
||||
Assert.assertEquals("baz", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("third-party")).getAuthNote("foo"));
|
||||
Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app"), tab1Id).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"));
|
||||
|
||||
resetSession();
|
||||
|
||||
rootAuthSession = session.authenticationSessions().getRootAuthenticationSession(realm, authSessionId);
|
||||
Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app")).getAuthNote("foo"));
|
||||
Assert.assertNull(rootAuthSession.getAuthenticationSession(realm.getClientByClientId("third-party")));
|
||||
Assert.assertEquals("bar", rootAuthSession.getAuthenticationSession(realm.getClientByClientId("test-app"), tab1Id).getAuthNote("foo"));
|
||||
Assert.assertNull(rootAuthSession.getAuthenticationSession(realm.getClientByClientId("third-party"), tab2Id));
|
||||
|
||||
// Revert client
|
||||
realm.addClient("third-party");
|
||||
|
|
Loading…
Reference in a new issue