KEYCLOAK-4627 IdP email account verification + code cleanup. Fix for concurrent access to auth session notes
This commit is contained in:
parent
168153c6e7
commit
c431cc1b01
28 changed files with 685 additions and 258 deletions
|
@ -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<String, String> authNotesFragment;
|
||||
|
||||
public static AuthenticationSessionAuthNoteUpdateEvent create(String authSessionId, Map<String, String> authNotesFragment) {
|
||||
AuthenticationSessionAuthNoteUpdateEvent event = new AuthenticationSessionAuthNoteUpdateEvent();
|
||||
event.authSessionId = authSessionId;
|
||||
event.authNotesFragment = new LinkedHashMap<>(authNotesFragment);
|
||||
return event;
|
||||
}
|
||||
|
||||
public String getAuthSessionId() {
|
||||
return authSessionId;
|
||||
}
|
||||
|
||||
public Map<String, String> getAuthNotesFragment() {
|
||||
return authNotesFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("AuthenticationSessionAuthNoteUpdateEvent [ authSessionId=%s, authNotesFragment=%s ]", authSessionId, authNotesFragment);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String, String> getClientNotes() {
|
||||
if (entity.getClientNotes() == null || entity.getClientNotes().isEmpty()) return Collections.emptyMap();
|
||||
Map<String, String> copy = new HashMap<>();
|
||||
Map<String, String> 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<String, String>());
|
||||
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<String, String>());
|
||||
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<String, String> copy = new HashMap<>();
|
||||
ConcurrentHashMap<String, String> copy = new ConcurrentHashMap<>();
|
||||
copy.putAll(entity.getUserSessionNotes());
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearUserSessionNotes() {
|
||||
entity.setUserSessionNotes(new HashMap<String, String>());
|
||||
entity.setUserSessionNotes(new ConcurrentHashMap<>());
|
||||
update();
|
||||
|
||||
}
|
||||
|
|
|
@ -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<String, String> 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() {
|
||||
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class InfinispanAuthenticationSessionProviderFactory implements AuthenticationSessionProviderFactory {
|
||||
|
||||
private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProviderFactory.class);
|
||||
|
||||
private volatile Cache<String, AuthenticationSessionEntity> 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<String, AuthenticationSessionEntity> 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<String, String> authNotesFragment) {
|
||||
if (authSession != null) {
|
||||
if (authSession.getAuthNotes() == null) {
|
||||
authSession.setAuthNotes(new ConcurrentHashMap<>());
|
||||
}
|
||||
|
||||
for (Entry<String, String> 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) {
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -23,4 +23,6 @@ import org.keycloak.provider.ProviderFactory;
|
|||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface AuthenticationSessionProviderFactory extends ProviderFactory<AuthenticationSessionProvider> {
|
||||
// TODO:hmlnarik: move this constant out of an interface into a more appropriate class
|
||||
public static final String AUTHENTICATION_SESSION_EVENTS = "AUTHENTICATION_SESSION_EVENTS";
|
||||
}
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -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<String, String> authNotesFragment);
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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<T extends JsonWebToken> {
|
||||
|
||||
@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<T extends JsonWebToken> {
|
|||
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<T> handler) {
|
||||
public ActionTokenContext(KeycloakSession session, RealmModel realm, UriInfo uriInfo,
|
||||
ClientConnection clientConnection, HttpRequest request,
|
||||
EventBuilder event, ActionTokenHandler<T> handler, String executionId,
|
||||
ProcessAuthenticateFlow processFlow, ProcessBrokerFlow processBrokerFlow) {
|
||||
this.session = session;
|
||||
this.realm = realm;
|
||||
this.uriInfo = uriInfo;
|
||||
|
@ -54,6 +72,9 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
|||
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<T extends JsonWebToken> {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,21 +35,6 @@ import javax.ws.rs.core.Response;
|
|||
*/
|
||||
public interface ActionTokenHandler<T extends JsonWebToken> 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<T> 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<T extends JsonWebToken> extends Provider {
|
|||
* @return
|
||||
* @throws VerificationException
|
||||
*/
|
||||
Response handleToken(T token, ActionTokenContext<T> tokenContext, ProcessFlow processFlow);
|
||||
Response handleToken(T token, ActionTokenContext<T> tokenContext);
|
||||
|
||||
/**
|
||||
* Returns the Java token class for use with deserialization.
|
||||
|
@ -67,6 +52,16 @@ public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
|
|||
*/
|
||||
Class<T> 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<T> 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<T extends JsonWebToken> 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<T> 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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<ExecuteActionsActionToken> tokenContext, ProcessFlow processFlow) {
|
||||
public Response handleToken(ExecuteActionsActionToken token, ActionTokenContext<ExecuteActionsActionToken> tokenContext) {
|
||||
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
|
||||
|
||||
String redirectUri = RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), token.getRedirectUri(),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<IdpVerifyAccountLinkActionToken> {
|
||||
|
||||
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<IdpVerifyAccountLinkActionToken> tokenContext) {
|
||||
return TokenUtils.predicates(
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response handleToken(IdpVerifyAccountLinkActionToken token, ActionTokenContext<IdpVerifyAccountLinkActionToken> 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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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<ResetCredentialsActionToken> 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
|
||||
|
|
|
@ -57,7 +57,7 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
|
|||
}
|
||||
|
||||
@Override
|
||||
public Response handleToken(VerifyEmailActionToken token, ActionTokenContext<VerifyEmailActionToken> tokenContext, ProcessFlow processFlow) {
|
||||
public Response handleToken(VerifyEmailActionToken token, ActionTokenContext<VerifyEmailActionToken> tokenContext) {
|
||||
UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
|
||||
EventBuilder event = tokenContext.getEvent();
|
||||
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -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<String, String> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 <T>
|
||||
*/
|
||||
public static <T extends JsonWebToken> void checkAuthenticationSessionFromCookieMatchesOneFromToken(ActionTokenContext<T> context, String authSessionIdFromToken) throws VerificationException {
|
||||
public static <T extends JsonWebToken> boolean doesAuthenticationSessionFromCookieMatchOneFromToken(ActionTokenContext<T> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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<FederatedIdentityModel> 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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
|
||||
</p>
|
||||
<p id="instruction2" class="instruction">
|
||||
${msg("emailLinkIdp2")} <a href="${url.firstBrokerLoginUrl}">${msg("doClickHere")}</a> ${msg("emailLinkIdp3")}
|
||||
${msg("emailLinkIdp2")} <a href="${url.loginAction}">${msg("doClickHere")}</a> ${msg("emailLinkIdp3")}
|
||||
</p>
|
||||
</#if>
|
||||
</@layout.registrationLayout>
|
Loading…
Reference in a new issue