Preventing Session Timeout Extension in JdbcSession

Published by Bilal Kaun on

Among other things, Sessions are an important aspect of application security and establish a realm of trust between the end user and the application. I won’t be going into the details of JSesson here, nor the background of how Spring manages sessions. More information on Spring Session can be found here.

Sessions of course timeout as part of security mechanism. The timeout, specified as maxInactiveInterval attribute of HttpSession) is specified by the application or business policy and by default on tomcat is 30 minutes of idle. In most vanilla cases, the session timeout is “slid” forward every time the user makes a call to the server. This is tracked by the lastAccessedTime attribute of the HttpSession. When this value plus the maxInactiveInterval are greater than the current server time, the session is considered no longer valid.

The Problem

One of our apps is an Angular-fronted SPA application, powered by Spring Boot in the back. We chose to not use JWT as there is just one back-end and no micro-services to deal with. To facilitate load-balancing without JWT we chose to push the Spring Session into database-persisted form via JdbcSessions. The SPA front-end periodically synchronizes the session status by invoking a controller method which returns time remaining. However invoking this end-point would result in extending the timeout; the act of observing the session’s state should be passive.

Go directly to github samples

The Solution

In short: Prevent lastAccessedTime from being updated in the database during this request
Figure 1 – Rough overview of where the relevant points of are in the request exec.
* Configurable in Spring Boot 2.2+

Figure 1 which shows roughly the path of execution and where the relevant session-related routines are performed.

In forward stack dive, SessionRepositoryFilter creates or updates the existing session with the latest value for lastAccessedTime (set as now). On the stack unwind path, SessionRepositoryFilter commits the session. At that point, the implementation, in our case JdbcIndexedSessionRepository class, persists the session attributes to the database. One of the properties saved is the lastAccessedTime that we wish to prevent from being saved.

As Spring Session, nor the JdbcSession library provide any natural API to manipulate session in such a fashion, the obvious place to manipulate it is in, what I’ll call, the Application space (as opposed to Spring Framework space) – more specifically, the controller body.

The idea is straight forward, in the HttpSession instance, reset the lastAccessedTime attribute. The first problem arises when we need to get at the instance we wish to reset. We need the actual instance of the session object stored in memory. We could rely on Spring’s autowiring to get the Request scope’s HttpSession.

@Autowired
private final HttpSession httpSession;

However as shown in Figure 2, the wired object itself is a proxy, and that is not the object we want – we want the object being pointed to by the proxy. A possible workaround is to use reflections to get at the target object inside the TargetSource proxy instance.

Figure 2 – Rough cartoon of proxy redirection

However, if we’re going to be relying on accessing private APIs via reflection, in my opinion, it is preferable to source the session instance from the request context than unmask the cglib proxy target. So where in the HttpServletRequest is the session stored?

Figure 3 – Inside the SessionRepositoryFilter, HttpSession instance is wrapped in a private class and added as an attribute into the HttpServletRequest instance

Trouble is HttpSession is not a class, it is an interface defined in Java EE. The implementation class in our case is defined as a nested private class, HttpSessionWrapper, inside SessionRepositoryFilter. Yet due to internal architectural design choices, Spring Framework has its own Session representation interface, Session. This is implemented by the private nested class JdbcSession inside JdbcOperationsSessionsRepository. The JdbcSession instance is the payload inside the HttpSessionWrapper class – allowing Spring Session implementation to interface with the HttpSession mechanism.

Figure 4 – In order to decouple session from HTTP, Spring has an internal Session representation

So it is this JdbcSession instance we’re after and fortunately, as figure 4 shows, it comes with two essential flags – new and changed – we’ll take a closer look at the two in a moment.

Inside the Application Space, Spring can autowire HttpServletRequest for us, and from figure 3, we know that it contains the reference to our HttpSession instance as a session attribute.

A passing note here: As with HttpSession itself, Spring wraps other elements as well, in particular, HttpServletRequest and HttpServletResponse get wrapped in private classes at SessionRepositoryFilter and passed to subsequent chain members. We won’t consider this here as it is not too relevant.

The HttpServletRequest attribute holding the reference to the session is key’d by the constant at SessionRepositoryFilter.SessionRepositoryRequestWrapper.CURRENT_SESSION_ATTR. Unfortunately it is a private member of a private class and so extracting it is more trouble than simply copying the constant itself into your own code.

private final String CURRENT_SESSION_ATTR = HttpServletRequestWrapper.class.getName();

@Autowired
private final HttpServletRequest httpServletRequest;
...

// Grab the Current Session
Object wrappedSession = httpServletRequest.getAttribute(CURRENT_SESSION_ATTR);

This was the relatively straight forward part. As I mentioned above, the HttpSession payload inside HttpServletRequest is not the one we’re after, as that is the wrapper class instance of SessionRepositoryFilter.HttpSessionWrapper. We need access to the payload inside the wrapper, which is the Spring Session instance of the implementing class JdbcOperationsSessionRepository.JdbcSession.

SessionRepositoryFilter.HttpSessionWrapper extends the ExpiringSessionHttpSession class which offers the api getSession, however we cannot import that class into our code (because of it’s private access modifier) and so cannot easily cast to it. At this point we must use Java Reflections to get the class reference and then the method reference to the getSession method. We can do this several ways, however the way I’ve done it, is use a visible class’ loader to specify the class in the package directly.

private static final Method getSessionMethod;
static {
        Class<?> expiringSessionHttpSessionClazz = null;
        try {
            expiringSessionHttpSessionClazz = SessionRepositoryFilter.class
                    .getClassLoader().loadClass("org.springframework.session.web.http.ExpiringSessionHttpSession");
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Cannot start without class reference", e);
        }

        Method[] expiringSessionHttpSessionDeclaredMethods = expiringSessionHttpSessionClazz.getDeclaredMethods();

        getSessionMethod = Arrays.stream(expiringSessionHttpSessionDeclaredMethods)
                .filter(method -> method.getName().endsWith("getSession"))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("ExpiringSessionHttpSession.getSession() method reference required for timeout-slider prevention mechanism"));

        getSessionMethod.setAccessible(true);
}

The previous code can be expended upon as such:

try {
        Object session = getSessionMethod.invoke(wrappedSession);
            
    } catch (Exception e) {
        log.warn("Infinite session possible due to timeout-slider-prevention mechanism failure.", e);
    }

At this point we have access to JdbcSession instance and can perform the intended logic to ensure lastAccessedTime does not advance into the database. Recall that committing the session occurs on the stack wind down of the filter-chain (illustrated by figure 1 above). What happens there is that the JdbcSession::isChanged() method is consulted for changes before an update query is executed and the updated lastAccessedTime saved to the database. Our best approach is to reset this bool in our Application Space so that the filter would ignore the updated attributes when it sees that changes have not occurred. Incredibly, JdbcSession::clearChangeFlags() is an existing method that we can access via reflections for this very job.

private static final Method clearChangeFlagsMethod;
static {
    Class<?>[] nestedInternalClasses = JdbcOperationsSessionRepository.class.getDeclaredClasses();
    Class<?> jdbcSessionClazz = Arrays.stream(nestedInternalClasses)
            .filter(clazz -> clazz.getName().endsWith("JdbcSession"))
            .findFirst()
            .orElseThrow(() -> new RuntimeException("JdbcSession Internal class reference required for timeout-slider prevention mechanism"));

    Method[] jdbcSessionClazzDeclaredMethods = jdbcSessionClazz.getDeclaredMethods();
    clearChangeFlagsMethod = Arrays.stream(jdbcSessionClazzDeclaredMethods)
            .filter(method -> method.getName().endsWith("clearChangeFlags"))
            .findFirst()
            .orElseThrow(() -> new RuntimeException("JdbcSession.clearChangeFlags() method reference required for timeout-slider prevention mechanism"));

    clearChangeFlagsMethod.setAccessible(true);


}

The idea is simple, the JdbcOperationsSessionRepository is visible to us, so we drilled down into its nested classes using the getDeclaredClasses() method. We find the JdbcSession class and similarly drill down into its methods using the getDeclaredMethods() method until we get to our target method of clearChangeFlags(). This can then be invoked like so: clearChangeFlagsMethod.invoke(session);

That’s it – this will clear out the dirty flags and that will prevent the latest lastAccessTime from advancing in the database.

Due to the use of internal and unsupported APIs via Reflections, we can expect this method to break without warning with any upgrade of the framework or libraries. In fact, the above method works until prior to Spring Boot 2.2. Spring Boot 2.2 brings in some changes that refactors the code, however the fix is straight forward and did not break the mechanism. The github repo contains working sample with both Spring Boot 1.4.3 (and previous), and Spring Boot 2.2 and newer.

Full working samples at github. The samples take the concept further and show how we’ve addressed the problem in our enterprise application. We’ve decoupled the session dirty flag clearing routine into an Aspect code and mark the method that should not advance the lastAccessedTime variable using a custom annotation.


0 Comments

Leave a Reply