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;
|
package org.keycloak.models.sessions.infinispan;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -144,21 +144,27 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getClientNote(String name) {
|
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
|
@Override
|
||||||
public void setClientNote(String name, String value) {
|
public void setClientNote(String name, String value) {
|
||||||
if (entity.getClientNotes() == null) {
|
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();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeClientNote(String name) {
|
public void removeClientNote(String name) {
|
||||||
if (entity.getClientNotes() != null) {
|
if (entity.getClientNotes() != null && name != null) {
|
||||||
entity.getClientNotes().remove(name);
|
entity.getClientNotes().remove(name);
|
||||||
}
|
}
|
||||||
update();
|
update();
|
||||||
|
@ -167,34 +173,40 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
||||||
@Override
|
@Override
|
||||||
public Map<String, String> getClientNotes() {
|
public Map<String, String> getClientNotes() {
|
||||||
if (entity.getClientNotes() == null || entity.getClientNotes().isEmpty()) return Collections.emptyMap();
|
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());
|
copy.putAll(entity.getClientNotes());
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearClientNotes() {
|
public void clearClientNotes() {
|
||||||
entity.setClientNotes(new HashMap<>());
|
entity.setClientNotes(new ConcurrentHashMap<>());
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getAuthNote(String name) {
|
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
|
@Override
|
||||||
public void setAuthNote(String name, String value) {
|
public void setAuthNote(String name, String value) {
|
||||||
if (entity.getAuthNotes() == null) {
|
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();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeAuthNote(String name) {
|
public void removeAuthNote(String name) {
|
||||||
if (entity.getAuthNotes() != null) {
|
if (entity.getAuthNotes() != null && name != null) {
|
||||||
entity.getAuthNotes().remove(name);
|
entity.getAuthNotes().remove(name);
|
||||||
}
|
}
|
||||||
update();
|
update();
|
||||||
|
@ -202,16 +214,22 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearAuthNotes() {
|
public void clearAuthNotes() {
|
||||||
entity.setAuthNotes(new HashMap<>());
|
entity.setAuthNotes(new ConcurrentHashMap<>());
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setUserSessionNote(String name, String value) {
|
public void setUserSessionNote(String name, String value) {
|
||||||
if (entity.getUserSessionNotes() == null) {
|
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();
|
update();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -221,14 +239,14 @@ public class AuthenticationSessionAdapter implements AuthenticationSessionModel
|
||||||
if (entity.getUserSessionNotes() == null) {
|
if (entity.getUserSessionNotes() == null) {
|
||||||
return Collections.EMPTY_MAP;
|
return Collections.EMPTY_MAP;
|
||||||
}
|
}
|
||||||
HashMap<String, String> copy = new HashMap<>();
|
ConcurrentHashMap<String, String> copy = new ConcurrentHashMap<>();
|
||||||
copy.putAll(entity.getUserSessionNotes());
|
copy.putAll(entity.getUserSessionNotes());
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clearUserSessionNotes() {
|
public void clearUserSessionNotes() {
|
||||||
entity.setUserSessionNotes(new HashMap<String, String>());
|
entity.setUserSessionNotes(new ConcurrentHashMap<>());
|
||||||
update();
|
update();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.keycloak.models.sessions.infinispan;
|
package org.keycloak.models.sessions.infinispan;
|
||||||
|
|
||||||
|
import org.keycloak.cluster.ClusterProvider;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -27,6 +28,7 @@ import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate;
|
import org.keycloak.models.sessions.infinispan.stream.AuthenticationSessionPredicate;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
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
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
|
|
||||||
|
|
|
@ -19,18 +19,28 @@ package org.keycloak.models.sessions.infinispan;
|
||||||
|
|
||||||
import org.infinispan.Cache;
|
import org.infinispan.Cache;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.cluster.ClusterEvent;
|
||||||
|
import org.keycloak.cluster.ClusterProvider;
|
||||||
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.cache.infinispan.events.AuthenticationSessionAuthNoteUpdateEvent;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.AuthenticationSessionEntity;
|
||||||
import org.keycloak.sessions.AuthenticationSessionProvider;
|
import org.keycloak.sessions.AuthenticationSessionProvider;
|
||||||
import org.keycloak.sessions.AuthenticationSessionProviderFactory;
|
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>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class InfinispanAuthenticationSessionProviderFactory implements AuthenticationSessionProviderFactory {
|
public class InfinispanAuthenticationSessionProviderFactory implements AuthenticationSessionProviderFactory {
|
||||||
|
|
||||||
|
private static final Logger log = Logger.getLogger(InfinispanAuthenticationSessionProviderFactory.class);
|
||||||
|
|
||||||
|
private volatile Cache<String, AuthenticationSessionEntity> authSessionsCache;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
@ -39,12 +49,53 @@ public class InfinispanAuthenticationSessionProviderFactory implements Authentic
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticationSessionProvider create(KeycloakSession session) {
|
public AuthenticationSessionProvider create(KeycloakSession session) {
|
||||||
InfinispanConnectionProvider connections = session.getProvider(InfinispanConnectionProvider.class);
|
lazyInit(session);
|
||||||
Cache<String, AuthenticationSessionEntity> authSessionsCache = connections.getCache(InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME);
|
|
||||||
|
|
||||||
return new InfinispanAuthenticationSessionProvider(session, authSessionsCache);
|
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
|
@Override
|
||||||
public void postInit(KeycloakSessionFactory factory) {
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,6 +92,8 @@ public enum EventType {
|
||||||
USER_INFO_REQUEST(false),
|
USER_INFO_REQUEST(false),
|
||||||
USER_INFO_REQUEST_ERROR(false),
|
USER_INFO_REQUEST_ERROR(false),
|
||||||
|
|
||||||
|
IDENTITY_PROVIDER_LINK_ACCOUNT(true),
|
||||||
|
IDENTITY_PROVIDER_LINK_ACCOUNT_ERROR(true),
|
||||||
IDENTITY_PROVIDER_LOGIN(false),
|
IDENTITY_PROVIDER_LOGIN(false),
|
||||||
IDENTITY_PROVIDER_LOGIN_ERROR(false),
|
IDENTITY_PROVIDER_LOGIN_ERROR(false),
|
||||||
IDENTITY_PROVIDER_FIRST_LOGIN(true),
|
IDENTITY_PROVIDER_FIRST_LOGIN(true),
|
||||||
|
@ -129,6 +131,10 @@ public enum EventType {
|
||||||
this.saveByDefault = saveByDefault;
|
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() {
|
public boolean isSaveByDefault() {
|
||||||
return saveByDefault;
|
return saveByDefault;
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,4 +23,6 @@ import org.keycloak.provider.ProviderFactory;
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public interface AuthenticationSessionProviderFactory extends ProviderFactory<AuthenticationSessionProvider> {
|
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.ClientModel;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
@ -39,5 +40,14 @@ public interface AuthenticationSessionProvider extends Provider {
|
||||||
void onRealmRemoved(RealmModel realm);
|
void onRealmRemoved(RealmModel realm);
|
||||||
void onClientRemoved(RealmModel realm, ClientModel client);
|
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;
|
package org.keycloak.authentication.actiontoken;
|
||||||
|
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.models.*;
|
import org.keycloak.models.*;
|
||||||
|
@ -25,6 +26,8 @@ import org.keycloak.representations.JsonWebToken;
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
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.UriBuilderException;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
@ -35,6 +38,16 @@ import org.jboss.resteasy.spi.HttpRequest;
|
||||||
*/
|
*/
|
||||||
public class ActionTokenContext<T extends JsonWebToken> {
|
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 KeycloakSession session;
|
||||||
private final RealmModel realm;
|
private final RealmModel realm;
|
||||||
private final UriInfo uriInfo;
|
private final UriInfo uriInfo;
|
||||||
|
@ -45,8 +58,13 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
||||||
private AuthenticationSessionModel authenticationSession;
|
private AuthenticationSessionModel authenticationSession;
|
||||||
private boolean authenticationSessionFresh;
|
private boolean authenticationSessionFresh;
|
||||||
private String executionId;
|
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.session = session;
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
this.uriInfo = uriInfo;
|
this.uriInfo = uriInfo;
|
||||||
|
@ -54,6 +72,9 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
||||||
this.request = request;
|
this.request = request;
|
||||||
this.event = event;
|
this.event = event;
|
||||||
this.handler = handler;
|
this.handler = handler;
|
||||||
|
this.executionId = executionId;
|
||||||
|
this.processAuthenticateFlow = processFlow;
|
||||||
|
this.processBrokerFlow = processBrokerFlow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventBuilder getEvent() {
|
public EventBuilder getEvent() {
|
||||||
|
@ -131,4 +152,12 @@ public class ActionTokenContext<T extends JsonWebToken> {
|
||||||
public void setExecutionId(String executionId) {
|
public void setExecutionId(String executionId) {
|
||||||
this.executionId = 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 {
|
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
|
* Performs the action as per the token details. This method is only called if all verifiers
|
||||||
* returned in {@link #handleToken} succeed.
|
* returned in {@link #handleToken} succeed.
|
||||||
|
@ -59,7 +44,7 @@ public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
|
||||||
* @return
|
* @return
|
||||||
* @throws VerificationException
|
* @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.
|
* Returns the Java token class for use with deserialization.
|
||||||
|
@ -67,6 +52,16 @@ public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
|
||||||
*/
|
*/
|
||||||
Class<T> getTokenClass();
|
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
|
* Returns an authentication session ID requested from within the given token
|
||||||
* @param token Token. Can be {@code null}
|
* @param token Token. Can be {@code null}
|
||||||
|
@ -95,17 +90,8 @@ public interface ActionTokenHandler<T extends JsonWebToken> extends Provider {
|
||||||
String getDefaultErrorMessage();
|
String getDefaultErrorMessage();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a response that restarts a flow that this action token initiates, or {@code null} if
|
* Creates a fresh authentication session according to the information from the token. The default
|
||||||
* no special handling is requested.
|
* implementation creates a new authentication session that requests termination after required actions.
|
||||||
*
|
|
||||||
* @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.
|
|
||||||
* @param token
|
* @param token
|
||||||
* @param tokenContext
|
* @param tokenContext
|
||||||
* @return
|
* @return
|
||||||
|
|
|
@ -25,7 +25,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
*/
|
*/
|
||||||
public class DefaultActionTokenKey extends JsonWebToken {
|
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 static final String ACTION_TOKEN_USER_ID = "ACTION_TOKEN_USER";
|
||||||
|
|
||||||
public DefaultActionTokenKey(String userId, String actionId) {
|
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.protocol.oidc.utils.RedirectUtils;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.LoginActionsServiceChecks;
|
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.CommonClientSessionModel.Action;
|
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,7 +59,7 @@ public class ExecuteActionsActionTokenHandler extends AbstractActionTokenHander<
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response handleToken(ExecuteActionsActionToken token, ActionTokenContext<ExecuteActionsActionToken> tokenContext, ProcessFlow processFlow) {
|
public Response handleToken(ExecuteActionsActionToken token, ActionTokenContext<ExecuteActionsActionToken> tokenContext) {
|
||||||
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
|
AuthenticationSessionModel authSession = tokenContext.getAuthenticationSession();
|
||||||
|
|
||||||
String redirectUri = RedirectUtils.verifyRedirectUri(tokenContext.getUriInfo(), token.getRedirectUri(),
|
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
|
@Override
|
||||||
public Response handleToken(ResetCredentialsActionToken token, ActionTokenContext tokenContext, ProcessFlow processFlow) {
|
public Response handleToken(ResetCredentialsActionToken token, ActionTokenContext tokenContext) {
|
||||||
AuthenticationProcessor authProcessor = new ResetCredsAuthenticationProcessor();
|
AuthenticationProcessor authProcessor = new ResetCredsAuthenticationProcessor();
|
||||||
|
|
||||||
return processFlow.processFlow(
|
return tokenContext.processFlow(
|
||||||
false,
|
false,
|
||||||
tokenContext.getExecutionId(),
|
|
||||||
tokenContext.getAuthenticationSession(),
|
|
||||||
RESET_CREDENTIALS_PATH,
|
RESET_CREDENTIALS_PATH,
|
||||||
tokenContext.getRealm().getResetCredentialsFlow(),
|
tokenContext.getRealm().getResetCredentialsFlow(),
|
||||||
null,
|
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 {
|
public static class ResetCredsAuthenticationProcessor extends AuthenticationProcessor {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -57,7 +57,7 @@ public class VerifyEmailActionTokenHandler extends AbstractActionTokenHander<Ver
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Response handleToken(VerifyEmailActionToken token, ActionTokenContext<VerifyEmailActionToken> tokenContext, ProcessFlow processFlow) {
|
public Response handleToken(VerifyEmailActionToken token, ActionTokenContext<VerifyEmailActionToken> tokenContext) {
|
||||||
UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
|
UserModel user = tokenContext.getAuthenticationSession().getAuthenticatedUser();
|
||||||
EventBuilder event = tokenContext.getEvent();
|
EventBuilder event = tokenContext.getEvent();
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,10 @@ package org.keycloak.authentication.authenticators.broker;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
import org.keycloak.authentication.AuthenticationFlowError;
|
import org.keycloak.authentication.AuthenticationFlowError;
|
||||||
|
import org.keycloak.authentication.actiontoken.idpverifyemail.IdpVerifyAccountLinkActionToken;
|
||||||
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
import org.keycloak.authentication.authenticators.broker.util.SerializedBrokeredIdentityContext;
|
||||||
import org.keycloak.authentication.requiredactions.VerifyEmail;
|
|
||||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
import org.keycloak.email.EmailException;
|
import org.keycloak.email.EmailException;
|
||||||
import org.keycloak.email.EmailTemplateProvider;
|
import org.keycloak.email.EmailTemplateProvider;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
|
@ -30,20 +31,21 @@ import org.keycloak.events.Errors;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.ClientSessionModel;
|
|
||||||
import org.keycloak.models.Constants;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.services.ServicesLogger;
|
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.messages.Messages;
|
||||||
import org.keycloak.services.resources.LoginActionsService;
|
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
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.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import javax.ws.rs.core.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @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);
|
private static Logger logger = Logger.getLogger(IdpEmailVerificationAuthenticator.class);
|
||||||
|
|
||||||
|
public static final String VERIFY_ACCOUNT_IDP_USERNAME = "VERIFY_ACCOUNT_IDP_USERNAME";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
|
protected void authenticateImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext serializedCtx, BrokeredIdentityContext brokerContext) {
|
||||||
KeycloakSession session = context.getSession();
|
KeycloakSession session = context.getSession();
|
||||||
RealmModel realm = context.getRealm();
|
RealmModel realm = context.getRealm();
|
||||||
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
AuthenticationSessionModel authSession = context.getAuthenticationSession();
|
||||||
|
|
||||||
if (realm.getSmtpConfig().size() == 0) {
|
if (realm.getSmtpConfig().isEmpty()) {
|
||||||
ServicesLogger.LOGGER.smtpNotConfigured();
|
ServicesLogger.LOGGER.smtpNotConfigured();
|
||||||
context.attempted();
|
context.attempted();
|
||||||
return;
|
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())
|
logger.debugf("User '%s' confirmed that wants to link with identity provider '%s' . Identity provider username is '%s' ", existingUser.getUsername(),
|
||||||
.queryParam(Constants.KEY, clientSession.getNote(Constants.VERIFY_EMAIL_KEY))
|
brokerContext.getIdpConfig().getAlias(), brokerContext.getUsername());
|
||||||
.build().toString();
|
|
||||||
|
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)
|
EventBuilder event = context.getEvent().clone().event(EventType.SEND_IDENTITY_PROVIDER_LINK)
|
||||||
.user(existingUser)
|
.user(existingUser)
|
||||||
.detail(Details.USERNAME, existingUser.getUsername())
|
.detail(Details.USERNAME, existingUser.getUsername())
|
||||||
.detail(Details.EMAIL, existingUser.getEmail())
|
.detail(Details.EMAIL, existingUser.getEmail())
|
||||||
.detail(Details.CODE_ID, clientSession.getId())
|
.detail(Details.CODE_ID, authSession.getId())
|
||||||
.removeDetail(Details.AUTH_METHOD)
|
.removeDetail(Details.AUTH_METHOD)
|
||||||
.removeDetail(Details.AUTH_TYPE);
|
.removeDetail(Details.AUTH_TYPE);
|
||||||
|
|
||||||
long expiration = TimeUnit.SECONDS.toMinutes(context.getRealm().getAccessCodeLifespanUserAction());
|
IdpVerifyAccountLinkActionToken token = new IdpVerifyAccountLinkActionToken(
|
||||||
try {
|
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)
|
context.getSession().getProvider(EmailTemplateProvider.class)
|
||||||
.setRealm(realm)
|
.setRealm(realm)
|
||||||
.setUser(existingUser)
|
.setUser(existingUser)
|
||||||
.setAttribute(EmailTemplateProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
|
.setAttribute(EmailTemplateProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
|
||||||
.sendConfirmIdentityBrokerLink(link, expiration);
|
.sendConfirmIdentityBrokerLink(link, expirationInMinutes);
|
||||||
|
|
||||||
event.success();
|
event.success();
|
||||||
} catch (EmailException e) {
|
} catch (EmailException e) {
|
||||||
|
@ -101,62 +146,14 @@ public class IdpEmailVerificationAuthenticator extends AbstractIdpAuthenticator
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String accessCode = context.generateAccessCode();
|
||||||
|
URI action = context.getActionUrl(accessCode);
|
||||||
|
|
||||||
Response challenge = context.form()
|
Response challenge = context.form()
|
||||||
.setStatus(Response.Status.OK)
|
.setStatus(Response.Status.OK)
|
||||||
.setAttribute(LoginFormsProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
|
.setAttribute(LoginFormsProvider.IDENTITY_PROVIDER_BROKER_CONTEXT, brokerContext)
|
||||||
|
.setActionUri(action)
|
||||||
.createIdpLinkEmailPage();
|
.createIdpLinkEmailPage();
|
||||||
context.forceChallenge(challenge);*/
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,6 @@ import org.keycloak.events.EventType;
|
||||||
import org.keycloak.forms.login.LoginFormsProvider;
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.*;
|
import org.keycloak.models.*;
|
||||||
import org.keycloak.models.UserModel.RequiredAction;
|
import org.keycloak.models.UserModel.RequiredAction;
|
||||||
import org.keycloak.models.utils.HmacOTP;
|
|
||||||
import org.keycloak.services.Urls;
|
import org.keycloak.services.Urls;
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.services.validation.Validation;
|
||||||
|
|
||||||
|
@ -87,7 +86,7 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void processAction(RequiredActionContext context) {
|
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
|
// This will allow user to re-send email again
|
||||||
context.getAuthenticationSession().removeAuthNote(Constants.VERIFY_EMAIL_KEY);
|
context.getAuthenticationSession().removeAuthNote(Constants.VERIFY_EMAIL_KEY);
|
||||||
|
@ -152,9 +151,4 @@ public class VerifyEmail implements RequiredActionProvider, RequiredActionFactor
|
||||||
|
|
||||||
return forms.createResponse(UserModel.RequiredAction.VERIFY_EMAIL);
|
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);
|
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) {
|
public static UriBuilder actionTokenBuilder(URI baseUri, String tokenString) {
|
||||||
return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken")
|
return loginActionsBase(baseUri).path(LoginActionsService.class, "executeActionToken")
|
||||||
.queryParam("key", tokenString);
|
.queryParam("key", tokenString);
|
||||||
|
|
|
@ -67,7 +67,6 @@ import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.managers.AuthenticationSessionManager;
|
import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
import org.keycloak.services.managers.ClientSessionCode;
|
import org.keycloak.services.managers.ClientSessionCode;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.LoginActionsServiceChecks.RestartFlowException;
|
|
||||||
import org.keycloak.services.util.CacheControlUtil;
|
import org.keycloak.services.util.CacheControlUtil;
|
||||||
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
import org.keycloak.services.util.AuthenticationFlowURLHelper;
|
||||||
import org.keycloak.services.util.BrowserHistoryHelper;
|
import org.keycloak.services.util.BrowserHistoryHelper;
|
||||||
|
@ -448,7 +447,6 @@ public class LoginActionsService {
|
||||||
.secretKey(session.keys().getActiveHmacKey(realm).getSecretKey())
|
.secretKey(session.keys().getActiveHmacKey(realm).getSecretKey())
|
||||||
.verify();
|
.verify();
|
||||||
|
|
||||||
// TODO:hmlnarik Optimize
|
|
||||||
token = TokenVerifier.create(tokenString, handler.getTokenClass()).getToken();
|
token = TokenVerifier.create(tokenString, handler.getTokenClass()).getToken();
|
||||||
} catch (TokenNotActiveException ex) {
|
} catch (TokenNotActiveException ex) {
|
||||||
if (authSession != null) {
|
if (authSession != null) {
|
||||||
|
@ -469,40 +467,32 @@ public class LoginActionsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now proceed with the verification and handle the token
|
// 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 {
|
try {
|
||||||
tokenContext.setExecutionId(execution);
|
|
||||||
|
|
||||||
String tokenAuthSessionId = handler.getAuthenticationSessionIdFromToken(token);
|
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 (authSession == null) {
|
||||||
if (tokenAuthSessionId != null) {
|
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
|
||||||
// This can happen if the token contains ID but user opens the link in a new browser
|
tokenContext.setAuthenticationSession(authSession, true);
|
||||||
LoginActionsServiceChecks.checkNotLoggedInYet(tokenContext, tokenAuthSessionId);
|
} 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);
|
authSession = handler.startFreshAuthenticationSession(token, tokenContext);
|
||||||
tokenContext.setAuthenticationSession(authSession, true);
|
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.checkIsUserValid(token, tokenContext);
|
||||||
LoginActionsServiceChecks.checkIsClientValid(token, tokenContext);
|
LoginActionsServiceChecks.checkIsClientValid(token, tokenContext);
|
||||||
|
|
||||||
|
@ -519,14 +509,9 @@ public class LoginActionsService {
|
||||||
|
|
||||||
authSession.setAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID, token.getUserId());
|
authSession.setAuthNote(DefaultActionTokenKey.ACTION_TOKEN_USER_ID, token.getUserId());
|
||||||
|
|
||||||
return handler.handleToken(token, tokenContext, this::processFlow);
|
return handler.handleToken(token, tokenContext);
|
||||||
} catch (ExplainedTokenVerificationException ex) {
|
} catch (ExplainedTokenVerificationException ex) {
|
||||||
return handleActionTokenVerificationException(tokenContext, ex, ex.getErrorEvent(), ex.getMessage());
|
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) {
|
} catch (LoginActionsServiceException ex) {
|
||||||
Response response = ex.getResponse();
|
Response response = ex.getResponse();
|
||||||
return response == null
|
return response == null
|
||||||
|
@ -779,37 +764,6 @@ public class LoginActionsService {
|
||||||
return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, clientSession.getUserSession(), clientSession, request, uriInfo, clientConnection, event, authSession.getProtocol());
|
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) {
|
private void initLoginEvent(AuthenticationSessionModel authSession) {
|
||||||
String responseType = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
|
String responseType = authSession.getClientNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
|
||||||
if (responseType == null) {
|
if (responseType == null) {
|
||||||
|
|
|
@ -44,11 +44,6 @@ public class LoginActionsServiceChecks {
|
||||||
|
|
||||||
private static final Logger LOG = Logger.getLogger(LoginActionsServiceChecks.class.getName());
|
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
|
* This check verifies that user ID (subject) from the token matches
|
||||||
* the one from the authentication session.
|
* the one from the authentication session.
|
||||||
|
@ -264,32 +259,32 @@ public class LoginActionsServiceChecks {
|
||||||
*
|
*
|
||||||
* @param <T>
|
* @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) {
|
if (authSessionIdFromToken == null) {
|
||||||
throw new RestartFlowException();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationSessionManager asm = new AuthenticationSessionManager(context.getSession());
|
AuthenticationSessionManager asm = new AuthenticationSessionManager(context.getSession());
|
||||||
String authSessionIdFromCookie = asm.getCurrentAuthenticationSessionId(context.getRealm());
|
String authSessionIdFromCookie = asm.getCurrentAuthenticationSessionId(context.getRealm());
|
||||||
|
|
||||||
if (authSessionIdFromCookie == null) {
|
if (authSessionIdFromCookie == null) {
|
||||||
throw new RestartFlowException();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationSessionModel authSessionFromCookie = context.getSession()
|
AuthenticationSessionModel authSessionFromCookie = context.getSession()
|
||||||
.authenticationSessions().getAuthenticationSession(context.getRealm(), authSessionIdFromCookie);
|
.authenticationSessions().getAuthenticationSession(context.getRealm(), authSessionIdFromCookie);
|
||||||
if (authSessionFromCookie == null) { // Cookie contains ID of expired auth session
|
if (authSessionFromCookie == null) { // Cookie contains ID of expired auth session
|
||||||
throw new RestartFlowException();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Objects.equals(authSessionIdFromCookie, authSessionIdFromToken)) {
|
if (Objects.equals(authSessionIdFromCookie, authSessionIdFromToken)) {
|
||||||
context.setAuthenticationSession(authSessionFromCookie, false);
|
context.setAuthenticationSession(authSessionFromCookie, false);
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String parentSessionId = authSessionFromCookie.getAuthNote(AuthenticationProcessor.FORKED_FROM);
|
String parentSessionId = authSessionFromCookie.getAuthNote(AuthenticationProcessor.FORKED_FROM);
|
||||||
if (parentSessionId == null || ! Objects.equals(authSessionIdFromToken, parentSessionId)) {
|
if (parentSessionId == null || ! Objects.equals(authSessionIdFromToken, parentSessionId)) {
|
||||||
throw new RestartFlowException();
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AuthenticationSessionModel authSessionFromParent = context.getSession()
|
AuthenticationSessionModel authSessionFromParent = context.getSession()
|
||||||
|
@ -299,12 +294,14 @@ public class LoginActionsServiceChecks {
|
||||||
// from the login form (browser flow) but from the token's flow
|
// from the login form (browser flow) but from the token's flow
|
||||||
// Don't expire KC_RESTART cookie at this point
|
// Don't expire KC_RESTART cookie at this point
|
||||||
asm.removeAuthenticationSession(context.getRealm(), authSessionFromCookie, false);
|
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
|
// Refresh browser cookie
|
||||||
asm.setAuthSessionCookie(parentSessionId, context.getRealm());
|
asm.setAuthSessionCookie(parentSessionId, context.getRealm());
|
||||||
|
|
||||||
context.setAuthenticationSession(authSessionFromParent, false);
|
context.setAuthenticationSession(authSessionFromParent, false);
|
||||||
context.setExecutionId(authSessionFromParent.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION));
|
context.setExecutionId(authSessionFromParent.getAuthNote(AuthenticationProcessor.LAST_PROCESSED_EXECUTION));
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler
|
org.keycloak.authentication.actiontoken.resetcred.ResetCredentialsActionTokenHandler
|
||||||
org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionTokenHandler
|
org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionTokenHandler
|
||||||
org.keycloak.authentication.actiontoken.verifyemail.VerifyEmailActionTokenHandler
|
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.drone.api.annotation.Drone;
|
||||||
import org.jboss.arquillian.graphene.page.Page;
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.jboss.arquillian.test.api.ArquillianResource;
|
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.admin.client.resource.IdentityProviderResource;
|
import org.keycloak.admin.client.resource.IdentityProviderResource;
|
||||||
|
@ -62,7 +60,6 @@ import org.openqa.selenium.WebDriver;
|
||||||
import javax.mail.MessagingException;
|
import javax.mail.MessagingException;
|
||||||
import javax.mail.internet.MimeMessage;
|
import javax.mail.internet.MimeMessage;
|
||||||
import javax.ws.rs.ClientErrorException;
|
import javax.ws.rs.ClientErrorException;
|
||||||
import javax.ws.rs.NotFoundException;
|
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
|
@ -52,6 +52,8 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
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.assertEquals;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
@ -298,6 +300,147 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractIdentityProvi
|
||||||
Assert.assertTrue(user.isEmailVerified());
|
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)
|
* 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
|
// Simulate 2nd browser
|
||||||
WebRule webRule2 = new WebRule(this);
|
WebRule webRule2 = new WebRule(this);
|
||||||
webRule2.before();
|
try {
|
||||||
|
webRule2.initProperties();
|
||||||
|
|
||||||
WebDriver driver2 = webRule2.getDriver();
|
WebDriver driver2 = webRule2.getDriver();
|
||||||
LoginPasswordUpdatePage passwordUpdatePage2 = webRule2.getPage(LoginPasswordUpdatePage.class);
|
LoginPasswordUpdatePage passwordUpdatePage2 = webRule2.getPage(LoginPasswordUpdatePage.class);
|
||||||
InfoPage infoPage2 = webRule2.getPage(InfoPage.class);
|
InfoPage infoPage2 = webRule2.getPage(InfoPage.class);
|
||||||
|
|
||||||
driver2.navigate().to(linkFromMail.trim());
|
driver2.navigate().to(linkFromMail.trim());
|
||||||
|
|
||||||
// Need to update password now
|
// Need to update password now
|
||||||
passwordUpdatePage2.assertCurrent();
|
passwordUpdatePage2.assertCurrent();
|
||||||
passwordUpdatePage2.changePassword("password", "password");
|
passwordUpdatePage2.changePassword("password", "password");
|
||||||
|
|
||||||
// authenticated, but not redirected to app. Just seeing info page.
|
// authenticated, but not redirected to app. Just seeing info page.
|
||||||
infoPage2.assertCurrent();
|
infoPage2.assertCurrent();
|
||||||
Assert.assertEquals("Your account has been updated.", infoPage2.getInfo());
|
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
|
// User is not yet linked with identity provider. He needs to authenticate again in 1st browser
|
||||||
RealmModel realmWithBroker = getRealm();
|
RealmModel realmWithBroker = getRealm();
|
||||||
Set<FederatedIdentityModel> federatedIdentities = this.session.users().getFederatedIdentities(this.session.users().getUserByUsername("pedroigor", realmWithBroker), realmWithBroker);
|
Set<FederatedIdentityModel> federatedIdentities = this.session.users().getFederatedIdentities(this.session.users().getUserByUsername("pedroigor", realmWithBroker), realmWithBroker);
|
||||||
assertEquals(0, federatedIdentities.size());
|
assertEquals(0, federatedIdentities.size());
|
||||||
|
|
||||||
// Continue with 1st browser
|
// Continue with 1st browser. Note that the user has already authenticated with brokered IdP in the beginning of this test
|
||||||
loginIDP("pedroigor");
|
// so entering their credentials there is now skipped.
|
||||||
|
loginToIDPWhenAlreadyLoggedIntoProviderIdP("pedroigor");
|
||||||
|
|
||||||
this.idpConfirmLinkPage.assertCurrent();
|
this.idpConfirmLinkPage.assertCurrent();
|
||||||
Assert.assertEquals("User with email psilva@redhat.com already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage());
|
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
|
// authenticated and redirected to app. User is linked with identity provider
|
||||||
assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
|
assertFederatedUser("pedroigor", "psilva@redhat.com", "pedroigor");
|
||||||
|
|
||||||
// Revert everything
|
|
||||||
webRule2.after();
|
|
||||||
|
|
||||||
brokerServerRule.update(new KeycloakRule.KeycloakSetup() {
|
brokerServerRule.update(new KeycloakRule.KeycloakSetup() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -37,14 +37,7 @@ import org.keycloak.testsuite.MailUtil;
|
||||||
import org.keycloak.testsuite.OAuthClient;
|
import org.keycloak.testsuite.OAuthClient;
|
||||||
import org.keycloak.testsuite.broker.util.UserSessionStatusServlet;
|
import org.keycloak.testsuite.broker.util.UserSessionStatusServlet;
|
||||||
import org.keycloak.testsuite.broker.util.UserSessionStatusServlet.UserSessionStatus;
|
import org.keycloak.testsuite.broker.util.UserSessionStatusServlet.UserSessionStatus;
|
||||||
import org.keycloak.testsuite.pages.AccountFederatedIdentityPage;
|
import org.keycloak.testsuite.pages.*;
|
||||||
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.rule.GreenMailRule;
|
import org.keycloak.testsuite.rule.GreenMailRule;
|
||||||
import org.keycloak.testsuite.rule.LoggingRule;
|
import org.keycloak.testsuite.rule.LoggingRule;
|
||||||
import org.keycloak.testsuite.rule.WebResource;
|
import org.keycloak.testsuite.rule.WebResource;
|
||||||
|
@ -61,9 +54,8 @@ import java.net.URI;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.hamcrest.Matchers.startsWith;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.*;
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author pedroigor
|
* @author pedroigor
|
||||||
|
@ -115,6 +107,9 @@ public abstract class AbstractIdentityProviderTest {
|
||||||
@WebResource
|
@WebResource
|
||||||
protected ErrorPage errorPage;
|
protected ErrorPage errorPage;
|
||||||
|
|
||||||
|
@WebResource
|
||||||
|
protected InfoPage infoPage;
|
||||||
|
|
||||||
protected KeycloakSession session;
|
protected KeycloakSession session;
|
||||||
|
|
||||||
protected int logoutTimeOffset = 0;
|
protected int logoutTimeOffset = 0;
|
||||||
|
@ -210,18 +205,29 @@ public abstract class AbstractIdentityProviderTest {
|
||||||
protected void loginIDP(String username) {
|
protected void loginIDP(String username) {
|
||||||
driver.navigate().to("http://localhost:8081/test-app");
|
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
|
// choose the identity provider
|
||||||
this.loginPage.clickSocial(getProviderId());
|
this.loginPage.clickSocial(getProviderId());
|
||||||
|
|
||||||
String currentUrl = this.driver.getCurrentUrl();
|
String currentUrl = this.driver.getCurrentUrl();
|
||||||
assertTrue(currentUrl.startsWith("http://localhost:8082/auth/"));
|
assertThat(currentUrl, startsWith("http://localhost:8082/auth/"));
|
||||||
// log in to identity provider
|
// log in to identity provider
|
||||||
this.loginPage.login(username, "password");
|
this.loginPage.login(username, "password");
|
||||||
doAfterProviderAuthentication();
|
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() {
|
protected UserModel getFederatedUser() {
|
||||||
UserSessionStatus userSessionStatus = retrieveSessionStatus();
|
UserSessionStatus userSessionStatus = retrieveSessionStatus();
|
||||||
IDToken idToken = userSessionStatus.getIdToken();
|
IDToken idToken = userSessionStatus.getIdToken();
|
||||||
|
|
|
@ -23,6 +23,7 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.testsuite.KeycloakServer;
|
import org.keycloak.testsuite.KeycloakServer;
|
||||||
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
|
import org.keycloak.testsuite.rule.AbstractKeycloakRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
|
|
@ -28,11 +28,22 @@ public class IdpLinkEmailPage extends AbstractPage {
|
||||||
@FindBy(id = "instruction1")
|
@FindBy(id = "instruction1")
|
||||||
private WebElement message;
|
private WebElement message;
|
||||||
|
|
||||||
|
@FindBy(linkText = "Click here")
|
||||||
|
private WebElement resendEmailLink;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isCurrent() {
|
public boolean isCurrent() {
|
||||||
return driver.getTitle().startsWith("Link ");
|
return driver.getTitle().startsWith("Link ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void clickResendEmail() {
|
||||||
|
resendEmailLink.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getResendEmailLink() {
|
||||||
|
return resendEmailLink.getAttribute("href");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void open() throws Exception {
|
public void open() throws Exception {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
|
|
|
@ -40,10 +40,14 @@ public class WebRule extends ExternalResource {
|
||||||
this.test = test;
|
this.test = test;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void initProperties() {
|
||||||
public void before() throws Throwable {
|
|
||||||
driver = createWebDriver();
|
driver = createWebDriver();
|
||||||
oauth = new OAuthClient(driver);
|
oauth = new OAuthClient(driver);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void before() throws Throwable {
|
||||||
|
initProperties();
|
||||||
initWebResources(test);
|
initWebResources(test);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +62,7 @@ public class WebRule extends ExternalResource {
|
||||||
HtmlUnitDriver d = new HtmlUnitDriver();
|
HtmlUnitDriver d = new HtmlUnitDriver();
|
||||||
d.getWebClient().getOptions().setJavaScriptEnabled(true);
|
d.getWebClient().getOptions().setJavaScriptEnabled(true);
|
||||||
d.getWebClient().getOptions().setCssEnabled(false);
|
d.getWebClient().getOptions().setCssEnabled(false);
|
||||||
|
d.getWebClient().getOptions().setTimeout(1000000);
|
||||||
driver = d;
|
driver = d;
|
||||||
} else if (browser.equals("chrome")) {
|
} else if (browser.equals("chrome")) {
|
||||||
driver = new ChromeDriver();
|
driver = new ChromeDriver();
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
|
${msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
|
||||||
</p>
|
</p>
|
||||||
<p id="instruction2" class="instruction">
|
<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>
|
</p>
|
||||||
</#if>
|
</#if>
|
||||||
</@layout.registrationLayout>
|
</@layout.registrationLayout>
|
Loading…
Reference in a new issue