KEYCLOAK-5938 Authentication sessions: Support for logins of multiple tabs of same client

This commit is contained in:
mposolda 2017-12-07 11:41:30 +01:00 committed by Marek Posolda
parent c3855510ef
commit 63efee6e15
57 changed files with 905 additions and 567 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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