Search code examples
javaservletsjettysingle-sign-onform-authentication

Single Sign On Jetty with multiple instances of WebAppContext


I have an embedded jetty server that is iterating through a list of webapps (the list varies between deployments) from many different locations. I'm trying to make the transition from basic authentication to form authentication.

What I'd like to do is something like:

// create constraint
Constraint usersOnly = new Constraint(Constraint.__FORM_AUTH, "user");
usersOnly.setAuthenticate(true);
ConstraintMapping requireAuthentication = new ConstraintMapping();
requireAuthentication.setConstraint(usersOnly);
requireAuthentication.setPathSpec("/*");

// create login service
LoginService loginService = new HashLoginService("realm");
loginService.setConfig("users.txt");

// create form authentication
FormAuthenticator formAuthenticator = new FormAuthenticator("/login", "/login", true);

// create /login route
ServletHolder loginServlet = new ServletHolder(new DefaultServlet() {
@Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    response.getWriter().append("<html>\n<head>\n<title>Login</title>\n</head>\n<body>\n"
    + "<form method='POST' action='/j_security_check'>\n"
    + "<input type='text' name='j_username'/>\n"
    + "<input type='password' name='j_password'/>\n"
    + "<input type='submit' value='Login'/>\n</form>\n</body>\n</html>\n");
  }
});

ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
securityHandler.addMapping(requireAuthentication);
securityHandler.setLoginService(loginService);
securityHandler.setAuthenticator(formAuthenticator);

// assign security to each webapp
for (WebAppContext webapp : webapps) {
  webapp.setSecurityHandler(securityHandler);
  webapp.addServlet(loginServlet, "/login");
}

This works as desired if there's only one webapp in webapps, but if there are multiple, you get prompted to sign in every time you follow a link from one webapp to another, and every time you reauthenticate, you get redirected to the base webapp at route "/" and are only authenticated for that one.

I want to get my contexts to share sessions.

According to this question, having a common SessionManager for each WebAppContext instance should solve the problem, but the asker only had one WebAppContext instance. If I try to assign the same SessionManager instance to each WebAppContext, I get NPEs.

I've also seen some resources point to setting the path of each context's SessionCookieConfig to a common context path and the useRequestedId to true for each WebAppContext's SessionManager, but this solution is for org.mortbay.jetty and is outdated.

If you have any insight or experience setting up SSO for an embedded jetty server with multiple WebAppContexts, or if you can think of a better way to serve up multiple distinct webapps with one common server, please point me in the right direction.

How can I allow a user to authenticate into all of the webapps handled by one server by filling out a single form?

Let me know if I'm not being clear or if you have any questions.


Solution

  • My solution was to extend the HashSessionManager class so that it queries the SessionIdManager before creating a new session. The result is that CrossContextSessionManager instances under the same SessionIdManager all share session contents instead of just session ids. Therefore, signing in to one webapp means signing in to them all.

    import java.util.Collection;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpSession;
    
    import org.eclipse.jetty.server.session.AbstractSession;
    import org.eclipse.jetty.server.session.HashSessionIdManager;
    import org.eclipse.jetty.server.session.HashSessionManager;
    
    /**
     * Allows the WebAppContext to check the server's SessionIdManager before creating a new session
     * so that WebAppContext can share session contents for each client rather than just session ids.
     */
    public class CrossContextSessionManager extends HashSessionManager {
    
      // Number of seconds before the user is automatically logged out of an idle webapp session
      private int defaultSessionTimeout = 1800;
    
      /**
       * Check for an existing session in the session id manager by the requested id.
       * If no session has that id, create a new HttpSession for the request.
       */
      @Override
      public HttpSession newHttpSession(HttpServletRequest request) {
        AbstractSession session = null;
    
        String requestedId = request.getRequestedSessionId();
        if (requestedId != null) {
          String clusterId = getSessionIdManager().getClusterId(requestedId);
          Collection<HttpSession> sessions = ((HashSessionIdManager) getSessionIdManager()).getSession(clusterId);
          for (HttpSession httpSession : sessions) {
            session = (AbstractSession) httpSession;
            break;
          }
        }
    
        if (session == null) {
          session = newSession(request);
          session.setMaxInactiveInterval(defaultSessionTimeout);
          addSession(session,true);
        }
    
        return session;
      }
    }
    

    If the request is already carrying an id, newSessionId will just pull that id out of it. Otherwise, it will create a new id unique from all existing ids.