diff --git a/src/main/java/tomcat/request/session/data/cache/DataCache.java b/src/main/java/tomcat/request/session/data/cache/DataCache.java index 84d4a81..701f424 100644 --- a/src/main/java/tomcat/request/session/data/cache/DataCache.java +++ b/src/main/java/tomcat/request/session/data/cache/DataCache.java @@ -45,4 +45,12 @@ public interface DataCache { * @return - Returns the number of keys that were removed. */ Long delete(String key); + + /** + * Check the key exists in data-cache. + * + * @param key - key with which the specified value is to be associated. + * @return - Returns true, if the key exists. + */ + Boolean exists(String key); } diff --git a/src/main/java/tomcat/request/session/data/cache/impl/StandardDataCache.java b/src/main/java/tomcat/request/session/data/cache/impl/StandardDataCache.java index e56d4ee..2ee7e32 100644 --- a/src/main/java/tomcat/request/session/data/cache/impl/StandardDataCache.java +++ b/src/main/java/tomcat/request/session/data/cache/impl/StandardDataCache.java @@ -110,6 +110,12 @@ public class StandardDataCache extends RedisCache { return (value == null) ? 0L : 1L; } + /** {@inheritDoc} */ + @Override + public Boolean exists(String key) { + return this.sessionData.containsKey(key); + } + /** Session data. */ private static class SessionData implements Serializable { private byte[] value; diff --git a/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisCache.java b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisCache.java index 19e7e76..1a4f780 100644 --- a/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisCache.java +++ b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisCache.java @@ -24,31 +24,37 @@ public class RedisCache implements DataCache { /** {@inheritDoc} */ @Override public byte[] set(String key, byte[] value) { - return dataCache.set(key, value); + return this.dataCache.set(key, value); } /** {@inheritDoc} */ @Override public Long setnx(String key, byte[] value) { - return dataCache.setnx(key, value); + return this.dataCache.setnx(key, value); } /** {@inheritDoc} */ @Override public Long expire(String key, int seconds) { - return dataCache.expire(key, seconds); + return this.dataCache.expire(key, seconds); } /** {@inheritDoc} */ @Override public byte[] get(String key) { - return dataCache.get(key); + return this.dataCache.get(key); } /** {@inheritDoc} */ @Override public Long delete(String key) { - return dataCache.delete(key); + return this.dataCache.delete(key); + } + + /** {@inheritDoc} */ + @Override + public Boolean exists(String key) { + return this.dataCache.exists(key); } private void initialize(Config config) { diff --git a/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisClusterManager.java b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisClusterManager.java index 6f2316a..44eb8e3 100644 --- a/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisClusterManager.java +++ b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisClusterManager.java @@ -115,4 +115,22 @@ class RedisClusterManager extends RedisManager { } while (retry && tries <= NUM_RETRIES); return retVal; } + + /** {@inheritDoc} */ + @Override + public Boolean exists(String key) { + int tries = 0; + boolean retry = true; + Boolean retVal = null; + do { + tries++; + try { + retVal = this.cluster.exists(key); + retry = false; + } catch (JedisRedirectionException | JedisConnectionException ex) { + handleException(tries, ex); + } + } while (retry && tries <= NUM_RETRIES); + return retVal; + } } diff --git a/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisManager.java b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisManager.java index 9a30bf6..d1a48fd 100644 --- a/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisManager.java +++ b/src/main/java/tomcat/request/session/data/cache/impl/redis/RedisManager.java @@ -113,6 +113,24 @@ abstract class RedisManager implements DataCache { return retVal; } + /** {@inheritDoc} */ + @Override + public Boolean exists(String key) { + int tries = 0; + boolean retry = true; + Boolean retVal = null; + do { + tries++; + try (Jedis jedis = this.pool.getResource()) { + retVal = jedis.exists(key); + retry = false; + } catch (JedisConnectionException ex) { + handleException(tries, ex); + } + } while (retry && tries <= NUM_RETRIES); + return retVal; + } + /** * To handle jedis exception. * diff --git a/src/main/java/tomcat/request/session/model/SingleSignOnEntry.java b/src/main/java/tomcat/request/session/model/SingleSignOnEntry.java new file mode 100644 index 0000000..a364b6d --- /dev/null +++ b/src/main/java/tomcat/request/session/model/SingleSignOnEntry.java @@ -0,0 +1,93 @@ +package tomcat.request.session.model; + +import org.apache.catalina.Session; +import org.apache.catalina.authenticator.SingleSignOnListener; +import org.apache.catalina.authenticator.SingleSignOnSessionKey; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.security.Principal; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** author: Ranjith Manickam @ 20 Mar' 2020 */ +public class SingleSignOnEntry implements Serializable { + + private String authType; + private String password; + private Principal principal; + private String username; + private boolean canReauthenticate = false; + private final ConcurrentMap sessionKeys; + + public SingleSignOnEntry() { + this.sessionKeys = new ConcurrentHashMap<>(); + } + + public SingleSignOnEntry(Principal principal, String authType, String username, String password) { + this.sessionKeys = new ConcurrentHashMap<>(); + this.updateCredentials(principal, authType, username, password); + } + + public void addSession(String ssoId, Session session) { + SingleSignOnSessionKey key = new SingleSignOnSessionKey(session); + SingleSignOnSessionKey currentKey = this.sessionKeys.putIfAbsent(key, key); + if (currentKey == null) { + session.addSessionListener(new SingleSignOnListener(ssoId)); + } + } + + public void removeSession(Session session) { + SingleSignOnSessionKey key = new SingleSignOnSessionKey(session); + this.sessionKeys.remove(key); + } + + public Set findSessions() { + return this.sessionKeys.keySet(); + } + + public String getAuthType() { + return this.authType; + } + + public boolean getCanReauthenticate() { + return this.canReauthenticate; + } + + public String getPassword() { + return this.password; + } + + public Principal getPrincipal() { + return this.principal; + } + + public String getUsername() { + return this.username; + } + + public synchronized void updateCredentials(Principal principal, String authType, String username, String password) { + this.principal = principal; + this.authType = authType; + this.username = username; + this.password = password; + this.canReauthenticate = "BASIC".equals(authType) || "FORM".equals(authType); + } + + public void writeObjectData(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + out.writeBoolean(true); + out.writeObject(this.principal); + } + + public void readObjectData(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + boolean hasPrincipal = in.readBoolean(); + if (hasPrincipal) { + this.principal = (Principal) in.readObject(); + } + } +} diff --git a/src/main/java/tomcat/request/session/redis/SessionManager.java b/src/main/java/tomcat/request/session/redis/SessionManager.java index faf9bec..837ea50 100644 --- a/src/main/java/tomcat/request/session/redis/SessionManager.java +++ b/src/main/java/tomcat/request/session/redis/SessionManager.java @@ -17,6 +17,7 @@ import tomcat.request.session.model.Config; import tomcat.request.session.model.Session; import tomcat.request.session.model.SessionContext; import tomcat.request.session.model.SessionMetadata; +import tomcat.request.session.model.SingleSignOnEntry; import tomcat.request.session.util.ConfigUtil; import tomcat.request.session.util.SerializationUtil; @@ -77,6 +78,14 @@ public class SessionManager extends ManagerBase implements Lifecycle { initializedValve = true; break; } + + if (valve instanceof SingleSignOnValve) { + SingleSignOnValve ssoValve = (SingleSignOnValve) valve; + ssoValve.setSessionManager(this); + ssoValve.setContext(context); + initializedValve = true; + break; + } } if (!initializedValve) { @@ -335,4 +344,41 @@ public class SessionManager extends ManagerBase implements Lifecycle { this.sessionPolicy.add(SessionPolicy.fromName(sessionPolicyName)); } } + + /** To set single-sign-on entry to cache. */ + void setSingleSignOnEntry(String ssoId, SingleSignOnEntry entry) { + if (entry == null) { + return; + } + try { + byte[] data = this.serializer.serializeSingleSignOnEntry(entry); + this.dataCache.set(ssoId, data); + } catch (IOException ex) { + LOGGER.error("Error occurred while serializing the single-sign-on entry..", ex); + } + } + + /** To get single-sign-on entry from cache. */ + SingleSignOnEntry getSingleSignOnEntry(String ssoId) { + byte[] data = this.dataCache.get(ssoId); + SingleSignOnEntry entry = new SingleSignOnEntry(); + + try { + this.serializer.deserializeSingleSignOnEntry(data, entry); + } catch (IOException | ClassNotFoundException ex) { + LOGGER.error("Error occurred while de-serializing the single-sign-on entry..", ex); + return null; + } + return entry; + } + + /** To check single-sign-on entry exists from cache. */ + Boolean singleSignOnEntryExists(String ssoId) { + return this.dataCache.exists(ssoId); + } + + /** To delete single-sign-on entry from cache. */ + void deleteSingleSignOnEntry(String ssoId) { + this.dataCache.delete(ssoId); + } } diff --git a/src/main/java/tomcat/request/session/redis/SingleSignOnValve.java b/src/main/java/tomcat/request/session/redis/SingleSignOnValve.java new file mode 100644 index 0000000..b57207f --- /dev/null +++ b/src/main/java/tomcat/request/session/redis/SingleSignOnValve.java @@ -0,0 +1,266 @@ +package tomcat.request.session.redis; + +import org.apache.catalina.Container; +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.LifecycleState; +import org.apache.catalina.Realm; +import org.apache.catalina.Session; +import org.apache.catalina.authenticator.Constants; +import org.apache.catalina.authenticator.SingleSignOn; +import org.apache.catalina.authenticator.SingleSignOnSessionKey; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tomcat.request.session.exception.BackendException; +import tomcat.request.session.model.SingleSignOnEntry; + +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import java.io.IOException; +import java.security.Principal; +import java.util.Set; + +/** author: Ranjith Manickam @ 20 Mar' 2020 */ +public class SingleSignOnValve extends SingleSignOn { + + private static final Logger LOGGER = LoggerFactory.getLogger(SingleSignOnValve.class); + + private Context context; + private SessionManager manager; + + /** {@inheritDoc} */ + @Override + protected synchronized void startInternal() throws LifecycleException { + super.setState(LifecycleState.STARTING); + super.startInternal(); + } + + /** {@inheritDoc} */ + @Override + protected synchronized void stopInternal() throws LifecycleException { + super.setState(LifecycleState.STOPPING); + super.stopInternal(); + this.context = null; + } + + /** {@inheritDoc} */ + @Override + public void invoke(Request request, Response response) throws BackendException { + try { + request.removeNote("org.apache.catalina.request.SSOID"); + LOGGER.debug("singleSignOn.debug.invoke, requestURI: {}", request.getRequestURI()); + + if (request.getUserPrincipal() == null) { + LOGGER.debug("singleSignOn.debug.cookieCheck"); + Cookie cookie = null; + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + for (Cookie value : cookies) { + if (Constants.SINGLE_SIGN_ON_COOKIE.equals(value.getName())) { + cookie = value; + break; + } + } + } + + if (cookie == null) { + LOGGER.debug("singleSignOn.debug.cookieNotFound"); + } else { + LOGGER.debug("singleSignOn.debug.principalCheck, ssoId: {}", cookie.getValue()); + + SingleSignOnEntry entry = this.manager.getSingleSignOnEntry(cookie.getValue()); + if (entry == null) { + LOGGER.debug("singleSignOn.debug.principalNotFound, ssoId: {}", cookie.getValue()); + cookie.setValue("REMOVE"); + cookie.setMaxAge(0); + cookie.setPath("/"); + String domain = this.getCookieDomain(); + if (domain != null) { + cookie.setDomain(domain); + } + + cookie.setSecure(request.isSecure()); + if (request.getServletContext().getSessionCookieConfig().isHttpOnly() || request.getContext().getUseHttpOnly()) { + cookie.setHttpOnly(true); + } + response.addCookie(cookie); + } else { + LOGGER.debug("singleSignOn.debug.principalFound, principal: {}, authType: {}", (entry.getPrincipal() != null ? entry.getPrincipal().getName() : ""), entry.getAuthType()); + request.setNote("org.apache.catalina.request.SSOID", cookie.getValue()); + if (!this.getRequireReauthentication()) { + request.setAuthType(entry.getAuthType()); + request.setUserPrincipal(entry.getPrincipal()); + } + } + } + } else { + LOGGER.debug("singleSignOn.debug.hasPrincipal, principal: {}", request.getUserPrincipal().getName()); + } + this.getNext().invoke(request, response); + + } catch (IOException | ServletException | RuntimeException ex) { + LOGGER.error("Error processing request", ex); + throw new BackendException(); + } finally { + this.manager.afterRequest(); + } + } + + /** {@inheritDoc} */ + @Override + public void sessionDestroyed(String ssoId, Session session) { + if (this.getState().isAvailable()) { + if ((session.getMaxInactiveInterval() <= 0 || + session.getIdleTimeInternal() < (long) (session.getMaxInactiveInterval() * 1000)) + && session.getManager().getContext().getState().isAvailable()) { + + LOGGER.debug("singleSignOn.debug.sessionLogout, session: {}", session); + this.removeSession(ssoId, session); + if (this.manager.singleSignOnEntryExists(ssoId)) { + this.deregister(ssoId); + } + return; + } + + LOGGER.debug("singleSignOn.debug.sessionTimeout, ssoId: {}, session: {}", ssoId, session); + this.removeSession(ssoId, session); + } + } + + /** {@inheritDoc} */ + @Override + protected boolean associate(String ssoId, Session session) { + SingleSignOnEntry entry = this.manager.getSingleSignOnEntry(ssoId); + if (entry == null) { + LOGGER.debug("singleSignOn.debug.associateFail, ssoId: {}, session: {}", ssoId, session); + return false; + } + + LOGGER.debug("singleSignOn.debug.associate, ssoId: {}, session: {}", ssoId, session); + entry.addSession(ssoId, session); + this.manager.setSingleSignOnEntry(ssoId, entry); + return true; + } + + /** {@inheritDoc} */ + @Override + protected void deregister(String ssoId) { + SingleSignOnEntry entry = this.manager.getSingleSignOnEntry(ssoId); + this.manager.deleteSingleSignOnEntry(ssoId); + if (entry == null) { + LOGGER.debug("singleSignOn.debug.deregisterFail, ssoId: {}", ssoId); + return; + } + + Set ssoKeys = entry.findSessions(); + if (ssoKeys.isEmpty()) { + LOGGER.debug("singleSignOn.debug.deregisterNone, ssoId: {}", ssoId); + } + + for (SingleSignOnSessionKey ssoKey : ssoKeys) { + this.expire(ssoKey); + LOGGER.debug("singleSignOn.debug.deregister, ssoKey: {}, ssoId: {}", ssoKey, ssoId); + } + } + + /** {@inheritDoc} */ + @Override + protected boolean reauthenticate(String ssoId, Realm realm, Request request) { + if (ssoId == null || realm == null) { + return false; + } + + boolean reAuthenticated = false; + SingleSignOnEntry entry = this.manager.getSingleSignOnEntry(ssoId); + if (entry != null && entry.getCanReauthenticate()) { + String username = entry.getUsername(); + if (username != null) { + Principal reAuthPrincipal = realm.authenticate(username, entry.getPassword()); + if (reAuthPrincipal != null) { + reAuthenticated = true; + request.setAuthType(entry.getAuthType()); + request.setUserPrincipal(reAuthPrincipal); + } + } + } + return reAuthenticated; + } + + /** {@inheritDoc} */ + @Override + protected void register(String ssoId, Principal principal, String authType, String username, String password) { + LOGGER.debug("singleSignOn.debug.register, ssoId: {}, principal: {}, authType: {}", ssoId, (principal != null ? principal.getName() : ""), authType); + SingleSignOnEntry entry = new SingleSignOnEntry(principal, authType, username, password); + this.manager.setSingleSignOnEntry(ssoId, entry); + } + + /** {@inheritDoc} */ + @Override + protected boolean update(String ssoId, Principal principal, String authType, String username, String password) { + SingleSignOnEntry entry = this.manager.getSingleSignOnEntry(ssoId); + if (entry == null || !entry.getCanReauthenticate()) { + return false; + } + + LOGGER.debug("singleSignOn.debug.update, ssoId: {}, authType: {}", ssoId, authType); + entry.updateCredentials(principal, authType, username, password); + this.manager.setSingleSignOnEntry(ssoId, entry); + return true; + } + + /** {@inheritDoc} */ + @Override + protected void removeSession(String ssoId, Session session) { + LOGGER.debug("singleSignOn.debug.removeSession, ssoId: {}, session: {}", ssoId, session); + SingleSignOnEntry entry = this.manager.getSingleSignOnEntry(ssoId); + if (entry != null) { + entry.removeSession(session); + if (entry.findSessions().size() == 0) { + this.deregister(ssoId); + } + } + } + + /** To set session manager. */ + void setSessionManager(SessionManager manager) { + this.manager = manager; + } + + /** To set context. */ + void setContext(Context context) { + this.context = context; + } + + /** To expire session. */ + private void expire(SingleSignOnSessionKey key) { + if (this.context == null) { + LOGGER.warn("singleSignOn.sessionExpire.engineNull, key: {}", key); + } else { + Container host = this.context.findChild(key.getHostName()); + if (host == null) { + LOGGER.warn("singleSignOn.sessionExpire.hostNotFound, key: {}", key); + } else { + Context context = (Context) host.findChild(key.getContextName()); + if (context == null) { + LOGGER.warn("singleSignOn.sessionExpire.contextNotFound, key: {}", key); + } else { + Session session; + try { + session = this.manager.findSession(key.getSessionId()); + } catch (IOException ex) { + LOGGER.warn("singleSignOn.sessionExpire.managerError, key: {}, exception: {}", key, ex); + return; + } + + if (session == null) { + LOGGER.warn("singleSignOn.sessionExpire.sessionNotFound, key: {}", key); + } else { + session.expire(); + } + } + } + } + } +} diff --git a/src/main/java/tomcat/request/session/util/SerializationUtil.java b/src/main/java/tomcat/request/session/util/SerializationUtil.java index 223624c..6e0ef9a 100644 --- a/src/main/java/tomcat/request/session/util/SerializationUtil.java +++ b/src/main/java/tomcat/request/session/util/SerializationUtil.java @@ -3,6 +3,7 @@ package tomcat.request.session.util; import org.apache.catalina.util.CustomObjectInputStream; import tomcat.request.session.model.Session; import tomcat.request.session.model.SessionMetadata; +import tomcat.request.session.model.SingleSignOnEntry; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -76,4 +77,25 @@ public class SerializationUtil { session.readObjectData(ois); } } + + /** To serialize single-sign-on entry. */ + public byte[] serializeSingleSignOnEntry(SingleSignOnEntry entry) throws IOException { + byte[] serialized; + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos))) { + entry.writeObjectData(oos); + oos.flush(); + serialized = bos.toByteArray(); + } + return serialized; + } + + /** To de-serialize single-sign-on entry. */ + public void deserializeSingleSignOnEntry(byte[] data, SingleSignOnEntry entry) + throws IOException, ClassNotFoundException { + try (BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data)); + ObjectInputStream ois = new CustomObjectInputStream(bis, this.loader)) { + entry.readObjectData(ois); + } + } }