Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Revamp the session interceptor to work when submission is unset. #557

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.web.filter.ForwardedHeaderFilter;

Expand Down Expand Up @@ -40,17 +41,29 @@ public DefaultCookieSerializer setDefaultSecurityCookie() {
*/
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.formLogin(AbstractHttpConfigurer::disable);
httpSecurity
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement(sessionManagementConfigurer -> {
sessionManagementConfigurer
.invalidSessionUrl("/?sessionInvalid=true")
.maximumSessions(1)
.expiredUrl("/?sessionExpired=true");
});
return httpSecurity.build();
}

/**
* Use X-Forwarded-For / X-Forwarded-Proto headers when generating full link URLs.
*
*
* @return ForwardedHeaderFilter object
*/
@Bean
public ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter();
}

@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package formflow.library.config;

import formflow.library.interceptors.SessionContinuityInterceptor;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
Expand All @@ -17,15 +20,18 @@ public class SessionContinuityInterceptorConfiguration implements WebMvcConfigur

@Autowired
List<FlowConfiguration> flowConfigurations;



@Value("${form-flow.session-continuity-interceptor.redirect-url:/}")
private String redirectUrl;

/**
* Adds the SessionContinuityInterceptor to the Interceptor registry.
*
* @param registry the Interceptor registry.
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SessionContinuityInterceptor(flowConfigurations))
registry.addInterceptor(new SessionContinuityInterceptor(flowConfigurations, redirectUrl))
.addPathPatterns(List.of(SessionContinuityInterceptor.FLOW_PATH_FORMAT,
SessionContinuityInterceptor.NAVIGATION_FLOW_PATH_FORMAT));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package formflow.library.filters;

import formflow.library.ScreenController;
import formflow.library.config.FlowConfiguration;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

@Component
public class SessionExpirationFilter extends OncePerRequestFilter {
private static final Set<String> SAFE_METHODS = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
public static final String FLOW_PATH_FORMAT = ScreenController.FLOW + "/" + ScreenController.FLOW_SCREEN_PATH;
public static final String NAVIGATION_FLOW_PATH_FORMAT = FLOW_PATH_FORMAT + "/navigation";
public List<FlowConfiguration> flowConfigurations;

public SessionExpirationFilter(List<FlowConfiguration> flowConfigurations) {
this.flowConfigurations = flowConfigurations;
}

@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain)
throws ServletException, IOException {

if (SAFE_METHODS.contains(request.getMethod())) {
filterChain.doFilter(request, response);
return;
}

String path = request.getRequestURI();
if ("/".equals(path) || "/index".equals(path)) {
filterChain.doFilter(request, response);
return;
}

String pathFormat = path.contains("navigation") ? NAVIGATION_FLOW_PATH_FORMAT : FLOW_PATH_FORMAT;
Map<String, String> parsedUrl = new AntPathMatcher().extractUriTemplateVariables(pathFormat, request.getRequestURI());

HttpSession session = request.getSession(false);

FlowConfiguration flowConfiguration = flowConfigurations.stream()
.filter(fc -> fc.getName().equals(parsedUrl.get("flow")))
.findFirst()
.orElse(null);


// Check if the session is null or expired
if (session == null) {
// Redirect to the session expired page
if (flowConfiguration != null && !parsedUrl.get("screen").equals(flowConfiguration.getLandmarks().getFirstScreen())) {
response.sendRedirect("/?sessionExpired=true");
return;
}
}
// Continue the filter chain
filterChain.doFilter(request, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,36 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.HandlerInterceptor;

/**
* This interceptor prevents users from jumping to random pages in a flow.
* This interceptor prevents users with an invalid session from jumping to random pages in a flow.
*/
@Component
@Slf4j
@ConditionalOnProperty(name = "form-flow.session-continuity-interceptor.enabled", havingValue = "true")
public class SessionContinuityInterceptor implements HandlerInterceptor, Ordered {

public static final String FLOW_PATH_FORMAT = ScreenController.FLOW + "/" + ScreenController.FLOW_SCREEN_PATH;
public static final String NAVIGATION_FLOW_PATH_FORMAT = FLOW_PATH_FORMAT + "/navigation";

private static final String REDIRECT_URL = "/";

private final String redirectUrl;
public List<FlowConfiguration> flowConfigurations;

public SessionContinuityInterceptor(List<FlowConfiguration> flowConfigurations) {
public SessionContinuityInterceptor(List<FlowConfiguration> flowConfigurations, String redirectUrl) {
this.flowConfigurations = flowConfigurations;
this.redirectUrl = redirectUrl;
}

/**
Expand All @@ -42,12 +44,13 @@ public SessionContinuityInterceptor(List<FlowConfiguration> flowConfigurations)
* @param handler chosen handler to execute, for type and/or instance evaluation
* @return Boolean True - allows the request to proceed to the ScreenController, False - stops the request from reaching the
* Screen Controller.
* @throws IOException - thrown in the event that an input or output exception occurs when this method does a
* redirect.
* @throws IOException - thrown in the event that an input or output exception occurs when this method does a
* redirect.
*/
@Override
public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler)
throws IOException {

String pathFormat = request.getRequestURI().contains("navigation") ? NAVIGATION_FLOW_PATH_FORMAT : FLOW_PATH_FORMAT;
Map<String, String> parsedUrl = new AntPathMatcher().extractUriTemplateVariables(pathFormat, request.getRequestURI());

Expand All @@ -68,16 +71,21 @@ public boolean preHandle(HttpServletRequest request, @NotNull HttpServletRespons
if (parsedUrl.get("screen").equals(firstScreen)) {
return true;
}
log.error("No active session found for request to {}. Redirecting to landing page.", request.getRequestURI());
response.sendRedirect(REDIRECT_URL);
log.warn("No active session found for request to {}. Redirecting to landing page.", request.getRequestURI());
response.sendRedirect(redirectUrl);
return false;
}

if (FormFlowController.getSubmissionIdForFlow(session, parsedUrl.get("flow")) == null &&
!parsedUrl.get("screen").equals(firstScreen)) {
log.error("A submission ID was not found in the session for request to {}. Redirecting to landing page.",
UUID submissionId = null;
try {
submissionId = FormFlowController.getSubmissionIdForFlow(session, parsedUrl.get("flow"));
} catch (ResponseStatusException ignored) {
}

if (submissionId == null && !parsedUrl.get("screen").equals(firstScreen)) {
log.warn("A submission ID was not found in the session for request to {}. Redirecting to landing page.",
request.getRequestURI());
response.sendRedirect(REDIRECT_URL);
response.sendRedirect(redirectUrl);
return false;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package formflow.library.listeners;

import org.springframework.context.event.EventListener;
import org.springframework.security.web.session.HttpSessionCreatedEvent;
import org.springframework.security.web.session.HttpSessionDestroyedEvent;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class SessionListenerLogger {

@EventListener
public void onSessionCreated(HttpSessionCreatedEvent event) {
log.info("Session created: ID={}", event.getSession().getId());
}

@EventListener
public void onSessionDestroyed(HttpSessionDestroyedEvent event) {
log.info("Session destroyed: ID={}", event.getId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import formflow.library.config.FormFlowConfigurationProperties;
import formflow.library.config.FlowConfiguration;

import java.util.List;

import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
Expand All @@ -15,34 +18,37 @@
@TestConfiguration
public class SpyInterceptorConfig implements WebMvcConfigurer {

@Autowired
private List<FlowConfiguration> flowConfigurations;

@Autowired
private FormFlowConfigurationProperties formFlowConfigurationProperties;

@Bean
@Primary
public LocaleChangeInterceptor localeChangeInterceptor() {
return Mockito.spy(new LocaleChangeInterceptor());
}

@Bean
@Primary // Ensure this bean takes precedence over the real one
public SessionContinuityInterceptor dataRequiredInterceptor() {
return Mockito.spy(new SessionContinuityInterceptor(flowConfigurations));
}

@Bean
@Primary
public DisabledFlowInterceptor disabledFlowInterceptor() {
return Mockito.spy(new DisabledFlowInterceptor(formFlowConfigurationProperties));
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
registry.addInterceptor(dataRequiredInterceptor());
registry.addInterceptor(disabledFlowInterceptor());
}
@Autowired
private List<FlowConfiguration> flowConfigurations;

@Autowired
private FormFlowConfigurationProperties formFlowConfigurationProperties;

@Value("${form-flow.session-continuity-interceptor.redirect-url:/}")
private String redirectUrl;

@Bean
@Primary
public LocaleChangeInterceptor localeChangeInterceptor() {
return Mockito.spy(new LocaleChangeInterceptor());
}

@Bean
@Primary // Ensure this bean takes precedence over the real one
public SessionContinuityInterceptor dataRequiredInterceptor() {
return Mockito.spy(new SessionContinuityInterceptor(flowConfigurations, redirectUrl));
}

@Bean
@Primary
public DisabledFlowInterceptor disabledFlowInterceptor() {
return Mockito.spy(new DisabledFlowInterceptor(formFlowConfigurationProperties));
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
registry.addInterceptor(dataRequiredInterceptor());
registry.addInterceptor(disabledFlowInterceptor());
}
}
Loading