How to invalidate HTTP session due to user inactivity in Vaadin/Spring Boot?

I am trying to implement an inactive session expiry in my Vaadin application using OKTA for auth.

Right now, the application shows this build-in dialogue (I set the text) after the server.servlet.session.timeout is reached:

image

The issue is that the JSESSIONID (i.e. the HTTP session) does not change/get recreated after the user clicks on the window/presses escape which currently results in the user getting logged in again. That happens as the code “sees” a valid OKTA session and logs back the user automatically.

How do I make sure that the HTTP session gets terminated/recreated as well when the session expires?

Here is my SecurityConfiguration:

@EnableWebSecurity
@Configuration
@Order(99)
public class S1SecurityConfiguration extends SecurityConfiguration {
    ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http); // Apply default configurations first - it sets up anonymous-user handing

        http.oauth2Login(oauth2 - >
            oauth2
            .userInfoEndpoint(userInfo - > userInfo.oidcUserService(oidcUserService()))
            .authorizationEndpoint(authEndpoint - > authEndpoint
                .authorizationRequestResolver(
                    new ForcePromptLoginRequestResolver(
                        clientRegistrationRepository,
                        "/oauth2/authorization"
                    )
                )
            )
            .successHandler(s1authSuccessHandler)
            .failureHandler(authFailureHandler)

        );

        // Finally, enable concurrency in session management
        http.sessionManagement(sessionManagement - >
            sessionManagement
            .sessionFixation(sessionFixation - > sessionFixation.migrateSession())
            .sessionConcurrency(sessionConcurrency - >
                sessionConcurrency
                .sessionRegistry(sessionRegistry())
                .maximumSessions(1)
                .maxSessionsPreventsLogin(false) // second login is allowed, but will invalidate the first
                .expiredUrl("/session-expired") // redirect to this page if the session is expired
            )
        );

        ...
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    public SessionAuthenticationStrategy sessionAuthenticationStrategy(SessionRegistry sessionRegistry) {
        // Concurrency strategy
        ConcurrentSessionControlAuthenticationStrategy concurrencyStrategy =
            new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry);
        concurrencyStrategy.setMaximumSessions(1);
        concurrencyStrategy.setExceptionIfMaximumExceeded(true); // same as maxSessionsPreventsLogin(true)

        // Combine concurrency + session fixation protection
        return new CompositeSessionAuthenticationStrategy(
            Arrays.asList(
                new ChangeSessionIdAuthenticationStrategy(), // or MigrateSession
                concurrencyStrategy
            )
        );
    }

    @Bean
    public S1AuthenticationSuccessHandler s1authFailureHandler(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
        return new S1AuthenticationSuccessHandler(sessionAuthenticationStrategy);
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }

    @Bean
    public OAuth2UserService < OidcUserRequest,
    OidcUser > oidcUserService() {
        final OidcUserService delegate = new OidcUserService();
        return userRequest - > {
            // Load the default OidcUser via the delegate
            OidcUser oidcUser = delegate.loadUser(userRequest);

            String principalName = oidcUser.getAttribute("email");

            // Return a SimpleOidcUser that uses only or email for equality checks (to make the concurrency check work)
            return new SimpleOidcUser(
                oidcUser.getAuthorities(),
                oidcUser.getIdToken(),
                oidcUser.getUserInfo(),
                principalName
            );
        };
    }
}

application.properties:

server.servlet.session.timeout=30m
# set closeIdleSessions to true so heartbeat/push requests do not keep resetting the above session inactivity timer
vaadin.closeIdleSessions=true

SessionExpiredMessageInitServiceListener.java

@Component
public class SessionExpiredMessageInitServiceListener implements VaadinServiceInitListener {

    @Override
    public void serviceInit(ServiceInitEvent event) {
        event.getSource().setSystemMessagesProvider(new SystemMessagesProvider() {
            @Override
            public CustomizedSystemMessages getSystemMessages(SystemMessagesInfo systemMessagesInfo) {
                CustomizedSystemMessages messages = new CustomizedSystemMessages();
                messages.setSessionExpiredCaption("Session expired");
                messages.setSessionExpiredMessage(
                        "Your session has expired. Press ESC or click anywhere in this window to continue."
                );
                // If you have a static page or route for session-expired:
                messages.setSessionExpiredURL("/envdata");
                messages.setSessionExpiredNotificationEnabled(true);
                return messages;
            }
        });
    }
}

Note: The JSESSIONID token stays the same even after the next request after I click on the Session Expired dialog above.

Not an expert with OKTa, but quickly checking you have two different session expired urls, one in Spring Security configs and one in Vaadin configs, is that on purpose? Somehow you should probably invalidate the “OKTA session” when a user is redirected to session expired url :thinking:

Hi Matti, thanks for your response. Good point, so how I understand it (I am new to Vaadin/Springboot altogether) the /session-expired page will show when someone tries to use an expired session (e.g. an attacker). In contrast, the SessionExpiredMessageInitServiceListener class shows this dialog when the session expires to the user.

Invalidating the OKTA session is not something I am considering now.

In another light, I reached out to Vaadin Expert Chat and they suggested adding server.servlet.session.cookie.max-age config parameter to my application.properties file which resolved the issue with the HTTP session/JSESSIONID not refreshing after the vaadin session expired.

Example application.properties file:

server.servlet.session.timeout=15m
server.servlet.session.cookie.max-age=15m

Update:

That does not seem to resolve the issue as the session is now invalidated even though the user is actively using the site.

As the “OKTA session” is independent of the servlet session, so I don’t know if there is easy other way around invalidating that (without completely designing this part by yourself, which I can’t suggest unless you really know what you are doing).

Shorter servlet session as configured above just closes that earlier than expected (although it sounds “unexpected” that it gets closed although actively using, that shouldn’t happen).

Got it, thanks, Matti. What would be a “standard” way to invalidate the OKTA session for a user?

I know that we will have to make that request to invalidate a particular OKTA session (see below) but I am struggling to understand when and how I can make that request when the Vaadin (and/or raw HTTP) session expires.

https://{myDomain}/oauth2/default/v1/logout?id_token_hint={idToken}&post_logout_redirect_uri=https://myapp/logout

Note: I remember that I tried to make that request when I first started working on the above issue but I hit a blocker not being “hook into” the Springboot request pipeline and making the request at the time correct time so that the user lands on the /session-expired page

I’d need to build a complete test case (and open OKTA docs) to give you perfect answers, but may maybe you can forward user to that (or some other) “logout url” from your web server when its session is timed out. Based on that template you can then configure that to make another redirect back to your apps URL again.

I think this is what you are looking for OIDC Logout :: Spring Security.

BTW, since 24.7, VaadinWebSecurity will automatically set the oidcLogoutSuccessHandler when using the setOAuth2LoginPage() method.

1 Like

Hi Marco, thanks for your suggestion. Looking at the docs, adding LogoutSuccessHandler alone does not trigger it on session expiry due to inactivity - only when the user nagicates to logout page.

The logic to trigger the logout when the Vaadin session expires will need to be manually added and I am not sure what’s the best way to do that.

One idea that chatgpt suggested to me is to use a Custom InvalidSessionStrategy:

http.sessionManagement(session -> session
    .invalidSessionStrategy(new MyInvalidSessionStrategy())
    // or .invalidSessionUrl("/myInvalidSessionUrl")
);

public class MyInvalidSessionStrategy implements InvalidSessionStrategy {
    private LogoutSuccessHandler oidcLogoutHandler; // e.g., OidcClientInitiatedLogoutSuccessHandler

    public MyInvalidSessionStrategy(LogoutSuccessHandler oidcLogoutHandler) {
        this.oidcLogoutHandler = oidcLogoutHandler;
    }

    @Override
    public void onInvalidSessionDetected(HttpServletRequest request,
                                         HttpServletResponse response)
            throws IOException, ServletException {
        // Call the OIDC logout handler’s onLogoutSuccess
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        oidcLogoutHandler.onLogoutSuccess(request, response, auth);
    }
}

But I am not sure if that is going to work or if it’s a “standard” way to go in my case.

@georgi.koemdzhiev You’re right, sorry for the wrong suggestion.

I don’t know OKTA, but I wonder if you can set up there a session/idle timeout per application, with a value smaller than the Vaadin app HTTP session timeout.

From my tests, if the underling HTTP session is invalidated (that invalidates the Vaadin session as it’s build on top of it) and that causes the Spring Security/OKTA to require a new login.

So keeping both the session expiry and HTTP session the same should be the approach. However, I am not sure yet how to tell Spring Boot to expire the HTTP session when the Vaadin session expires.

What do you mean by “when Vaadin session expires”?
Vaadin session does not have any expiration logic. It can be closed programmatically or automatically when the HTTP session expires (VaadinSession is stored into HTTP session).

I wonder if you can configure OKTA to have the same idle/session timeout as the application. Perhaps you can set a policy in OKTA per application, and not only globally.

Above I meant when the Vaadin session expires which is controlled by this parameter server.servlet.session.timeout=15m

OKTA does not have the concept of “User being inactive” as I know. That must be configured/controlled by the app itself.

server.servlet.session.timeout determines the HTTP session timeout.

Quickly searching on the Internet, it looks like Okta has the possibility to define a session lifetime and expiration settings.

However, not being an Okta user, I’m not sure if the linked documentation could help or not.

1 Like

Hi Marco,

That’s a good find, thank you!

While the server.servlet.session.timeout property controls the HTTP session timeout on our side, Okta’s session lifetime and expiration settings are managed separately as per my understanding. They only refresh when there’s direct activity on Okta’s endpoints—not necessarily when the user is active in our app.

So using that OKTA setting won’t be helping in my case as my goal is to invalidate the Okta/HTTP session only when the user isn’t actively using our application, relying solely on Okta’s parameters won’t achieve that, since those settings don’t automatically extend the session based on in-app activity.

After reading the whole thread… am I missing something or why aren’t you using Final: OpenID Connect RP-Initiated Logout 1.0? That’s exactly for this use case.

I agree with Christian.
If you want the user to be logged out from the IDP when the local application session expires, then RP-initiated logout seems the proper solution,
Otherwise, you need to find a way to make the IDP session shorted than the local application, otherwise even if you logout and destroy the application session, you will be immediately logged in again if you navigate to a protected page and the IDP session is still active.

Thank you both. @knoobie. Question Re: RP-Initiated Logout method. If I setup that, would the OKTA logout trigger when the session expires?

That’s one of the bits I am not sure how to do at the moment, how (and where) to fire that logout OKTA sessions. That does seem to resolve the “how” but I am not sure if that will fire also when the Vaadin session expires automatically or the user specifically/manually needs to press a logout button for that to fire.

An extract from the docs:

“An RP requests that the OP log out the End-User by redirecting the End-User’s User Agent to the OP’s Logout Endpoint.”
— OpenID Connect RP-Initiated Logout 1.0

This shows that the logout must be explicitly initiated by your application which is not what I want. I want the user to be logged out from OKTA when their session expires.

That part is up to you.

Pseudo code:

  • implement http session listener
  • On session end
  • call okta

I tried to make a call to the OKTA’s logout endpoint:

https://{yourOktaDomain}/oauth2/default/v1/logout?id_token_hint={ID_TOKEN}&post_logout_redirect_uri={REDIRECT_URI}&state={STATE}

Using a VaadinSericeInitListener:

@Component
public class HttpSessionInvalidationOnVaadinSessionDestroy implements VaadinServiceInitListener {
    @Override
    public void serviceInit(ServiceInitEvent event) {
        // Add a listener to invalidate the HTTP session when a Vaadin session is destroyed.
        event.getSource().addSessionDestroyListener(new SessionDestroyListener() {
            @Override
            public void sessionDestroy(SessionDestroyEvent event) {
                LogoutuserFromOKTA();
            }
        });
    }
}

However, the above approach does not work with the Vaadin’s routing and the user is not taken to the OKTA’s logging page