diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java
new file mode 100644
index 0000000000..d7bdcdfcce
--- /dev/null
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/events/AuthenticationSessionAuthNoteUpdateEvent.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.models.cache.infinispan.events;
+
+import org.keycloak.cluster.ClusterEvent;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class AuthenticationSessionAuthNoteUpdateEvent implements ClusterEvent {
+
+ private String authSessionId;
+
+ private Map authNotesFragment;
+
+ public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, Map authNotesFragment) {
+ AuthenticationSessionAuthNoteUpdateEvent event = new AuthenticationSessionAuthNoteUpdateEvent();
+ event.authSessionId = authSessionId;
+ event.authNotesFragment = new LinkedHashMap<>(authNotesFragment);
+ return event;
+ }
+
+ public String getAuthSessionId() {
+ return authSessionId;
+ }
+
+ public Map getAuthNotesFragment() {
+ return authNotesFragment;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, authNotesFragment=%s ]", authSessionId, authNotesFragment);
+ }
+
+}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java
index 202fe5c99a..05a762b54f 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/AuthenticationSessionAdapter.java
@@ -18,7 +18,7 @@
package org.keycloak.models.sessions.infinispan;
import java.util.Collections;
-import java.util.HashMap;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@@ -144,21 +144,27 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
@Override
public String getClientNote(String name) {
- return entity.getClientNotes() != null ? entity.getClientNotes().get(name) : null;
+ return (entity.getClientNotes() != null && name != null) ? entity.getClientNotes().get(name) : null;
}
@Override
public void setClientNote(String name, String value) {
if (entity.getClientNotes() == null) {
- entity.setClientNotes(new HashMap<>());
+ entity.setClientNotes(new ConcurrentHashMap<>());
+ }
+ if (name != null) {
+ if (value == null) {
+ entity.getClientNotes().remove(name);
+ } else {
+ entity.getClientNotes().put(name, value);
+ }
}
- entity.getClientNotes().put(name, value);
update();
}
@Override
public void removeClientNote(String name) {
- if (entity.getClientNotes() != null) {
+ if (entity.getClientNotes() != null && name != null) {
entity.getClientNotes().remove(name);
}
update();
@@ -167,34 +173,40 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
@Override
public Map getClientNotes() {
if (entity.getClientNotes() == null || entity.getClientNotes().isEmpty()) return Collections.emptyMap();
- Map copy = new HashMap<>();
+ Map copy = new ConcurrentHashMap<>();
copy.putAll(entity.getClientNotes());
return copy;
}
@Override
public void clearClientNotes() {
- entity.setClientNotes(new HashMap<>());
+ entity.setClientNotes(new ConcurrentHashMap<>());
update();
}
@Override
public String getAuthNote(String name) {
- return entity.getAuthNotes() != null ? entity.getAuthNotes().get(name) : null;
+ return (entity.getAuthNotes() != null && name != null) ? entity.getAuthNotes().get(name) : null;
}
@Override
public void setAuthNote(String name, String value) {
if (entity.getAuthNotes() == null) {
- entity.setAuthNotes(new HashMap());
+ entity.setAuthNotes(new ConcurrentHashMap<>());
+ }
+ if (name != null) {
+ if (value == null) {
+ entity.getAuthNotes().remove(name);
+ } else {
+ entity.getAuthNotes().put(name, value);
+ }
}
- entity.getAuthNotes().put(name, value);
update();
}
@Override
public void removeAuthNote(String name) {
- if (entity.getAuthNotes() != null) {
+ if (entity.getAuthNotes() != null && name != null) {
entity.getAuthNotes().remove(name);
}
update();
@@ -202,16 +214,22 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
@Override
public void clearAuthNotes() {
- entity.setAuthNotes(new HashMap<>());
+ entity.setAuthNotes(new ConcurrentHashMap<>());
update();
}
@Override
public void setUserSessionNote(String name, String value) {
if (entity.getUserSessionNotes() == null) {
- entity.setUserSessionNotes(new HashMap());
+ entity.setUserSessionNotes(new ConcurrentHashMap<>());
+ }
+ if (name != null) {
+ if (value == null) {
+ entity.getUserSessionNotes().remove(name);
+ } else {
+ entity.getUserSessionNotes().put(name, value);
+ }
}
- entity.getUserSessionNotes().put(name, value);
update();
}
@@ -221,14 +239,14 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
if (entity.getUserSessionNotes() == null) {
return Collections.EMPTY_MAP;
}
- HashMap copy = new HashMap<>();
+ ConcurrentHashMap copy = new ConcurrentHashMap<>();
copy.putAll(entity.getUserSessionNotes());
return copy;
}
@Override
public void clearUserSessionNotes() {
- entity.setUserSessionNotes(new HashMap());
+ entity.setUserSessionNotes(new ConcurrentHashMap<>());
update();
}
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
index a802544cf7..5991f98944 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProvider.java
@@ -17,6 +17,7 @@
package org.keycloak.models.sessions.infinispan;
+import org.keycloak.cluster.ClusterProvider;
import java.util.Iterator;
import java.util.Map;
@@ -27,6 +28,7 @@ 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.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate;
import org.keycloak.models.utils.KeycloakModelUtils;
@@ -138,6 +140,20 @@ public class InfinispanAuthenticationSessionProvider implements AuthenticationSe
}
}
+ @Override
+ public void updateNonlocalSessionAuthNotes(String authSessionId, Map authNotesFragment) {
+ if (authSessionId == null) {
+ return;
+ }
+
+ ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+ cluster.notify(
+ InfinispanAuthenticationSessionProviderFactory.AUTHENTICATION_SESSION_EVENTS,
+ AuthenticationSessionAuthNoteUpdateEvent.create(authSessionId, authNotesFragment),
+ true
+ );
+ }
+
@Override
public void close() {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
index e6da14ef0c..aa6ede3e9a 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanAuthenticationSessionProviderFactory.java
@@ -19,18 +19,28 @@ package org.keycloak.models.sessions.infinispan;
import org.infinispan.Cache;
import org.keycloak.Config;
+import org.keycloak.cluster.ClusterEvent;
+import org.keycloak.cluster.ClusterProvider;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
import org.keycloak.sessions.AuthenticationSessionProvider;
import org.keycloak.sessions.AuthenticationSessionProviderFactory;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import org.jboss.logging.Logger;
/**
* @author Marek Posolda
*/
public class InfinispanAuthenticationSessionProviderFactory implements AuthenticationSessionProviderFactory {
+ private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProviderFactory.class);
+
+ private volatile Cache authSessionsCache;
@Override
public void init(Config.Scope config) {
@@ -39,12 +49,53 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
@Override
public AuthenticationSessionProvider create(KeycloakSession session) {
- InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
- Cache authSessionsCache = connections.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME);
-
+ lazyInit(session);
return new InfinispanAuthenticationSessionProvider(session, authSessionsCache);
}
+ private void updateAuthNotes(ClusterEvent clEvent) {
+ if (! (clEvent instanceof AuthenticationSessionAuthNoteUpdateEvent)) {
+ return;
+ }
+
+ AuthenticationSessionAuthNoteUpdateEvent event = (AuthenticationSessionAuthNoteUpdateEvent) clEvent;
+ AuthenticationSessionEntity authSession = this.authSessionsCache.get(event.getAuthSessionId());
+ updateAuthSession(authSession, event.getAuthNotesFragment());
+ }
+
+ private static void updateAuthSession(AuthenticationSessionEntity authSession, Map authNotesFragment) {
+ if (authSession != null) {
+ if (authSession.getAuthNotes() == null) {
+ authSession.setAuthNotes(new ConcurrentHashMap<>());
+ }
+
+ for (Entry me : authNotesFragment.entrySet()) {
+ String value = me.getValue();
+ if (value == null) {
+ authSession.getAuthNotes().remove(me.getKey());
+ } else {
+ authSession.getAuthNotes().put(me.getKey(), value);
+ }
+ }
+ }
+ }
+
+ private void lazyInit(KeycloakSession session) {
+ if (authSessionsCache == null) {
+ synchronized (this) {
+ if (authSessionsCache == null) {
+ InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
+ authSessionsCache = connections.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME);
+
+ ClusterProvider cluster = session.getProvider(ClusterProvider.class);
+ cluster.registerListener(AUTHENTICATION_SESSION_EVENTS, this::updateAuthNotes);
+
+ log.debug("Registered cluster listeners");
+ }
+ }
+ }
+ }
+
@Override
public void postInit(KeycloakSessionFactory factory) {
}
diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventType.java b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
index 583b0d4061..920646fa58 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/EventType.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/EventType.java
@@ -92,6 +92,8 @@ public enum EventType {
USER_INFO_REQUEST(false),
USER_INFO_REQUEST_ERROR(false),
+ IDENTITY_PROVIDER_LINK_ACCOUNT(true),
+ IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR(true),
IDENTITY_PROVIDER_LOGIN(false),
IDENTITY_PROVIDER_LOGIN_ERROR(false),
IDENTITY_PROVIDER_FIRST_LOGIN(true),
@@ -129,6 +131,10 @@ public enum EventType {
this.saveByDefault = saveByDefault;
}
+ /**
+ * Determines whether this event is stored when the admin has not set a specific set of event types to save.
+ * @return
+ */
public boolean isSaveByDefault() {
return saveByDefault;
}
diff --git a/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java
index b182458b5e..c8758cafdd 100644
--- a/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java
+++ b/server-spi-private/src/main/java/org/keycloak/sessions/AuthenticationSessionProviderFactory.java
@@ -23,4 +23,6 @@ import org.keycloak.provider.ProviderFactory;
* @author Marek Posolda
*/
public interface AuthenticationSessionProviderFactory extends ProviderFactory {
+ // TODO:hmlnarik: move this constant out of an interface into a more appropriate class
+ public static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS";
}
diff --git a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
index f284d934ff..99806d45b2 100644
--- a/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
+++ b/server-spi/src/main/java/org/keycloak/sessions/AuthenticationSessionProvider.java
@@ -20,6 +20,7 @@ package org.keycloak.sessions;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.Provider;
+import java.util.Map;
/**
* @author Marek Posolda
@@ -39,5 +40,14 @@ public interface AuthenticationSessionProvider extends Provider {
void onRealmRemoved(RealmModel realm);
void onClientRemoved(RealmModel realm, ClientModel client);
+ /**
+ * Requests update of authNotes of an authentication session that is not owned
+ * by this instance but might exist somewhere in the cluster.
+ *
+ * @param authSessionId
+ * @param authNotesFragment Map with authNote values. Auth note is removed if the corresponding value in the map is {@code null}.
+ */
+ void updateNonlocalSessionAuthNotes(String authSessionId, Map authNotesFragment);
+
}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
index 26598c1cee..ce45deb1b8 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenContext.java
@@ -17,6 +17,7 @@
package org.keycloak.authentication.actiontoken;
import org.keycloak.OAuth2Constants;
+import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.*;
@@ -25,6 +26,8 @@ import org.keycloak.representations.JsonWebToken;
import org.keycloak.services.Urls;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.sessions.AuthenticationSessionModel;
+import java.util.function.Function;
+import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilderException;
import javax.ws.rs.core.UriInfo;
import org.jboss.resteasy.spi.HttpRequest;
@@ -35,6 +38,16 @@ import org.jboss.resteasy.spi.HttpRequest;
*/
public class ActionTokenContext {
+ @FunctionalInterface
+ public interface ProcessAuthenticateFlow {
+ Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor);
+ };
+
+ @FunctionalInterface
+ public interface ProcessBrokerFlow {
+ Response brokerLoginFlow(String code, String execution, String flowPath);
+ };
+
private final KeycloakSession session;
private final RealmModel realm;
private final UriInfo uriInfo;
@@ -45,8 +58,13 @@ public class ActionTokenContext {
private AuthenticationSessionModel authenticationSession;
private boolean authenticationSessionFresh;
private String executionId;
+ private final ProcessAuthenticateFlow processAuthenticateFlow;
+ private final ProcessBrokerFlow processBrokerFlow;
- public ActionTokenContext(KeycloakSession session, RealmModel realm, UriInfo uriInfo, ClientConnection clientConnection, HttpRequest request, EventBuilder event, ActionTokenHandler handler) {
+ public ActionTokenContext(KeycloakSession session, RealmModel realm, UriInfo uriInfo,
+ ClientConnection clientConnection, HttpRequest request,
+ EventBuilder event, ActionTokenHandler handler, String executionId,
+ ProcessAuthenticateFlow processFlow, ProcessBrokerFlow processBrokerFlow) {
this.session = session;
this.realm = realm;
this.uriInfo = uriInfo;
@@ -54,6 +72,9 @@ public class ActionTokenContext {
this.request = request;
this.event = event;
this.handler = handler;
+ this.executionId = executionId;
+ this.processAuthenticateFlow = processFlow;
+ this.processBrokerFlow = processBrokerFlow;
}
public EventBuilder getEvent() {
@@ -131,4 +152,12 @@ public class ActionTokenContext {
public void setExecutionId(String executionId) {
this.executionId = executionId;
}
+
+ public Response processFlow(boolean action, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor) {
+ return processAuthenticateFlow.processFlow(action, getExecutionId(), getAuthenticationSession(), flowPath, flow, errorMessage, processor);
+ }
+
+ public Response brokerFlow(String code, String flowPath) {
+ return processBrokerFlow.brokerLoginFlow(code, getExecutionId(), flowPath);
+ }
}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
index e573df4c74..4368a747ff 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/ActionTokenHandler.java
@@ -35,21 +35,6 @@ import javax.ws.rs.core.Response;
*/
public interface ActionTokenHandler extends Provider {
- @FunctionalInterface
- public interface ProcessFlow {
- Response processFlow(boolean action, String execution, AuthenticationSessionModel authSession, String flowPath, AuthenticationFlowModel flow, String errorMessage, AuthenticationProcessor processor);
- };
-
- /**
- * Returns an array of verifiers that are tested prior to handling the token. All verifiers have to pass successfully
- * for token to be handled. The returned array must not be {@code null}.
- * @param tokenContext
- * @return Verifiers or an empty array
- */
- default Predicate super T>[] getVerifiers(ActionTokenContext tokenContext) {
- return new Predicate[] {};
- }
-
/**
* Performs the action as per the token details. This method is only called if all verifiers
* returned in {@link #handleToken} succeed.
@@ -59,7 +44,7 @@ public interface ActionTokenHandler extends Provider {
* @return
* @throws VerificationException
*/
- Response handleToken(T token, ActionTokenContext tokenContext, ProcessFlow processFlow);
+ Response handleToken(T token, ActionTokenContext tokenContext);
/**
* Returns the Java token class for use with deserialization.
@@ -67,6 +52,16 @@ public interface ActionTokenHandler extends Provider {
*/
Class getTokenClass();
+ /**
+ * Returns an array of verifiers that are tested prior to handling the token. All verifiers have to pass successfully
+ * for token to be handled. The returned array must not be {@code null}.
+ * @param tokenContext
+ * @return Verifiers or an empty array. The returned array must not be {@code null}.
+ */
+ default Predicate super T>[] getVerifiers(ActionTokenContext tokenContext) {
+ return new Predicate[] {};
+ }
+
/**
* Returns an authentication session ID requested from within the given token
* @param token Token. Can be {@code null}
@@ -95,17 +90,8 @@ public interface ActionTokenHandler extends Provider {
String getDefaultErrorMessage();
/**
- * Returns a response that restarts a flow that this action token initiates, or {@code null} if
- * no special handling is requested.
- *
- * @return
- */
- default Response handleRestartRequest(T token, ActionTokenContext tokenContext, ProcessFlow processFlow) {
- return null;
- }
-
- /**
- * Creates a fresh authentication session according to the information from the token.
+ * Creates a fresh authentication session according to the information from the token. The default
+ * implementation creates a new authentication session that requests termination after required actions.
* @param token
* @param tokenContext
* @return
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
index 117c4659f9..a5440a9b87 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/DefaultActionTokenKey.java
@@ -25,7 +25,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
*/
public class DefaultActionTokenKey extends JsonWebToken {
- // The authenticationSession note with ID of the user authenticated via the action token
+ /** The authenticationSession note with ID of the user authenticated via the action token */
public static final String ACTION_TOKEN_USER_ID = "ACTION_TOKEN_USER";
public DefaultActionTokenKey(String userId, String actionId) {
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
index 691fff527d..010c5174a9 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/execactions/ExecuteActionsActionTokenHandler.java
@@ -25,9 +25,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.messages.Messages;
-import org.keycloak.services.resources.LoginActionsServiceChecks;
import org.keycloak.sessions.AuthenticationSessionModel;
-import org.keycloak.sessions.CommonClientSessionModel.Action;
import javax.ws.rs.core.Response;
/**
@@ -61,7 +59,7 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander<
}
@Override
- public Response handleToken(ExecuteActionsActionToken token, ActionTokenContext tokenContext, ProcessFlow processFlow) {
+ public Response handleToken(ExecuteActionsActionToken token, ActionTokenContext tokenContext) {
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
String redirectUri = RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), token.getRedirectUri(),
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
new file mode 100644
index 0000000000..ea705edd66
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionToken.java
@@ -0,0 +1,69 @@
+/*
+ * 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.authentication.actiontoken.idpverifyemail;
+
+import org.keycloak.authentication.actiontoken.verifyemail.*;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.UUID;
+import org.keycloak.authentication.actiontoken.DefaultActionToken;
+
+/**
+ * Representation of a token that represents a time-limited verify e-mail action.
+ *
+ * @author hmlnarik
+ */
+public class IdpVerifyAccountLinkActionToken extends DefaultActionToken {
+
+ public static final String TOKEN_TYPE = "idp-verify-account-via-email";
+
+ private static final String JSON_FIELD_IDENTITY_PROVIDER_USERNAME = "idpu";
+ private static final String JSON_FIELD_IDENTITY_PROVIDER_ALIAS = "idpa";
+
+ @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_USERNAME)
+ private String identityProviderUsername;
+
+ @JsonProperty(value = JSON_FIELD_IDENTITY_PROVIDER_ALIAS)
+ private String identityProviderAlias;
+
+ public IdpVerifyAccountLinkActionToken(String userId, int absoluteExpirationInSecs, UUID actionVerificationNonce, String authenticationSessionId,
+ String identityProviderUsername, String identityProviderAlias) {
+ super(userId, TOKEN_TYPE, absoluteExpirationInSecs, actionVerificationNonce);
+ setAuthenticationSessionId(authenticationSessionId);
+ this.identityProviderUsername = identityProviderUsername;
+ this.identityProviderAlias = identityProviderAlias;
+ }
+
+ private IdpVerifyAccountLinkActionToken() {
+ super(null, TOKEN_TYPE, -1, null);
+ }
+
+ public String getIdentityProviderUsername() {
+ return identityProviderUsername;
+ }
+
+ public void setIdentityProviderUsername(String identityProviderUsername) {
+ this.identityProviderUsername = identityProviderUsername;
+ }
+
+ public String getIdentityProviderAlias() {
+ return identityProviderAlias;
+ }
+
+ public void setIdentityProviderAlias(String identityProviderAlias) {
+ this.identityProviderAlias = identityProviderAlias;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
new file mode 100644
index 0000000000..3aec118260
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/idpverifyemail/IdpVerifyAccountLinkActionTokenHandler.java
@@ -0,0 +1,97 @@
+/*
+ * 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.authentication.actiontoken.idpverifyemail;
+
+import org.keycloak.authentication.actiontoken.AbstractActionTokenHander;
+import org.keycloak.TokenVerifier.Predicate;
+import org.keycloak.authentication.AuthenticationProcessor;
+import org.keycloak.authentication.actiontoken.*;
+import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticator;
+import org.keycloak.events.*;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.managers.AuthenticationSessionManager;
+import org.keycloak.services.messages.Messages;
+import org.keycloak.sessions.AuthenticationSessionModel;
+import org.keycloak.sessions.AuthenticationSessionProvider;
+import java.util.Collections;
+import javax.ws.rs.core.Response;
+
+/**
+ * Action token handler for verification of e-mail address.
+ * @author hmlnarik
+ */
+public class IdpVerifyAccountLinkActionTokenHandler extends AbstractActionTokenHander {
+
+ public IdpVerifyAccountLinkActionTokenHandler() {
+ super(
+ IdpVerifyAccountLinkActionToken.TOKEN_TYPE,
+ IdpVerifyAccountLinkActionToken.class,
+ Messages.STALE_CODE,
+ EventType.IDENTITY_PROVIDER_LINK_ACCOUNT,
+ Errors.INVALID_TOKEN
+ );
+ }
+
+ @Override
+ public Predicate super IdpVerifyAccountLinkActionToken>[] getVerifiers(ActionTokenContext tokenContext) {
+ return TokenUtils.predicates(
+ );
+ }
+
+ @Override
+ public Response handleToken(IdpVerifyAccountLinkActionToken token, ActionTokenContext tokenContext) {
+ UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
+ EventBuilder event = tokenContext.getEvent();
+
+ event.event(EventType.IDENTITY_PROVIDER_LINK_ACCOUNT)
+ .detail(Details.EMAIL, user.getEmail())
+ .detail(Details.IDENTITY_PROVIDER, token.getIdentityProviderAlias())
+ .detail(Details.IDENTITY_PROVIDER_USERNAME, token.getIdentityProviderUsername())
+ .success();
+
+ // verify user email as we know it is valid as this entry point would never have gotten here.
+ user.setEmailVerified(true);
+
+ AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
+ if (tokenContext.isAuthenticationSessionFresh()) {
+ AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession());
+ asm.removeAuthenticationSession(tokenContext.getRealm(), authSession, true);
+
+ AuthenticationSessionProvider authSessProvider = tokenContext.getSession().authenticationSessions();
+ authSession = authSessProvider.getAuthenticationSession(tokenContext.getRealm(), token.getAuthenticationSessionId());
+
+ if (authSession != null) {
+ authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
+ } else {
+ authSessProvider.updateNonlocalSessionAuthNotes(
+ token.getAuthenticationSessionId(),
+ Collections.singletonMap(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername())
+ );
+ }
+
+ return tokenContext.getSession().getProvider(LoginFormsProvider.class)
+ .setSuccess(Messages.IDENTITY_PROVIDER_LINK_SUCCESS, token.getIdentityProviderAlias(), token.getIdentityProviderUsername())
+ .createInfoPage();
+ }
+
+ authSession.setAuthNote(IdpEmailVerificationAuthenticator.VERIFY_ACCOUNT_IDP_USERNAME, token.getIdentityProviderUsername());
+
+ return tokenContext.brokerFlow(null, authSession.getAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH));
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java
index c6f834b347..34174311e7 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/resetcred/ResetCredentialsActionTokenHandler.java
@@ -62,13 +62,11 @@ public class ResetCredentialsActionTokenHandler extends AbstractActionTokenHande
}
@Override
- public Response handleToken(ResetCredentialsActionToken token, ActionTokenContext tokenContext, ProcessFlow processFlow) {
+ public Response handleToken(ResetCredentialsActionToken token, ActionTokenContext tokenContext) {
AuthenticationProcessor authProcessor = new ResetCredsAuthenticationProcessor();
- return processFlow.processFlow(
+ return tokenContext.processFlow(
false,
- tokenContext.getExecutionId(),
- tokenContext.getAuthenticationSession(),
RESET_CREDENTIALS_PATH,
tokenContext.getRealm().getResetCredentialsFlow(),
null,
@@ -76,17 +74,6 @@ public class ResetCredentialsActionTokenHandler extends AbstractActionTokenHande
);
}
- @Override
- public Response handleRestartRequest(ResetCredentialsActionToken token, ActionTokenContext tokenContext, ProcessFlow processFlow) {
- // In the case restart is requested, the handling is exactly the same as if a token had been
- // handled correctly but with a fresh authentication session
- AuthenticationSessionManager asm = new AuthenticationSessionManager(tokenContext.getSession());
- asm.removeAuthenticationSession(tokenContext.getRealm(), tokenContext.getAuthenticationSession(), false);
-
- tokenContext.setAuthenticationSession(tokenContext.createAuthenticationSessionForClient(null), true);
- return handleToken(token, tokenContext, processFlow);
- }
-
public static class ResetCredsAuthenticationProcessor extends AuthenticationProcessor {
@Override
diff --git a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
index 1d324c2550..abe2127098 100644
--- a/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
+++ b/services/src/main/java/org/keycloak/authentication/actiontoken/verifyemail/VerifyEmailActionTokenHandler.java
@@ -57,7 +57,7 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander tokenContext, ProcessFlow processFlow) {
+ public Response handleToken(VerifyEmailActionToken token, ActionTokenContext tokenContext) {
UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
EventBuilder event = tokenContext.getEvent();
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
index b70f69b78d..d8b9b30689 100755
--- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpEmailVerificationAuthenticator.java
@@ -20,9 +20,10 @@ package org.keycloak.authentication.authenticators.broker;
import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionToken;
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
-import org.keycloak.authentication.requiredactions.VerifyEmail;
import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.common.util.Time;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details;
@@ -30,20 +31,21 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
-import org.keycloak.models.ClientSessionModel;
-import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
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.services.resources.LoginActionsService;
import org.keycloak.sessions.AuthenticationSessionModel;
-import javax.ws.rs.core.MultivaluedMap;
+import java.net.URI;
+import java.util.Objects;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.util.concurrent.TimeUnit;
+import javax.ws.rs.core.*;
/**
* @author Marek Posolda
@@ -52,42 +54,85 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class);
+ public static final String VERIFY_ACCOUNT_IDP_USERNAME = "VERIFY_ACCOUNT_IDP_USERNAME";
+
@Override
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
KeycloakSession session = context.getSession();
RealmModel realm = context.getRealm();
AuthenticationSessionModel authSession = context.getAuthenticationSession();
- if (realm.getSmtpConfig().size() == 0) {
+ if (realm.getSmtpConfig().isEmpty()) {
ServicesLogger.LOGGER.smtpNotConfigured();
context.attempted();
return;
}
-/*
- VerifyEmail.setupKey(clientSession);
- UserModel existingUser = getExistingUser(session, realm, clientSession);
+ if (Objects.equals(authSession.getAuthNote(VERIFY_ACCOUNT_IDP_USERNAME), brokerContext.getUsername())) {
+ UserModel existingUser = getExistingUser(session, realm, authSession);
- String link = UriBuilder.fromUri(context.getActionUrl())
- .queryParam(Constants.KEY, clientSession.getNote(Constants.VERIFY_EMAIL_KEY))
- .build().toString();
+ logger.debugf("User '%s' confirmed that wants to link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(),
+ brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername());
+
+ context.setUser(existingUser);
+ context.success();
+ return;
+ }
+
+ UserModel existingUser = getExistingUser(session, realm, authSession);
+
+ sendVerifyEmail(session, context, existingUser, brokerContext);
+ }
+
+ @Override
+ protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
+ logger.debugf("Re-sending email requested for user, details follow");
+
+ // This will allow user to re-send email again
+ context.getAuthenticationSession().removeAuthNote(VERIFY_ACCOUNT_IDP_USERNAME);
+ authenticateImpl(context, serializedCtx, brokerContext);
+ }
+
+ @Override
+ public boolean requiresUser() {
+ return false;
+ }
+
+ @Override
+ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+ return false;
+ }
+
+ private void sendVerifyEmail(KeycloakSession session, AuthenticationFlowContext context, UserModel existingUser, BrokeredIdentityContext brokerContext) throws UriBuilderException, IllegalArgumentException {
+ RealmModel realm = session.getContext().getRealm();
+ UriInfo uriInfo = session.getContext().getUri();
+ AuthenticationSessionModel authSession = context.getAuthenticationSession();
+
+ int validityInSecs = realm.getAccessCodeLifespanUserAction();
+ int absoluteExpirationInSecs = Time.currentTime() + validityInSecs;
EventBuilder event = context.getEvent().clone().event(EventType.SEND_IDENTITY_PROVIDER_LINK)
.user(existingUser)
.detail(Details.USERNAME, existingUser.getUsername())
.detail(Details.EMAIL, existingUser.getEmail())
- .detail(Details.CODE_ID, clientSession.getId())
+ .detail(Details.CODE_ID, authSession.getId())
.removeDetail(Details.AUTH_METHOD)
.removeDetail(Details.AUTH_TYPE);
- long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
- try {
+ IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
+ existingUser.getId(), absoluteExpirationInSecs, null, authSession.getId(),
+ brokerContext.getUsername(), brokerContext.getIdpConfig().getAlias()
+ );
+ UriBuilder builder = Urls.actionTokenBuilder(uriInfo.getBaseUri(), token.serialize(session, realm, uriInfo));
+ String link = builder.queryParam("execution", context.getExecution().getId()).build(realm.getName()).toString();
+ long expirationInMinutes = TimeUnit.SECONDS.toMinutes(validityInSecs);
+ try {
context.getSession().getProvider(EmailTemplateProvider.class)
.setRealm(realm)
.setUser(existingUser)
.setAttribute(EmailTemplateProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
- .sendConfirmIdentityBrokerLink(link, expiration);
+ .sendConfirmIdentityBrokerLink(link, expirationInMinutes);
event.success();
} catch (EmailException e) {
@@ -101,62 +146,14 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
return;
}
+ String accessCode = context.generateAccessCode();
+ URI action = context.getActionUrl(accessCode);
+
Response challenge = context.form()
.setStatus(Response.Status.OK)
.setAttribute(LoginFormsProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
+ .setActionUri(action)
.createIdpLinkEmailPage();
- context.forceChallenge(challenge);*/
- }
-
- @Override
- protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
- /*MultivaluedMap queryParams = context.getSession().getContext().getUri().getQueryParameters();
- String key = queryParams.getFirst(Constants.KEY);
- ClientSessionModel clientSession = context.getClientSession();
- RealmModel realm = context.getRealm();
- KeycloakSession session = context.getSession();
-
- if (key != null) {
- String keyFromSession = clientSession.getNote(Constants.VERIFY_EMAIL_KEY);
- clientSession.removeNote(Constants.VERIFY_EMAIL_KEY);
- if (key.equals(keyFromSession)) {
- UserModel existingUser = getExistingUser(session, realm, clientSession);
-
- logger.debugf("User '%s' confirmed that wants to link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(),
- brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername());
-
- String actionCookieValue = LoginActionsService.getActionCookie(session.getContext().getRequestHeaders(), realm, session.getContext().getUri(), context.getConnection());
- if (actionCookieValue == null || !actionCookieValue.equals(clientSession.getId())) {
- clientSession.setNote(IS_DIFFERENT_BROWSER, "true");
- }
-
- // User successfully confirmed linking by email verification. His email was defacto verified
- existingUser.setEmailVerified(true);
-
- context.setUser(existingUser);
- context.success();
- } else {
- ServicesLogger.LOGGER.keyParamDoesNotMatch();
- Response challengeResponse = context.form()
- .setError(Messages.INVALID_ACCESS_CODE)
- .createErrorPage();
- context.failureChallenge(AuthenticationFlowError.IDENTITY_PROVIDER_ERROR, challengeResponse);
- }
- } else {
- Response challengeResponse = context.form()
- .setError(Messages.MISSING_PARAMETER, Constants.KEY)
- .createErrorPage();
- context.failureChallenge(AuthenticationFlowError.IDENTITY_PROVIDER_ERROR, challengeResponse);
- }*/
- }
-
- @Override
- public boolean requiresUser() {
- return false;
- }
-
- @Override
- public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
- return false;
+ context.forceChallenge(challenge);
}
}
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
index f3ea22fd8d..ad84f9d3b8 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/VerifyEmail.java
@@ -31,7 +31,6 @@ import org.keycloak.events.EventType;
import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.*;
import org.keycloak.models.UserModel.RequiredAction;
-import org.keycloak.models.utils.HmacOTP;
import org.keycloak.services.Urls;
import org.keycloak.services.validation.Validation;
@@ -87,7 +86,7 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
@Override
public void processAction(RequiredActionContext context) {
- logger.infof("Re-sending email requested for user: %s", context.getUser().getUsername());
+ logger.debugf("Re-sending email requested for user: %s", context.getUser().getUsername());
// This will allow user to re-send email again
context.getAuthenticationSession().removeAuthNote(Constants.VERIFY_EMAIL_KEY);
@@ -152,9 +151,4 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
return forms.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
}
-
- public static void setupKey(AuthenticationSessionModel session) {
- String secret = HmacOTP.generateSecret(10);
- session.setAuthNote(Constants.VERIFY_EMAIL_KEY, secret);
- }
}
diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java
index edeac7692a..e92aa05a74 100755
--- a/services/src/main/java/org/keycloak/services/Urls.java
+++ b/services/src/main/java/org/keycloak/services/Urls.java
@@ -178,10 +178,6 @@ public class Urls {
return loginResetCredentialsBuilder(baseUri).build(realmName);
}
- public static UriBuilder executeActionsBuilder(URI baseUri) {
- return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActions");
- }
-
public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString) {
return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken")
.queryParam("key", tokenString);
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
index 5dcb621b66..758b7a1c02 100755
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java
@@ -67,7 +67,6 @@ import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
-import org.keycloak.services.resources.LoginActionsServiceChecks.RestartFlowException;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.AuthenticationFlowURLHelper;
import org.keycloak.services.util.BrowserHistoryHelper;
@@ -448,7 +447,6 @@ public class LoginActionsService {
.secretKey(session.keys().getActiveHmacKey(realm).getSecretKey())
.verify();
- // TODO:hmlnarik Optimize
token = TokenVerifier.create(tokenString, handler.getTokenClass()).getToken();
} catch (TokenNotActiveException ex) {
if (authSession != null) {
@@ -469,40 +467,32 @@ public class LoginActionsService {
}
// Now proceed with the verification and handle the token
- tokenContext = new ActionTokenContext(session, realm, uriInfo, clientConnection, request, event, handler);
+ tokenContext = new ActionTokenContext(session, realm, uriInfo, clientConnection, request, event, handler, execution, this::processFlow, this::brokerLoginFlow);
try {
- tokenContext.setExecutionId(execution);
-
String tokenAuthSessionId = handler.getAuthenticationSessionIdFromToken(token);
+
+ if (tokenAuthSessionId != null) {
+ // This can happen if the token contains ID but user opens the link in a new browser
+ LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, tokenAuthSessionId);
+ }
+
if (authSession == null) {
- if (tokenAuthSessionId != null) {
- // This can happen if the token contains ID but user opens the link in a new browser
- LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, tokenAuthSessionId);
- }
+ authSession = handler.startFreshAuthenticationSession(token, tokenContext);
+ tokenContext.setAuthenticationSession(authSession, true);
+ } else if (tokenAuthSessionId == null ||
+ ! LoginActionsServiceChecks.doesAuthenticationSessionFromCookieMatchOneFromToken(tokenContext, tokenAuthSessionId)) {
+ // 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);
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
tokenContext.setAuthenticationSession(authSession, true);
-
- initLoginEvent(authSession);
- event.event(handler.eventType());
- } else {
- initLoginEvent(authSession);
- event.event(handler.eventType());
-
- if (tokenAuthSessionId == null) {
- // There exists an authentication session but no auth session ID was received in the action token
- logger.debugf("Authentication session exists while reauthentication was requested by using action token %s, restarting.", token.getId());
- new AuthenticationSessionManager(session).removeAuthenticationSession(realm, authSession, false);
-
- authSession = handler.startFreshAuthenticationSession(token, tokenContext);
- tokenContext.setAuthenticationSession(authSession, true);
- } else {
- LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, tokenAuthSessionId);
- LoginActionsServiceChecks.checkAuthenticationSessionFromCookieMatchesOneFromToken(tokenContext, tokenAuthSessionId);
- }
}
+ initLoginEvent(authSession);
+ event.event(handler.eventType());
+
LoginActionsServiceChecks.checkIsUserValid(token, tokenContext);
LoginActionsServiceChecks.checkIsClientValid(token, tokenContext);
@@ -519,14 +509,9 @@ public class LoginActionsService {
authSession.setAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID, token.getUserId());
- return handler.handleToken(token, tokenContext, this::processFlow);
+ return handler.handleToken(token, tokenContext);
} catch (ExplainedTokenVerificationException ex) {
return handleActionTokenVerificationException(tokenContext, ex, ex.getErrorEvent(), ex.getMessage());
- } catch (RestartFlowException ex) {
- Response response = handler.handleRestartRequest(token, tokenContext, this::processFlow);
- return response == null
- ? handleActionTokenVerificationException(tokenContext, ex, eventError, defaultErrorMessage)
- : response;
} catch (LoginActionsServiceException ex) {
Response response = ex.getResponse();
return response == null
@@ -779,37 +764,6 @@ public class LoginActionsService {
return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, clientSession.getUserSession(), clientSession, request, uriInfo, clientConnection, event, authSession.getProtocol());
}
-
- /**
- * Initiated by admin, not the user on login
- *
- * @param key
- * @return
- */
- @Path("execute-actions")
- @GET
- public Response executeActions(@QueryParam("key") String key) {
- // TODO:mposolda
- /*
- event.event(EventType.EXECUTE_ACTIONS);
- if (key != null) {
- SessionCodeChecks checks = checksForCode(key);
- if (!checks.verifyCode(ClientSessionModel.Action.EXECUTE_ACTIONS.name(), ClientSessionCode.ActionType.USER)) {
- return checks.response;
- }
- ClientSessionModel clientSession = checks.getClientSession();
- // verify user email as we know it is valid as this entry point would never have gotten here.
- clientSession.getUserSession().getUser().setEmailVerified(true);
- clientSession.setNote(AuthenticationManager.END_AFTER_REQUIRED_ACTIONS, "true");
- clientSession.setNote(ClientSessionModel.Action.EXECUTE_ACTIONS.name(), "true");
- return AuthenticationProcessor.redirectToRequiredActions(session, realm, clientSession, uriInfo);
- } else {
- event.error(Errors.INVALID_CODE);
- return ErrorPage.error(session, Messages.INVALID_CODE);
- }*/
- return null;
- }
-
private void initLoginEvent(AuthenticationSessionModel authSession) {
String responseType = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
if (responseType == null) {
diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
index cabb1b65f4..6d42d255ff 100644
--- a/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
+++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsServiceChecks.java
@@ -44,11 +44,6 @@ public class LoginActionsServiceChecks {
private static final Logger LOG = Logger.getLogger(LoginActionsServiceChecks.class.getName());
- /**
- * Exception signalling that flow needs to be restarted because authentication session IDs from cookie and token do not match.
- */
- public static class RestartFlowException extends VerificationException { }
-
/**
* This check verifies that user ID (subject) from the token matches
* the one from the authentication session.
@@ -264,32 +259,32 @@ public class LoginActionsServiceChecks {
*
* @param
*/
- public static void checkAuthenticationSessionFromCookieMatchesOneFromToken(ActionTokenContext context, String authSessionIdFromToken) throws VerificationException {
+ public static boolean doesAuthenticationSessionFromCookieMatchOneFromToken(ActionTokenContext context, String authSessionIdFromToken) throws VerificationException {
if (authSessionIdFromToken == null) {
- throw new RestartFlowException();
+ return false;
}
AuthenticationSessionManager asm = new AuthenticationSessionManager(context.getSession());
String authSessionIdFromCookie = asm.getCurrentAuthenticationSessionId(context.getRealm());
if (authSessionIdFromCookie == null) {
- throw new RestartFlowException();
+ return false;
}
AuthenticationSessionModel authSessionFromCookie = context.getSession()
.authenticationSessions().getAuthenticationSession(context.getRealm(), authSessionIdFromCookie);
if (authSessionFromCookie == null) { // Cookie contains ID of expired auth session
- throw new RestartFlowException();
+ return false;
}
if (Objects.equals(authSessionIdFromCookie, authSessionIdFromToken)) {
context.setAuthenticationSession(authSessionFromCookie, false);
- return;
+ return true;
}
String parentSessionId = authSessionFromCookie.getAuthNote(AuthenticationProcessor.FORKED_FROM);
if (parentSessionId == null || ! Objects.equals(authSessionIdFromToken, parentSessionId)) {
- throw new RestartFlowException();
+ return false;
}
AuthenticationSessionModel authSessionFromParent = context.getSession()
@@ -299,12 +294,14 @@ public class LoginActionsServiceChecks {
// 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.infof("Removed forked session: %s", authSessionFromCookie.getId());
+ LOG.debugf("Removed forked session: %s", authSessionFromCookie.getId());
// Refresh browser cookie
asm.setAuthSessionCookie(parentSessionId, context.getRealm());
context.setAuthenticationSession(authSessionFromParent, false);
context.setExecutionId(authSessionFromParent.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION));
+
+ return true;
}
}
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory
index 246758dfdb..2a5b9ec3e5 100644
--- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.actiontoken.ActionTokenHandlerFactory
@@ -1,3 +1,4 @@
org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler
org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionTokenHandler
org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionTokenHandler
+org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionTokenHandler
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 7f26d742b3..f34c32017f 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -21,9 +21,7 @@ import org.hamcrest.Matchers;
import org.jboss.arquillian.drone.api.annotation.Drone;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.arquillian.test.api.ArquillianResource;
-import org.junit.After;
import org.junit.Assert;
-import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.resource.IdentityProviderResource;
@@ -62,7 +60,6 @@ import org.openqa.selenium.WebDriver;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.ws.rs.ClientErrorException;
-import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java
index f2dffaf016..ba7bb65694 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java
@@ -52,6 +52,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -298,6 +300,147 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
Assert.assertTrue(user.isEmailVerified());
}
+ /**
+ * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by email
+ */
+ @Test
+ public void testLinkAccountByEmailVerificationTwice() throws Exception {
+ setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF);
+
+ loginIDP("pedroigor");
+
+ 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();
+
+ // Confirm linking account by email
+ this.idpLinkEmailPage.assertCurrent();
+ Assert.assertThat(
+ this.idpLinkEmailPage.getMessage(),
+ is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.")
+ );
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+ String linkFromMail = getVerificationEmailLink(message);
+
+ driver.navigate().to(linkFromMail.trim());
+
+ // authenticated and redirected to app. User is linked with identity provider
+ assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
+
+ // Assert user's email is verified now
+ UserModel user = getFederatedUser();
+ Assert.assertTrue(user.isEmailVerified());
+
+ // Attempt to use the link for the second time
+ driver.navigate().to(linkFromMail.trim());
+
+ infoPage.assertCurrent();
+ Assert.assertThat(infoPage.getInfo(), is("You are already logged in."));
+
+ // Log out
+ driver.navigate().to("http://localhost:8081/test-app/logout");
+
+ // Go to the same link again
+ driver.navigate().to(linkFromMail.trim());
+
+ infoPage.assertCurrent();
+ Assert.assertThat(infoPage.getInfo(), startsWith("Your account was successfully linked with " + getProviderId() + " account pedroigor"));
+ }
+
+ /**
+ * Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by email
+ */
+ @Test
+ public void testLinkAccountByEmailVerificationDifferentBrowser() throws Exception, Throwable {
+ setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF);
+
+ loginIDP("pedroigor");
+
+ 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();
+
+ // Confirm linking account by email
+ this.idpLinkEmailPage.assertCurrent();
+ Assert.assertThat(
+ this.idpLinkEmailPage.getMessage(),
+ is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.")
+ );
+
+ Assert.assertEquals(1, greenMail.getReceivedMessages().length);
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+ String linkFromMail = getVerificationEmailLink(message);
+
+ WebRule webRule2 = new WebRule(this);
+ try {
+ webRule2.initProperties();
+
+ WebDriver driver2 = webRule2.getDriver();
+ InfoPage infoPage2 = webRule2.getPage(InfoPage.class);
+
+ driver2.navigate().to(linkFromMail.trim());
+
+ // authenticated, but not redirected to app. Just seeing info page.
+ infoPage2.assertCurrent();
+ Assert.assertThat(infoPage2.getInfo(), startsWith("Your account was successfully linked with " + getProviderId() + " account pedroigor"));
+ } finally {
+ // Revert everything
+ webRule2.after();
+ }
+
+ driver.navigate().refresh();
+ this.loginExpiredPage.assertCurrent();
+ this.loginExpiredPage.clickLoginContinueLink();
+
+ // authenticated and redirected to app. User is linked with identity provider
+ assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
+
+ // Assert user's email is verified now
+ UserModel user = getFederatedUser();
+ Assert.assertTrue(user.isEmailVerified());
+ }
+
+ @Test
+ public void testLinkAccountByEmailVerificationResendEmail() throws Exception, Throwable {
+ setUpdateProfileFirstLogin(IdentityProviderRepresentation.UPFLM_OFF);
+
+ loginIDP("pedroigor");
+
+ 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();
+
+ // Confirm linking account by email
+ this.idpLinkEmailPage.assertCurrent();
+ Assert.assertThat(
+ this.idpLinkEmailPage.getMessage(),
+ is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.")
+ );
+
+ this.idpLinkEmailPage.clickResendEmail();
+
+ this.idpLinkEmailPage.assertCurrent();
+ Assert.assertThat(
+ this.idpLinkEmailPage.getMessage(),
+ is("An email with instructions to link " + ObjectUtil.capitalize(getProviderId()) + " account pedroigor with your " + APP_REALM_ID + " account has been sent to you.")
+ );
+
+ Assert.assertEquals(2, greenMail.getReceivedMessages().length);
+ MimeMessage message = greenMail.getReceivedMessages()[0];
+ String linkFromMail = getVerificationEmailLink(message);
+
+ driver.navigate().to(linkFromMail.trim());
+
+ // authenticated and redirected to app. User is linked with identity provider
+ assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
+
+ // Assert user's email is verified now
+ UserModel user = getFederatedUser();
+ Assert.assertTrue(user.isEmailVerified());
+ }
+
/**
* Tests that duplication is detected and user wants to link federatedIdentity with existing account. He will confirm link by reauthentication (confirm password on login screen)
@@ -557,29 +700,35 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
// Simulate 2nd browser
WebRule webRule2 = new WebRule(this);
- webRule2.before();
+ try {
+ webRule2.initProperties();
- WebDriver driver2 = webRule2.getDriver();
- LoginPasswordUpdatePage passwordUpdatePage2 = webRule2.getPage(LoginPasswordUpdatePage.class);
- InfoPage infoPage2 = webRule2.getPage(InfoPage.class);
+ WebDriver driver2 = webRule2.getDriver();
+ LoginPasswordUpdatePage passwordUpdatePage2 = webRule2.getPage(LoginPasswordUpdatePage.class);
+ InfoPage infoPage2 = webRule2.getPage(InfoPage.class);
- driver2.navigate().to(linkFromMail.trim());
+ driver2.navigate().to(linkFromMail.trim());
- // Need to update password now
- passwordUpdatePage2.assertCurrent();
- passwordUpdatePage2.changePassword("password", "password");
+ // Need to update password now
+ passwordUpdatePage2.assertCurrent();
+ passwordUpdatePage2.changePassword("password", "password");
- // authenticated, but not redirected to app. Just seeing info page.
- infoPage2.assertCurrent();
- Assert.assertEquals("Your account has been updated.", infoPage2.getInfo());
+ // authenticated, but not redirected to app. Just seeing info page.
+ infoPage2.assertCurrent();
+ Assert.assertEquals("Your account has been updated.", infoPage2.getInfo());
+ } finally {
+ // Revert everything
+ webRule2.after();
+ }
// User is not yet linked with identity provider. He needs to authenticate again in 1st browser
RealmModel realmWithBroker = getRealm();
Set federatedIdentities = this.session.users().getFederatedIdentities(this.session.users().getUserByUsername("pedroigor", realmWithBroker), realmWithBroker);
assertEquals(0, federatedIdentities.size());
- // Continue with 1st browser
- loginIDP("pedroigor");
+ // Continue with 1st browser. Note that the user has already authenticated with brokered IdP in the beginning of this test
+ // so entering their credentials there is now skipped.
+ loginToIDPWhenAlreadyLoggedIntoProviderIdP("pedroigor");
this.idpConfirmLinkPage.assertCurrent();
Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
@@ -591,9 +740,6 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
// authenticated and redirected to app. User is linked with identity provider
assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
- // Revert everything
- webRule2.after();
-
brokerServerRule.update(new KeycloakRule.KeycloakSetup() {
@Override
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
index 8efc8c0ba3..297d00a55b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
@@ -37,14 +37,7 @@ import org.keycloak.testsuite.MailUtil;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.broker.util.UserSessionStatusServlet;
import org.keycloak.testsuite.broker.util.UserSessionStatusServlet.UserSessionStatus;
-import org.keycloak.testsuite.pages.AccountFederatedIdentityPage;
-import org.keycloak.testsuite.pages.AccountPasswordPage;
-import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
-import org.keycloak.testsuite.pages.ErrorPage;
-import org.keycloak.testsuite.pages.LoginPage;
-import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
-import org.keycloak.testsuite.pages.OAuthGrantPage;
-import org.keycloak.testsuite.pages.VerifyEmailPage;
+import org.keycloak.testsuite.pages.*;
import org.keycloak.testsuite.rule.GreenMailRule;
import org.keycloak.testsuite.rule.LoggingRule;
import org.keycloak.testsuite.rule.WebResource;
@@ -61,9 +54,8 @@ import java.net.URI;
import java.util.List;
import java.util.Set;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.hamcrest.Matchers.startsWith;
+import static org.junit.Assert.*;
/**
* @author pedroigor
@@ -115,6 +107,9 @@ public abstract class AbstractIdentityProviderTest {
@WebResource
protected ErrorPage errorPage;
+ @WebResource
+ protected InfoPage infoPage;
+
protected KeycloakSession session;
protected int logoutTimeOffset = 0;
@@ -210,18 +205,29 @@ public abstract class AbstractIdentityProviderTest {
protected void loginIDP(String username) {
driver.navigate().to("http://localhost:8081/test-app");
- assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
+ assertThat(this.driver.getCurrentUrl(), startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
// choose the identity provider
this.loginPage.clickSocial(getProviderId());
String currentUrl = this.driver.getCurrentUrl();
- assertTrue(currentUrl.startsWith("http://localhost:8082/auth/"));
+ assertThat(currentUrl, startsWith("http://localhost:8082/auth/"));
// log in to identity provider
this.loginPage.login(username, "password");
doAfterProviderAuthentication();
}
+ protected void loginToIDPWhenAlreadyLoggedIntoProviderIdP(String username) {
+ driver.navigate().to("http://localhost:8081/test-app");
+
+ assertThat(this.driver.getCurrentUrl(), startsWith("http://localhost:8081/auth/realms/realm-with-broker/protocol/openid-connect/auth"));
+
+ // choose the identity provider
+ this.loginPage.clickSocial(getProviderId());
+
+ doAfterProviderAuthentication();
+ }
+
protected UserModel getFederatedUser() {
UserSessionStatus userSessionStatus = retrieveSessionStatus();
IDToken idToken = userSessionStatus.getIdToken();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java
index 200b0a7c60..234617b482 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/SAMLFirstBrokerLoginTest.java
@@ -23,6 +23,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
+import org.junit.Test;
/**
* @author Marek Posolda
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java
index 8ed8461070..22eb1560ba 100644
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/IdpLinkEmailPage.java
@@ -28,11 +28,22 @@ public class IdpLinkEmailPage extends AbstractPage {
@FindBy(id = "instruction1")
private WebElement message;
+ @FindBy(linkText = "Click here")
+ private WebElement resendEmailLink;
+
@Override
public boolean isCurrent() {
return driver.getTitle().startsWith("Link ");
}
+ public void clickResendEmail() {
+ resendEmailLink.click();
+ }
+
+ public String getResendEmailLink() {
+ return resendEmailLink.getAttribute("href");
+ }
+
@Override
public void open() throws Exception {
throw new UnsupportedOperationException();
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
index 2cea40a7cd..0d93d19a21 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/WebRule.java
@@ -40,10 +40,14 @@ public class WebRule extends ExternalResource {
this.test = test;
}
- @Override
- public void before() throws Throwable {
+ public void initProperties() {
driver = createWebDriver();
oauth = new OAuthClient(driver);
+ }
+
+ @Override
+ public void before() throws Throwable {
+ initProperties();
initWebResources(test);
}
@@ -58,6 +62,7 @@ public class WebRule extends ExternalResource {
HtmlUnitDriver d = new HtmlUnitDriver();
d.getWebClient().getOptions().setJavaScriptEnabled(true);
d.getWebClient().getOptions().setCssEnabled(false);
+ d.getWebClient().getOptions().setTimeout(1000000);
driver = d;
} else if (browser.equals("chrome")) {
driver = new ChromeDriver();
diff --git a/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl b/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl
index 5dc29f1c11..9cca544ae7 100644
--- a/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl
+++ b/themes/src/main/resources/theme/base/login/login-idp-link-email.ftl
@@ -9,7 +9,7 @@
${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
- ${msg("emailLinkIdp2")} ${msg("doClickHere")} ${msg("emailLinkIdp3")}
+ ${msg("emailLinkIdp2")} ${msg("doClickHere")} ${msg("emailLinkIdp3")}
#if>
@layout.registrationLayout>
\ No newline at end of file