spring-projects / spring-session

Spring Session
https://spring.io/projects/spring-session
Apache License 2.0
1.86k stars 1.11k forks source link

Support persistent cookies for storing session ID #3097

Closed theigl closed 1 week ago

theigl commented 1 month ago

Expected Behavior

We would like to store the session ID in a cookie with an expiration date that matches the session timeout. The cookie expiration date should be updated every time the user accesses the session.

Current Behavior

The session ID is stored in a session cookie by default. It is possible to override the cookie's maxAge in CookieSerializer, but there is no way to refresh the cookie whenever the session is accessed and the cookie would simply expire after maxAge, even if the session is still actively used.

Context

We are using Capacitor to bundle our web application as a native app. Many users are closing the application instead of pausing it/moving it to the background, only to re-open it a minute later. Every time the app is closed, all session cookies are cleared and the user is associated with a new session. Using a persistent cookie that is extended on every session access, would be an elegant solution to this issue.

Possible workarounds:

theigl commented 1 month ago

I created a draft PR that shows what changes would be necessary to support my use case: https://github.com/spring-projects/spring-session/pull/3098

marcusdacoregio commented 3 weeks ago

Hello @theigl. I'd like to know more about your use case and what you've tried.

Have you tried to do defaultCookieSerializer.setCookieMaxAge(Integer.MAX_VALUE);? This way the cookie expiration time is long and you rely only on the session expiration time.

Manually extend the session cookie in response to user actions or a globally in a filter

This can probably be achieved with a custom filter, have you tried it? I'm unsure if you want to write the Set-Cookie header back on every request the client makes.

theigl commented 3 weeks ago

Hi @marcusdacoregio!

Have you tried to do defaultCookieSerializer.setCookieMaxAge(Integer.MAX_VALUE);? This way the cookie expiration time is long and you rely only on the session expiration time.

This approach would work, but it feels a bit wrong. My session timeout is set to 30 minutes, so setting a persistent cookie with a very long lifetime will lead to a lot of requests for sessions that definitely do not exist anymore.

This can probably be achieved with a custom filter, have you tried it?

I did a quick prototype but soon realized that this is not trivial to implement. The filter has to be added after the session repository filter and it has to handle the case where the session is created or invalidated in the same request. There is no API for retrieving cookies from the HttpServletResponse so I would need to set some attribute on the request to know that the session has been created or invalidated or wrap the response and expose the cookies that have been added.

I'm unsure if you want to write the Set-Cookie header back on every request the client makes.

I would probably not set in on every request but check the cookie expiration date and only extend it if at least a minute has passed since the last extension.

marcusdacoregio commented 3 weeks ago

This approach would work, but it feels a bit wrong. My session timeout is set to 30 minutes, so setting a persistent cookie with a very long lifetime will lead to a lot of requests for sessions that definitely do not exist anymore.

I don't follow what you mean, if the session is expired the request will fail and a new session will be created for the client. It looks better than creating multiple sessions if the user closes the app.

I don't think it is trivial for the API to support that either, while it might make sense for your use case I don't feel yet that it is the right place to do it.

There is no API for retrieving cookies from the HttpServletResponse

You can retrieve the cookies from the HttpServletRequest or you can retrieve the Set-Cookie headers from the response.

Have you tried to do something like:

public class SessionExtenderFilter extends OncePerRequestFilter {

    private final Clock clock = Clock.systemUTC();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        filterChain.doFilter(request, response);
        HttpSession session = request.getSession(false);
        if (session == null) {
            return;
        }
        for (Cookie cookie : request.getCookies()) {
            if ("SESSION".equals(cookie.getName())) {
                extendSessionCookieIfNeeded(session, response, cookie);
            }
        }
    }

    private void extendSessionCookieIfNeeded(HttpSession session, HttpServletResponse response, Cookie cookie) {
        // you can also check any other attribute from the session here
        int maxAge = cookie.getMaxAge();
        Instant expires = this.clock.instant().plusSeconds(maxAge);
        Instant now = this.clock.instant();
        if (ChronoUnit.SECONDS.between(now, expires) < 60) {
            cookie.setMaxAge(1800);
            response.addCookie(cookie);
        }
    }

}

I would place this filter before the SessionRepositoryFilter since it will run after the response is returned.

spring-projects-issues commented 2 weeks ago

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

theigl commented 1 week ago

Thanks @marcusdacoregio. I think that will work for our use case. Thanks for taking the time to suggesting a workable solution!