Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tdakanalis/06d168925f80a72859cb to your computer and use it in GitHub Desktop.
Save tdakanalis/06d168925f80a72859cb to your computer and use it in GitHub Desktop.
Tomcat - Session Persistence With JAAS Realm

In Tomcat you can use the PersistentManager in order to swap active (but idle) sessions out to a persistent storage mechanism, as well as to save all sessions across a normal restart of Tomcat. Moreover, the JAASRealm is an implementation of the Tomcat Realm interface that authenticates users through the Java Authentication & Authorization Service (JAAS) framework.

However, the problems start when the PersistentManager deserializes the persisted sessions but the built in JAAS authenticators are not able to find any user principal in those sessions and as a result the user is asked to provide their credentials. The cause of the problem is that as it has been documented in the org.apache.catalina.session.StandardSession class:

/**
 * The authenticated Principal associated with this session, if any.
 * <b>IMPLEMENTATION NOTE:</b>  This object is <i>not</i> saved and
 * restored across session serializations!
 */
 protected transient Principal principal = null;

In order to overcome that problem you have to create a custom JAAS Authenticator that will be responsible either to dispatch the incoming requests to one of the existing JAAS Authenticators (e.g. FormAuthenticator, BasicAuthenticator) if there is not any deserialized session or to dispatch the request to a custom authenticator that will only set the missing principal without checking for the user credentials. Finally, you have to write a LoginModule that will check the provided authenticatino method and will set the user as authenticated without checking their credentials.

In the AuthenticatorDispatcher.java, NonLoginAuthenticator.java and AbstractLoginModule.java there are sample implementations of all the aforementioned classes.

package security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import javax.security.sasl.RealmCallback;
public abstract class AbstractLoginModule implements LoginModule {
protected static Logger logger = LoggerFactory.getLogger(LoginModule.class);
private CallbackHandler callbackHandler;
private boolean authState = false;
private boolean commitState = false;
private JAASUserInfo currentUser;
private Subject subject;
public boolean login() throws LoginException {
try {
if (callbackHandler == null) {
throw new LoginException("No callback handler");
}
Callback[] callbacks = configureCallbacks();
callbackHandler.handle(callbacks);
String webUserName = ((NameCallback) callbacks[0]).getName();
String webCredential = new String(((PasswordCallback) callbacks[1]).getPassword());
String authMethod = ((TextInputCallback) callbacks[2]).getText();
String domain = ((RealmCallback) callbacks[3]).getText();
if ((webUserName == null) || (webCredential == null)) {
authState = false;
throw new LoginException("invalid_username");
}
UserInfo userInfo = getUserInfo(webUserName, domain);
if (userInfo == null) {
authState = false;
throw new LoginException("invalid_username");
}
currentUser = new JAASUserInfo(userInfo);
//Override auth as it has already been persisted by the PersistentManager
if ("PERSISTED_SESSION".equals(authMethod)) {
authState = true; //Authentication is already done
} else {
authState = currentUser.checkCredential(webCredential);
}
return authState;
} catch (LoginException e) {
throw e;
} catch (Exception e) {
throw new LoginException(e.toString());
}
}
public Callback[] configureCallbacks() {
Callback[] callbacks = new Callback[4];
callbacks[0] = new NameCallback("username");
callbacks[1] = new PasswordCallback("password", true);
callbacks[2] = new TextInputCallback("authMethod");
callbacks[3] = new RealmCallback("realmName");
return callbacks;
}
public abstract UserInfo getUserInfo (String username, String domain) throws Exception;
}
package security;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.authenticator.AuthenticatorBase;
import org.apache.catalina.authenticator.BasicAuthenticator;
import org.apache.catalina.authenticator.FormAuthenticator;
import org.apache.catalina.connector.Request;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.realm.JAASCallbackHandler;
import org.apache.catalina.realm.JAASRealm;
public class AuthenticatorDispatcher extends AuthenticatorBase {
private FormAuthenticator form = null;
private BasicAuthenticator basic = null;
private NonLoginAuthenticator nla = null;
public AuthenticatorDispatcher(){
form = new FormAuthenticator();
basic = new BasicAuthenticator();
nla = new NonLoginAuthenticator();
}
@Override
public boolean authenticate(Request request, HttpServletResponse response, LoginConfig loginConfig) throws IOException {
String uri = request.getRequestURI();
Object user = request.getSession().getAttribute("user");
String username = (String) request.getSession().getAttribute("username");
String domain = request.getServerName();
//Check here if we have a deserialized session from the PersistentManager
if( user !=null && request.getUserPrincipal() == null && username != null ){
JAASRealm realm = (JAASRealm) context.getRealm();
JAASCallbackHandler callbackHandler = new JAASCallbackHandler(realm, username, "", null, null,null,null,domain,null, "PERSISTED_SESSION");
request.setUserPrincipal(realm.authenticate(username, callbackHandler));
return nla.authenticate(request, response, loginConfig);
} else if(uri.matches(request.getContextPath() + "BaseAuthentication paths")){
request.setAuthType("BASIC");
return basic.authenticate(request,response,loginConfig);
} else {
request.setAuthType("FORM");
return form.authenticate(request,response,loginConfig);
}
}
@Override
protected String getAuthMethod() {
return "AuthenticatorDispatcher";
}
@Override
public String getInfo() {
return "security.AuthenticatorDispatcher/1.0";
}
}
package security;
import org.apache.catalina.Session;
import org.apache.catalina.authenticator.AuthenticatorBase;
import org.apache.catalina.authenticator.Constants;
import org.apache.catalina.connector.Request;
import org.apache.catalina.deploy.LoginConfig;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.Principal;
/**
* An <b>Authenticator</b> and <b>Valve</b> implementation that checks
* only security constraints not involving user authentication.
*/
public class NonLoginAuthenticator extends AuthenticatorBase {
@Override
public boolean authenticate(Request request,
HttpServletResponse response,
LoginConfig config)
throws IOException {
containerLog = getContainer().getLogger();
request.setAttribute("isProtected",true);
Principal principal = request.getPrincipal();
if (principal != null) {
// excellent... we have already authenticated the client somehow,
// probably from another container that has a login-config
if (containerLog.isDebugEnabled())
containerLog.debug("Already authenticated as '"
+ principal.getName() + "'");
if (cache) {
// create a new session (only if necessary)
Session session = request.getSessionInternal(true);
// save the inherited Principal (if necessary) in this
// session so it can remain authenticated until it expires
session.setPrincipal(principal);
// is there an SSO session cookie?
String ssoId =
(String) request.getNote(Constants.REQ_SSOID_NOTE);
if (ssoId != null) {
if (containerLog.isDebugEnabled())
containerLog.debug("User authenticated by existing SSO");
// Associate session with the existing SSO ID if necessary
associate(ssoId, session);
}
}
// user was already authenticated, with or without a cookie
return true;
}
// No Principal means the user is not already authenticated
// and so will not be assigned any roles. It is safe to
// to say the user is now authenticated because access to
// protected resources will only be allowed with a matching role.
// i.e. SC_FORBIDDEN (403 status) will be generated later.
if (containerLog.isDebugEnabled())
containerLog.debug("User authenticated without any roles");
return true;
}
/**
* Return the authentication method, which is vendor-specific and
* not defined by HttpServletRequest.
*/
@Override
protected String getAuthMethod() {
return "NONE";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment