HTTPServlet POST/PUT... with no Session

Hello, I have already gained experience with the Spring Security Chain for my VNC proxy.

Furthermore I need 2 routes in my project, which should be accessible once with and once without authentication.

GET requests are always possible when you are authentificated, POST requests are rejected (403) although I send the CSRF token.

I have searched the forum and unfortunately cannot find a solution, even various articles on the Internet do not lead to a solution.

I wanted to push the topic again, maybe there are alternatives.

package de.bytestore.hostinger.api;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import de.bytestore.hostinger.entity.cms.Page;
import de.bytestore.hostinger.gson.templates.JSONStatus;
import de.bytestore.hostinger.gson.templates.JSONStatusMessage;
import de.bytestore.hostinger.handler.WebHandler;
import io.jmix.core.DataManager;
import io.jmix.core.JmixSecurityFilterChainOrder;
import io.jmix.core.querycondition.PropertyCondition;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.web.util.UriTemplate;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;

/**
 * BuilderAPIRoute is a HttpServlet that handles API routes related to a Builder functionality.
 *
 * This class provides methods for configuring security filters, registering servlets, and handling HTTP requests.
 * It also contains utility methods for extracting routes from URIs and generating JSON status messages.
 */
@Configuration
@WebServlet(value = "/internal/builder/{route}", loadOnStartup = 1)
public class BuilderAPIRoute extends HttpServlet {
    protected static final Logger log = LoggerFactory.getLogger(BuilderAPIRoute.class);
    protected final DataManager dataManager;

    public BuilderAPIRoute(DataManager dataManager) {
        this.dataManager = dataManager;
    }

    /**
     * Returns a ServletRegistrationBean for the example servlet.
     *
     * @return The ServletRegistrationBean for the example servlet.
     */
    @Bean
    public ServletRegistrationBean builderServletBean() {
        ServletRegistrationBean bean = new ServletRegistrationBean(
                this, "/internal/builder/*");
        bean.setLoadOnStartup(1);
        return bean;
    }

    @Bean(name = "builderSecurityFilterChain")
    @Order(JmixSecurityFilterChainOrder.FLOWUI - 10)
    // https://forum.jmix.io/t/static-resources-problem/2351/10
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(httpSecurityCsrfConfigurer -> {
            httpSecurityCsrfConfigurer.ignoringRequestMatchers(new AntPathRequestMatcher("/internal/builder/**"));
        });
        http.securityMatcher("/internal/builder/**")
                .authorizeHttpRequests((authorize) -> authorize.requestMatchers("/internal/builder/**").permitAll());
        return http.build();
    }



    /**
     * Handles a POST request and saves the page content to the database.
     *
     * @param requestIO  The HttpServletRequest object.
     * @param responseIO The HttpServletResponse object.
     * @throws ServletException If an error occurs during the request processing.
     * @throws IOException      If an I/O error occurs while handling the response.
     */
    @Override
    protected void doPost(HttpServletRequest requestIO, HttpServletResponse responseIO) throws ServletException, IOException {
        // Parse Route from Request.
        String routeIO  = extractRouteFromUri(requestIO.getRequestURI());


        this.handleCRSF(requestIO, responseIO);

        responseIO.getWriter().write("OK");
        responseIO.setStatus(HttpServletResponse.SC_OK);

//        try {
//            if (routeIO != null) {
//                // Get the Page Object of given Route.
//                de.bytestore.hostinger.entity.cms.Page pageIO = dataManager.load(de.bytestore.hostinger.entity.cms.Page.class).condition(PropertyCondition.equal("route", routeIO)).one();
//
//                if (pageIO != null) {
//                    // @todo: Add Permission Check.
//
//                    // Read Body of Request and set as Page Payload.
//                    pageIO.setContent(IOUtils.toString(requestIO.getReader()));
//
//                    // Save Page to Database.
//                    dataManager.save(pageIO);
//
//                    responseIO.setStatus(HttpServletResponse.SC_OK);
//                    responseIO.setContentType("application/json");
//                    responseIO.getWriter().write(new Gson().toJson(new JSONStatusMessage(JSONStatus.SUCCESS, "Page saved.")));
//                } else {
//                    notFound(responseIO);
//                }
//
//            } else {
//                notFound(responseIO);
//            }
//
//            } catch(Exception exceptionIO){
//                log.error("Unable to save Builder Template with Route '" + routeIO + "'.", exceptionIO);
//
//                notFound(responseIO);
//            }

    }

    /**
     * Sets the HTTP response status code to {@link HttpServletResponse#SC_INTERNAL_SERVER_ERROR} and writes a JSON status message to the response body indicating that the route was
     * not found.
     * The JSON status message is generated using {@link Gson} library.
     *
     * @param responseIO the HTTP servlet response object
     * @throws IOException if an I/O error occurs while writing the response
     */
    private void notFound(HttpServletResponse responseIO) throws IOException {
        responseIO.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        responseIO.setContentType("application/json");
        responseIO.getWriter().write(new Gson().toJson(new JSONStatusMessage(JSONStatus.ERROR, "Unable to find route.")));
    }

    /**
     * Handles a GET request and retrieves the page content based on the requested route.
     *
     * @param requestIO  The HttpServletRequest object.
     * @param responseIO The HttpServletResponse object.
     * @throws ServletException If an error occurs during the request processing.
     * @throws IOException      If an I/O error occurs while handling the response.
     */
    @Override
    protected void doGet(HttpServletRequest requestIO, HttpServletResponse responseIO) throws ServletException, IOException {
        // Parse Route from Request.
        String routeIO = extractRouteFromUri(requestIO.getRequestURI());

        this.handleCRSF(requestIO, responseIO);

        if (routeIO != null && routeIO.equalsIgnoreCase("listPages")) {
            // Get all Pages Objects.
            List<Page> pagesIO = dataManager.load(Page.class).all().list();

            // Create new JSON Response object.
            JSONStatusMessage statusIO = new JSONStatusMessage(JSONStatus.SUCCESS, new JsonObject());

            // Create new Array Buffer.
            JsonArray arrayIO =  new JsonArray();

            // Loop through JPA Entries.
            pagesIO.forEach(page -> {
                JsonObject objectIO  = new JsonObject();

                objectIO.addProperty("route", page.getId());
                objectIO.addProperty("name", page.getName());

                arrayIO.add(objectIO);
            });

            // Add Array to Response.
            statusIO.getValue().add("pages", arrayIO);

            responseIO.setContentType("application/json");
            responseIO.getWriter().write(new Gson().toJson(statusIO));

        } else {
            try {
                // Get the Page Object of given Route.
                Page pageIO = dataManager.load(Page.class).condition(PropertyCondition.equal("id", routeIO)).one();

                if (pageIO !=  null) {
                    // @todo: Add Permission Check.

                    // Return Body of Page.
                    responseIO.setContentType("text/html");

                    PrintWriter writerIO = responseIO.getWriter();
                    writerIO.print(pageIO.getContent());
                    writerIO.flush();
                    writerIO.close();
                } else {
                    this.notFound(responseIO);
                }

            } catch (Exception exceptionIO) {
                log.error("Unable to load Builder Template with Route '" + routeIO + "'.", exceptionIO);

                this.notFound(responseIO);
            }

//        responseIO.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
//        responseIO.getWriter().write(new Gson().toJson(new JSONStatusMessage(JSONStatus.ERROR, "Please use POST Method.")));
        }
    }

    private void handleCRSF(HttpServletRequest requestIO, HttpServletResponse responseIO) {
        CsrfToken tokenIO = WebHandler.getCSRFToken(requestIO);

        if(tokenIO != null) {
            // Create new CRSF Cookie.
            Cookie cookieIO = new Cookie("XSRF-TOKEN",  tokenIO.getToken());

            // Add Wilcard.
            cookieIO.setPath("/");

            // Add Cookie to Response.
            responseIO.addCookie(cookieIO);
        }  else  {
            // Print Warn / Debug Message.
            log.warn("Can't find CSRF token for request.");
        }
    }


    /**
     * Extracts the route from the given URI using the provided UriTemplate.
     *
     * @param uri The URI from which to extract the route.
     * @return The extracted route.
     */
    private String extractRouteFromUri(String uri) {
        UriTemplate template = new UriTemplate("/internal/builder/{route}");
        return template.match(uri).get("route");
    }
}

package de.bytestore.hostinger.handler;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.web.csrf.CsrfToken;

public class WebHandler {
    /**
     * Retrieves the CSRF token from the provided HttpServletRequest object.
     *
     * @param requestIO The HttpServletRequest object from which to retrieve the CSRF token.
     * @return The CSRF token.
     */
    public static CsrfToken getCSRFToken(HttpServletRequest requestIO) {
        return (CsrfToken) requestIO.getAttribute(CsrfToken.class.getName());
    }
}

Error:
image

I have also tried to switch off CRSF, but apparently this is not recognized / overwritten.

Hi,

Sorry, but it’s not fully clear for me what you’re trying to achieve and what problems you’re facing.

Could you please reproduce your issue in a small very simple sample project? Then attach it here and provide steps to get the wrong behavior and describe expected behavior.

Another question: why do you need a separate servlet?

Hello Maxim,

I have created an example, please have a look at the “README.md” first, maybe this already explains my problem.

I have described two of my examples, thank you for your help.

If you have any other questions, please do not hesitate to contact me…

jmix-example.zip (99,7 KB)

I don’t see a reason for creating separate servlets for this. If I understand your task right and all you need is to have GET and POST endpoints that should be available without authentication then the following should work.

Create a regular REST controller:

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/internal/payment")
public class PaymentController {

    @GetMapping("/{provider}/pay")
    public PaymentResponse paymentGet(@PathVariable String provider) {
        if ("paypal".equals(provider)) {
            return new PaymentResponse("OK");
        } else {
            return new PaymentResponse("ERROR");
        }
    }

    @PostMapping("/{provider}/pay")
    public PaymentResponse paymentPost(@PathVariable String provider) {
        if ("paypal".equals(provider)) {
            return new PaymentResponse("OK");
        } else {
            return new PaymentResponse("ERROR");
        }
    }

    public record PaymentResponse(String status) {}
}

Create a security configuration for these endpoints:

import io.jmix.core.JmixSecurityFilterChainOrder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class PaymentSecurityConfiguration {

    @Bean
    @Order(JmixSecurityFilterChainOrder.FLOWUI - 10)
    SecurityFilterChain paymentSecurityFilterChain(HttpSecurity http) throws Exception {
        http.securityMatcher("/internal/payment/**")
                .authorizeHttpRequests(requests ->
                        requests.anyRequest().permitAll())
                .csrf(csrf -> csrf.disable());
        return http.build();
    }
}

Here is the project that demonstrates this:
payment-builder-sample.zip (101.5 KB)

The following requests work fine:

curl -X POST http://localhost:8080/internal/payment/paypal/pay
curl -X GET http://localhost:8080/internal/payment/paypal/pay

Thank you, that helped me a lot!