Hi @all

jwt + statefull server (sessions) generates a new JSESSIONID with every request.

I use vaadin for UI which makes my application stateful (sessions) and jwt with resource-server for authentication/authorization (just accepting the Authorization header with bearer token, since I'm using a reverse proxy before my app).

When updating from Spring Boot 2.6x to 2.7 I get non deterministic reloads in my UI. I tracked the cause down to request having apparently no session and after the filter chain sth goes south within vaadin. But as it works just fine with 2.6.14 and there were a lot of changes in spring security between 5.6 and 5.6 with respect to session management I suspect a bug here.

I repeatedly called used my app until the unwanted behavior (reload) is triggered. Here is an (hopefully) important log:

good call
DEBUG o.s.s.o.s.r.w.BearerTokenAuthenticationFilter uid=user - Set SecurityContextHolder to JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@d0917f20, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=7AE37ABDEE5BB97C61DDF8750885886B], Granted Authorities=[]] 

bad call
DEBUG o.s.s.o.s.r.w.BearerTokenAuthenticationFilter uid=user - Set SecurityContextHolder to JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@d0917f20, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[]]

With respect to the title (the changing JSESSIONIDs). This does not happen in 5.6 but it does in 5.7. I think the reason for this is the change in HttpSessionSecurityContextRepository where those lines were added at the beginning of saveContext

    if (isTransient(context)) {
        return;
    }
    final Authentication authentication = context.getAuthentication();
    if (isTransient(authentication)) { //true for JWT Token!
        return;
    }

Due to this change, the 5.7 code never reaches further down in this method and therefore never calls

httpSession.setAttribute(springSecurityContextKey, context); //also verified this with conditional breakpoint directly at the StandardSession#setAttribute(). It is never called with 'name.equals("SPRING_SECURITY_CONTEXT")'

Now since the sessions never get this attribute set, the behavior in SessionManagementFilter changes.

//SessionManagementFilter#doFilter
    private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        if (request.getAttribute(FILTER_APPLIED) != null) {
            chain.doFilter(request, response);
            return;
        }
        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
        if (!this.securityContextRepository.containsContext(request)) { // this evaluates always to true in 5.7 
                        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            if (authentication != null && !this.trustResolver.isAnonymous(authentication)) {
                // The user has been authenticated during the current request, so call the
                // session strategy
                try {
                    this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response); //changes the JSESSIONID via ChangeSessionIdAuthenticationStrategy which in turn deletes the old session id from session securityContextRepository which it does only for the very first call in 5.6
                }
        ...

//HttpSessionSecurityContextRepository#containsContext
    @Override
    public boolean containsContext(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        if (session == null) {
            return false;
        }
        return session.getAttribute(this.springSecurityContextKey) != null; //always false, since it was never set due to the "new" isTransient check.
    }

Reasons why I THINK(GUESS)! that Vaadin struggles: - Vaadin sends multiple (2) request simultaneously - Depending on how exactly those two calls pass through the doFilter methods above, it results in a request with a null session (explaining the log in the beginning). Keep in mind, this behavior is not deterministic! - To be more precise: the timing of the two threads passing through if (!this.securityContextRepository.containsContext(request)) { and this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response);

I cloned this repo and looked through the entire diff between 5.6 and 5.7. But there were so many changes with respect to session management, that I wasn't able to exactly pin point this behavior.

I also tried to create a min rep example and got the changing session ID sometimes, but not always. I am not an expert with stateful servers (session). But since it might be helpful I pushed it to github here spring_security_mre. It uses a local IDP which I've set up like described here Keycloak - JWT token generation. I only got the changing IDs sometimes, but after I got it, every subsequent request changed the ID. But the example is not stateful and even with session management set to always, it would not create the JSESSIONID on it's one (as I said, I'm not an expert and try to stay stateless whenever I can; I can't with Vaadin though).

If there is anything I can provide, please let me know. This issue prevents me from updating to 2.7 and beyond for month now.

Comment From: RainerGanss

I have added a branch based on vaadin which 100% reproduces the changing JSESSIONIDs. Still using my local keycloak to use the full jwt stack and not some mocked version.

https://github.com/RainerGanss/spring_security_mre/tree/vaadin

Comment From: jzheaux

Hi, @RainerGanss, thanks for the detailed report. SessionManagementFilter's behavior was corrected to not save transient authentications, so I don't believe the code you pointed out is in error.

I guess I'm not completely clear on how Spring Security is behaving incorrectly. Stateless authentications have nothing to do with how sessions are managed, so I would guess that the issue is elsewhere. It seems more likely that things are working for you in 5.6 by coincidence.

That said, one change I'd be interested in you trying is to remove the SessionManagementFilter and rely on the authentication filter to save the authentication. You can do this by adding the following to your configuration:

http
    // ...
    .sessionManagement((session) -> session.requireExplicitSave(true))
    // ...

That additional configuration should remove any Spring Security filter that is creating or overriding sessions. At that point, if you are still seeing sessions getting created and re-created, there is probably something else going on in Vaadin. Will you try that setting and please let me know how the behavior changes?

Comment From: RainerGanss

Hi, @jzheaux, at least in 5.7 there is no method requireExplicitSave() for the sessionManagement. I only found it here doc

public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        // ...
        .securityContext((securityContext) -> securityContext
            .requireExplicitSave(true)
        );
    return http.build();
}

I tried that, but it did not solve the problem. Did you mean that or is there sth else, I can try?

Comment From: jzheaux

My apologies, the setting is in sessionManagement as you said. However, after reading your description over again, I think something different is going on.

Here is what appears to be happening. In a request where there is no session yet, and Authorization: Bearer exists:

  1. VaadinService eagerly creates the session
  2. Spring Security authenticates the bearer token; since it is a stateless authentication, the session is not updated
  3. The response includes the JSESSIONID that Vaadin generated

On a subsequent request that includes the JSESSIONID as well as Authorization: Bearer:

  1. Spring Security authenticates the bearer token; since it is a stateless authentication, the session is not updated
  2. SessionManagementFilter sees that the session has no authentication stored, but that one was processed on this request; because of that, it will invoke its session fixation protection, changing the session id
  3. The response includes the JSESSIONID with the updated session id

One thing I'd recommend is considering updating to 5.8 when you can. 5.8 cleans up some of these tricky arrangements where SessionManagementFilter is left to make guesses about the intent of the request. You can access those new features by turning the session management filter off and leaving storage to each authentication.

That said, I think there is a more important issue at hand, which is how you want requests to be authenticated. It seems like what you might be wanting is to use a JWT to establish an authenticated session. I think that OAuth 2.0 Client probably makes more sense in this case as it is built with that in mind. Bearer Token authentication (OAuth 2.0 Resource Server) is typically more compatible with REST APIs and doesn't use the session.

You can keep using this as a resource server, but you are going to run into mismatches like these. One way around it is to indicate that the authentication should be stored. You can do this by using an authentication token that does not have the @Transient annotation like so:

@Override
protected void configure(final HttpSecurity http) throws Exception {
    super.configure(http);
    JwtAuthenticationConverter authenticationConverter = new JwtAuthenticationConverter();
    http.oauth2ResourceServer().jwt().jwtAuthenticationConverter((jwt) ->  
        new JwtAuthentication(authenticationConverter.convert(jwt))
    );
}

// ...
public static final class StorableJwtAuthentication extends AbstractAuthenticationToken {

    private final AbstractAuthenticationToken jwt;

    public JwtAuthentication(AbstractAuthenticationToken jwt) {
        super(jwt.getAuthorities());
        setAuthenticated(true);
        this.jwt = jwt;
    }

    @Override
    public Object getCredentials() {
        return this.jwt.getCredentials();
    }

    @Override
    public Object getPrincipal() {
        return this.jwt.getPrincipal();
    }
}

In that case, you would present an Authorization header on the first request, and then only the JSESSIONID on subsequent requests. This is important defensively as Spring Security does not by default require CSRF if a valid bearer token is supplied.

Another way to address the mismatch is to not use the session and to present the bearer token on each request, creating a more traditional REST API arrangement. Not being very familiar with Vaadin, this seems like something Vaadin might not be built for, but I'm not sure.

Either way, I think at this point there isn't a bug to fix in Spring Security so I'm going to close this issue. If you have follow-up questions about how to use Spring Security, please post questions to Stack Overflow or find me on Gitter, and I'll be happy to continue helping you get this working.

Comment From: RainerGanss

@jzheaux Thank you very much for the detailed answer! I don't use the oauth client since I'm behind a reverse proxy that does all the IDP forwarding and stuff and I simple get the auth header with the jwt. The ressource server is only for validation.

We weren't able to do minor version updates of either vaadin or spring due to this behavior. I updated my mre to spring boot 3 and Vaadin 24 and the behavior was gone. So I'm positiv that updating to all new major versions will solve the problem since vaadin changed code explicitly related to session + jwt handling. fix: Use correct SecurityContextRepository for stateless authentication

I'll keep you posted. Updating to all new major versions at once incl. Java 17 and Hibernate 6 is quite rough, but it's the only way out of this it seems.