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.
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.