changes to support single-sign-on (sso)
parent
16fffca534
commit
2b13fdadc9
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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<SingleSignOnSessionKey, SingleSignOnSessionKey> 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<SingleSignOnSessionKey> 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<SingleSignOnSessionKey> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue