KEYCLOAK-4288 Use SessionListener to keep track of local HTTP-SSO session mappings

This commit is contained in:
Hynek Mlnarik committed 2017-04-28 12:37:17 +02:00
1 parent 7d8197b74e
commit cd47fd1f69
8 files changed
+287 -13

No files matched your search

@@ -0,0 +1,103 @@
/*
* 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.adapters.saml.undertow;
import org.keycloak.adapters.saml.SamlSession;
import org.keycloak.adapters.spi.SessionIdMapper;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.Session;
import io.undertow.server.session.SessionListener;
import java.util.Objects;
/**
*
* @author hmlnarik
*/
public class IdMapperUpdaterSessionListener implements SessionListener {
private final SessionIdMapper idMapper;
public IdMapperUpdaterSessionListener(SessionIdMapper idMapper) {
this.idMapper = idMapper;
}
@Override
public void sessionCreated(Session session, HttpServerExchange exchange) {
Object value = session.getAttribute(SamlSession.class.getName());
map(session.getId(), value);
}
@Override
public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) {
if (reason != SessionDestroyedReason.UNDEPLOY) {
unmap(session.getId(), session.getAttribute(SamlSession.class.getName()));
}
}
@Override
public void attributeAdded(Session session, String name, Object value) {
if (Objects.equals(name, SamlSession.class.getName())) {
map(session.getId(), value);
}
}
@Override
public void attributeUpdated(Session session, String name, Object newValue, Object oldValue) {
if (Objects.equals(name, SamlSession.class.getName())) {
unmap(session.getId(), oldValue);
map(session.getId(), newValue);
}
}
@Override
public void attributeRemoved(Session session, String name, Object oldValue) {
if (Objects.equals(name, SamlSession.class.getName())) {
unmap(session.getId(), oldValue);
}
}
@Override
public void sessionIdChanged(Session session, String oldSessionId) {
Object value = session.getAttribute(SamlSession.class.getName());
if (value != null) {
unmap(oldSessionId, value);
map(session.getId(), value);
}
}
private void map(String sessionId, Object value) {
if (! (value instanceof SamlSession) || sessionId == null) {
return;
}
SamlSession account = (SamlSession) value;
idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId);
}
private void unmap(String sessionId, Object value) {
if (! (value instanceof SamlSession) || sessionId == null) {
return;
}
SamlSession samlSession = (SamlSession) value;
if (samlSession.getSessionIndex() != null) {
idMapper.removeSession(sessionId);
}
}
}
@@ -154,7 +154,7 @@ public class SamlServletExtension implements ServletExtension {
servletContext.setAttribute(SamlDeploymentContext.class.getName(), deploymentContext);
UndertowUserSessionManagement userSessionManagement = new UndertowUserSessionManagement();
final ServletSamlAuthMech mech = createAuthMech(deploymentInfo, deploymentContext, userSessionManagement);
mech.addTokenStoreUpdaters(deploymentInfo);
// setup handlers
@@ -21,35 +21,89 @@ import io.undertow.security.api.SecurityContext;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.handlers.ServletRequestContext;
import io.undertow.util.Headers;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.SamlDeploymentContext;
import org.keycloak.adapters.saml.SamlSessionStore;
import org.keycloak.adapters.spi.HttpFacade;
import org.keycloak.adapters.spi.InMemorySessionIdMapper;
import org.keycloak.adapters.spi.SessionIdMapper;
import org.keycloak.adapters.spi.*;
import org.keycloak.adapters.undertow.ServletHttpFacade;
import org.keycloak.adapters.undertow.UndertowHttpFacade;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
import io.undertow.servlet.api.DeploymentInfo;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.lang.reflect.*;
import java.util.Map;
import org.jboss.logging.Logger;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ServletSamlAuthMech extends AbstractSamlAuthMech {
private static final Logger LOG = Logger.getLogger(ServletSamlAuthMech.class);
protected SessionIdMapper idMapper = new InMemorySessionIdMapper();
protected SessionIdMapperUpdater idMapperUpdater = SessionIdMapperUpdater.DIRECT;
public ServletSamlAuthMech(SamlDeploymentContext deploymentContext, UndertowUserSessionManagement sessionManagement, String errorPage) {
super(deploymentContext, sessionManagement, errorPage);
}
public void addTokenStoreUpdaters(DeploymentInfo deploymentInfo) {
deploymentInfo.addSessionListener(new IdMapperUpdaterSessionListener(idMapper)); // This takes care of HTTP sessions manipulated locally
SessionIdMapperUpdater updater = SessionIdMapperUpdater.EXTERNAL;
try {
Map<String, String> initParameters = deploymentInfo.getInitParameters();
String idMapperSessionUpdaterClasses = initParameters == null
? null
: initParameters.get("keycloak.sessionIdMapperUpdater.classes");
if (idMapperSessionUpdaterClasses == null) {
return;
}
for (String clazz : idMapperSessionUpdaterClasses.split("\\s*,\\s*")) {
if (! clazz.isEmpty()) {
updater = invokeAddTokenStoreUpdaterMethod(clazz, deploymentInfo, updater);
}
}
} finally {
setIdMapperUpdater(updater);
}
}
private SessionIdMapperUpdater invokeAddTokenStoreUpdaterMethod(String idMapperSessionUpdaterClass, DeploymentInfo deploymentInfo,
SessionIdMapperUpdater previousIdMapperUpdater) {
try {
Class<?> clazz = deploymentInfo.getClassLoader().loadClass(idMapperSessionUpdaterClass);
Method addTokenStoreUpdatersMethod = clazz.getMethod("addTokenStoreUpdaters", DeploymentInfo.class, SessionIdMapper.class, SessionIdMapperUpdater.class);
if (! Modifier.isStatic(addTokenStoreUpdatersMethod.getModifiers())
|| ! Modifier.isPublic(addTokenStoreUpdatersMethod.getModifiers())
|| ! SessionIdMapperUpdater.class.isAssignableFrom(addTokenStoreUpdatersMethod.getReturnType())) {
LOG.errorv("addTokenStoreUpdaters method in class {0} has to be public static. Ignoring class.", idMapperSessionUpdaterClass);
return previousIdMapperUpdater;
}
LOG.debugv("Initializing sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass);
return (SessionIdMapperUpdater) addTokenStoreUpdatersMethod.invoke(null, deploymentInfo, idMapper, previousIdMapperUpdater);
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException ex) {
LOG.warnv(ex, "Cannot use sessionIdMapperUpdater class {0}", idMapperSessionUpdaterClass);
return previousIdMapperUpdater;
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
LOG.warnv(ex, "Cannot use {0}.addTokenStoreUpdaters(DeploymentInfo, SessionIdMapper) method", idMapperSessionUpdaterClass);
return previousIdMapperUpdater;
}
}
@Override
protected SamlSessionStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, SamlDeployment deployment, SecurityContext securityContext) {
return new ServletSamlSessionStore(exchange, sessionManagement, securityContext, idMapper, deployment);
return new ServletSamlSessionStore(exchange, sessionManagement, securityContext, idMapper, idMapperUpdater, deployment);
}
@Override
@@ -84,5 +138,11 @@ public class ServletSamlAuthMech extends AbstractSamlAuthMech {
return null;
}
public SessionIdMapperUpdater getIdMapperUpdater() {
return idMapperUpdater;
}
protected void setIdMapperUpdater(SessionIdMapperUpdater idMapperUpdater) {
this.idMapperUpdater = idMapperUpdater;
}
}
@@ -24,11 +24,13 @@ import io.undertow.server.session.SessionManager;
import io.undertow.servlet.handlers.ServletRequestContext;
import io.undertow.servlet.spec.HttpSessionImpl;
import org.jboss.logging.Logger;
import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.SamlSession;
import org.keycloak.adapters.saml.SamlSessionStore;
import org.keycloak.adapters.saml.SamlUtil;
import org.keycloak.adapters.spi.SessionIdMapper;
import org.keycloak.adapters.spi.SessionIdMapperUpdater;
import org.keycloak.adapters.undertow.ChangeSessionId;
import org.keycloak.adapters.undertow.SavedRequest;
import org.keycloak.adapters.undertow.ServletHttpFacade;
@@ -44,6 +46,8 @@ import java.util.List;
import java.util.Set;
/**
* Session store manipulation methods per single HTTP exchange.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@@ -55,17 +59,20 @@ public class ServletSamlSessionStore implements SamlSessionStore {
private final UndertowUserSessionManagement sessionManagement;
private final SecurityContext securityContext;
private final SessionIdMapper idMapper;
private final SessionIdMapperUpdater idMapperUpdater;
protected final SamlDeployment deployment;
public ServletSamlSessionStore(HttpServerExchange exchange, UndertowUserSessionManagement sessionManagement,
SecurityContext securityContext,
SessionIdMapper idMapper, SamlDeployment deployment) {
SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater,
SamlDeployment deployment) {
this.exchange = exchange;
this.sessionManagement = sessionManagement;
this.securityContext = securityContext;
this.idMapper = idMapper;
this.deployment = deployment;
this.idMapperUpdater = idMapperUpdater;
}
@Override
@@ -97,7 +104,7 @@ public class ServletSamlSessionStore implements SamlSessionStore {
SamlSession samlSession = (SamlSession)session.getAttribute(SamlSession.class.getName());
if (samlSession != null) {
if (samlSession.getSessionIndex() != null) {
idMapper.removeSession(session.getId());
idMapperUpdater.removeSession(idMapper, session.getId());
}
session.removeAttribute(SamlSession.class.getName());
}
@@ -113,7 +120,7 @@ public class ServletSamlSessionStore implements SamlSessionStore {
ids.addAll(sessions);
logoutSessionIds(ids);
for (String id : ids) {
idMapper.removeSession(id);
idMapperUpdater.removeSession(idMapper, id);
}
}
@@ -127,7 +134,7 @@ public class ServletSamlSessionStore implements SamlSessionStore {
String sessionId = idMapper.getSessionFromSSO(id);
if (sessionId != null) {
sessionIds.add(sessionId);
idMapper.removeSession(sessionId);
idMapperUpdater.removeSession(idMapper, sessionId);
}
}
@@ -177,7 +184,7 @@ public class ServletSamlSessionStore implements SamlSessionStore {
session.setAttribute(SamlSession.class.getName(), account);
sessionManagement.login(servletRequestContext.getDeployment().getSessionManager());
String sessionId = changeSessionId(session);
idMapper.map(account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId);
idMapperUpdater.map(idMapper, account.getSessionIndex(), account.getPrincipal().getSamlSubject(), sessionId);
}
@@ -37,6 +37,6 @@ public class WildflySamlAuthMech extends ServletSamlAuthMech {
@Override
protected SamlSessionStore getTokenStore(HttpServerExchange exchange, HttpFacade facade, SamlDeployment deployment, SecurityContext securityContext) {
return new WildflySamlSessionStore(exchange, sessionManagement, securityContext, idMapper, deployment);
return new WildflySamlSessionStore(exchange, sessionManagement, securityContext, idMapper, getIdMapperUpdater(), deployment);
}
}
@@ -23,6 +23,7 @@ import org.keycloak.adapters.saml.SamlDeployment;
import org.keycloak.adapters.saml.SamlSession;
import org.keycloak.adapters.saml.undertow.ServletSamlSessionStore;
import org.keycloak.adapters.spi.SessionIdMapper;
import org.keycloak.adapters.spi.SessionIdMapperUpdater;
import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
/**
@@ -31,8 +32,10 @@ import org.keycloak.adapters.undertow.UndertowUserSessionManagement;
*/
public class WildflySamlSessionStore extends ServletSamlSessionStore {
public WildflySamlSessionStore(HttpServerExchange exchange, UndertowUserSessionManagement sessionManagement,
SecurityContext securityContext, SessionIdMapper idMapper, SamlDeployment resolvedDeployment) {
super(exchange, sessionManagement, securityContext, idMapper, resolvedDeployment);
SecurityContext securityContext,
SessionIdMapper idMapper, SessionIdMapperUpdater idMapperUpdater,
SamlDeployment resolvedDeployment) {
super(exchange, sessionManagement, securityContext, idMapper, idMapperUpdater, resolvedDeployment);
}
@Override
@@ -24,15 +24,43 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public interface SessionIdMapper {
/**
* Returns {@code true} if the mapper contains mapping for the given HTTP session ID.
* @param id
* @return
*/
boolean hasSession(String id);
/**
* Clears all mappings from this mapper.
*/
void clear();
/**
* Returns set of HTTP session IDs for the given principal.
* @param principal Principal
* @return
*/
Set<String> getUserSessions(String principal);
/**
* Returns HTTP session ID from the given user session ID.
* @param sso User session ID
* @return
*/
String getSessionFromSSO(String sso);
/**
* Establishes mapping between user session ID, principal and HTTP session ID.
* @param sso User session ID
* @param principal Principal
* @param session HTTP session ID
*/
void map(String sso, String principal, String session);
/**
* Removes mappings for the given HTTP session ID.
* @param session HTTP session ID.
*/
void removeSession(String session);
}
@@ -0,0 +1,73 @@
/*
* 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.adapters.spi;
/**
* Classes implementing this interface represent a mechanism for updating {@link SessionIdMapper} entries.
* @author hmlnarik
*/
public interface SessionIdMapperUpdater {
/**
* {@link SessionIdMapper} entries are updated directly.
*/
public static final SessionIdMapperUpdater DIRECT = new SessionIdMapperUpdater() {
@Override public void clear(SessionIdMapper idMapper) {
idMapper.clear();
}
@Override public void map(SessionIdMapper idMapper, String sso, String principal, String httpSessionId) {
idMapper.map(sso, principal, httpSessionId);
}
@Override public void removeSession(SessionIdMapper idMapper, String httpSessionId) {
idMapper.removeSession(httpSessionId);
}
};
/**
* Only HTTP session is manipulated with, {@link SessionIdMapper} entries are not updated by this updater and
* they have to be updated by some other means, e.g. by some listener of HTTP session changes.
*/
public static final SessionIdMapperUpdater EXTERNAL = new SessionIdMapperUpdater() {
@Override public void clear(SessionIdMapper idMapper) { }
@Override public void map(SessionIdMapper idMapper, String sso, String principal, String httpSessionId) { }
@Override public void removeSession(SessionIdMapper idMapper, String httpSessionId) { }
};
/**
* Delegates to {@link SessionIdMapper#clear} method..
*/
public abstract void clear(SessionIdMapper idMapper);
/**
* Delegates to {@link SessionIdMapper#map} method.
* @param idMapper Mapper
* @param sso User session ID
* @param principal Principal
* @param session HTTP session ID
*/
public abstract void map(SessionIdMapper idMapper, String sso, String principal, String session);
/**
* Delegates to {@link SessionIdMapper#removeSession} method.
* @param idMapper Mapper
* @param session HTTP session ID.
*/
public abstract void removeSession(SessionIdMapper idMapper, String session);
}