JWT: updating a user's role without forcing reauth, Vaadin 24.3.9+

In our app, a user’s role can change within a session. Our app uses JWT authentication based on the approach in this Vaadin blog. In versions up to 24.3.8, we accomplished this role change by writing a JWT with the updated role (code below). In v 24.3.9, we understand that JWT tokens started refreshing on every request, and our approach no longer works. We believe maybe our token is being intercepted and overwritten on the way out the door. Any ideas on how to tweak our current method, or try a new one? We suspect it’s just a matter of overriding a method in VaadinWebSecurity, but not sure where to start. Many thanks!!

@marcoc_753 have you run into this before?

//Update a user's JWT token when they're granted a new role.
public void updateUserRoles(AppUser user,  HttpServletRequest request, HttpServletResponse response) {

        //Grab the current jwt.headerAndPayload cookie
        String jwt = getJwtFromCookies(request);
        if (jwt != null) {
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        	Jwt principal = (Jwt) auth.getPrincipal();
            Map<String, Object> currentClaims = principal.getClaims();
            Map<String, Object> updatedClaims = new HashMap<>();

            //Copy the current claims (except for iat) to a new claims map
            for (Map.Entry<String,Object> entry : currentClaims.entrySet()) {
                if (!entry.getKey().equals("iat")) {
                    updatedClaims.put(entry.getKey(), entry.getValue());
                }
            }
            
            String internalRole = user.getRoleAsString();


            List<String> roleList = List.of(internalRole.replace("ROLE_", ""));

            //Add the new role to the updated claims.
            updatedClaims.put("roles", roleList);

            //Wrap the claims up into a JWT and sign it.
            String newJwt = Jwts.builder()
                    .setClaims(updatedClaims)
                    .signWith(secretKey)
                    .setExpiration(Date.from(Instant.ofEpochSecond(4622470422L)))
                    .compact();

            //Update the user's cookies with their new role. 
            setJwtInCookies(newJwt, response);

            //Update the current session with the new role
            Authentication newAuth = new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(newAuth);
        }
    }

/**
     * Parse a JWT token into two cookies (jwt.headerAndPayload and jwt.signature)
     * Send the cookies in the servlet response
      * @param jwt
     * @param response
     */
    private void setJwtInCookies(String jwt, HttpServletResponse response) {
        String[] jwtParts = jwt.split("\\.");
        if (jwtParts.length == 3) {
            String headerAndPayload = jwtParts[0] + "." + jwtParts[1];
            String signature = jwtParts[2];

            Cookie headerAndPayloadCookie = new Cookie(JWT_HEADER_AND_PAYLOAD_COOKIE_NAME, headerAndPayload);
            headerAndPayloadCookie.setPath("/");
            headerAndPayloadCookie.setHttpOnly(true);
            headerAndPayloadCookie.setMaxAge(60 * 60 * 24); // 1 day
            Cookie signatureCookie = new Cookie(JWT_SIGNATURE_COOKIE_NAME, signature);
            signatureCookie.setPath("/");
            signatureCookie.setHttpOnly(true);
            signatureCookie.setMaxAge(60 * 60 * 24); // 1 day

            response.addCookie(headerAndPayloadCookie);
            response.addCookie(signatureCookie);
        }
    }

IIRC the JWT cookie is handled when the response is about to be committed, and it is computed based on information set in SecurityContextHolder.getContext().getAuthentication(), so as long as the Authentication.authorities is updated before the filter is applied, the cookie should be written with the new roles.
However, is a Set-Cookie header is already present, it will not be overwritten.

You can take a look and debug UpdateJWTCookieOnCommitResponseWrapper class to further investigate.

@marcoc_753 …good feedback… I will check this out!

@marcoc_753 You are right! I was actually making it more difficult than it needs to be. The one nuance seems to be, for example, changing a role in the beforeEnter method of a BeforeEnterObserver.

Specifically, running the code below anywhere other than beforeEnter seems to generate an updated JWT appropriately, but when it’s in beforeEnter, an updated JWT with the new role isn’t generated. Not sure if this is by design or a bug.

 List<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("ROLE_SOME_NEW_ROLE"));

        // Create a new Authentication object
        Authentication newAuth = new UsernamePasswordAuthenticationToken(
                "user",     // Principal (username)
                "user",     // Credentials (password or token)
                authorities // Granted authorities
        );

        SecurityContextHolder.getContext().setAuthentication(newAuth);

Hard to say without knowing the application. Vaadin access check is performed by a BeforeEnterListener, so before your observer is invoked.
However, if your observer re-routes to another view, the check is executed again and perhaps the new role is not accepted.
But this is only a blind guess.

BTW, why are you changing roles before navigation happens? What is triggering the role change? Did you consider to implement your logic in a servlet filter on the Spring security filter chain, if it really needs to be executed early?

@marcoc_753 …all good questions and comments. For the sake of forum hygiene, I’m going to open a separate post on the beforeEnter behavior and mark this one as closed unless you object. Have a great new year and thanks for all your help!