admin管理员组

文章数量:1290957

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:

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;
            }
        });
    }
}

UPDATE:

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 of the HTTP session/JSESSIONID not refreshing but it forces the user to login again - i.e. it's not respecting the activity of the user.

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:

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;
            }
        });
    }
}

UPDATE:

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 of the HTTP session/JSESSIONID not refreshing but it forces the user to login again - i.e. it's not respecting the activity of the user.

Share Improve this question edited Feb 18 at 17:01 Doug Breaux 5,1151 gold badge27 silver badges68 bronze badges asked Feb 13 at 16:10 Gei KoemdzhievGei Koemdzhiev 11.9k18 gold badges73 silver badges146 bronze badges 7
  • 1 With your update, it's unclear to me exactly what behavior you're wanting. You do or do not want the user to have to login again? I guess, what's the exact flow you're trying to achieve, and where is that currently failing? – Doug Breaux Commented Feb 18 at 14:02
  • Hi Doug, good question. I do want to user to log in again after the session expires unless they are actively using the site. Essentially, I want the user to be logged out due to inactivity and their Vaadin/HTTP session invalidated. – Gei Koemdzhiev Commented Feb 18 at 14:05
  • 1 Ah, ok. So with the original setup, it's not requiring the user to login again, with the new change, it's not keeping the session alive with user activity, correct? (Which makes sense, as the cookie's age is a static value.) With the original, is the HTTP session actually invalidated, but the JSSESSIONID value is still known to something else? That is, the Okta service you're using? If so, you might want to look for a way to explicitly tell it to fet the ID? I'm guessing, I don't use most of this stuff, just Spring, without Spring Boot. – Doug Breaux Commented Feb 18 at 14:20
  • Yes, correct. When I added server.servlet.session.cookie.max-age, the HTTP session is not kept alive by the user action/requests. Before I added server.servlet.session.cookie.max-age the JSESSIONID was not getting refreshed/invalidated (which is a vulnerability I am trying to resolve). What I am trying to figure out is to make sure that the JSESSIONID/http session is invalided only due to inactivity. – Gei Koemdzhiev Commented Feb 18 at 15:50
  • 1 Right, but I suspect the session actually is invalidated with the application server, but recreated automatically, already authenticated, because of the Okta integration. Or, at least, that's what I'm trying to establish. Can you confirm that the original HTTP session is actually invalidated? Or if it's not, that would be a different kind of resolution than if it is, but the Okta session isn't. – Doug Breaux Commented Feb 18 at 16:05
 |  Show 2 more comments

1 Answer 1

Reset to default 0

Here’s what I did in the end to resolve the issue of the HTTP (JSESSIONID cookie) not invalidating when the user has been idle for a set time:

I added a Logout endpoint:

@Controller
public class LogoutController {
    @Value("${okta.oauth2.issuer}")
    private String oktaDomain;

    @Value("${okta.post.logout.redirect.uri}")
    private String oktaLogoutUrl;

    @GetMapping("/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) {

        var idTokenObj = request.getSession().getAttribute("idToken");
        if (idTokenObj == null) {
            // Redirect to OKTA logout endpoint
            return "redirect:/envdata";
        }
        var idToken = idTokenObj.toString();

        // Invalidate the HTTP session
        request.getSession().invalidate();

        // Clear the security context
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null) {
            new SecurityContextLogoutHandler().logout(request, response, auth);
        }

        String logoutUrl = oktaDomain + "/v1/logout" +
                "?id_token_hint=" + URLEncoder.encode(idToken, StandardCharsets.UTF_8) +
                "&post_logout_redirect_uri=" + oktaLogoutUrl;

        // Redirect to OKTA logout endpoint
        return "redirect:" + logoutUrl;
    }
}

Changed my SessionExpiredMessageInitServiceListener class to redirect to that endpoint - /logout after the user clicks on the “Session Expired” dialog (or presses ESC):

@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("/logout");
                messages.setSessionExpiredNotificationEnabled(true);
                return messages;
            }
        });
    }
}

With the above 2 additional changes the HTTP session is successfully invalidated and and the user is required to re-login using OKTA and JSESSIONID cookie is refreshed as I wanted.

本文标签: How to invalidate HTTP session due to user inactivity in VaadinSpring BootStack Overflow