Search code examples
sap-cloud-platformsap-cloud-sdks4hana

How do I handle the need for CSFR token when using SAP Cloud SDK?


I am using the SAP Cloud SDK for Java to do CRUD on the SalesOrder APIs in S/4. Everything works well in that I can carry out these actions from Postman. However, these requests from Postman only work if I include a pre-request script to get a csrf token as outlined in this blog post

If I run the requests without the pre-request script outlined in the blog post, I get a '403 Forbidden'. As I said it works from Postman, but I would like to understand how this should be handled without the need for this script, for example if I was making a request from another application. Does the SDK allow me to handle this from the application code somehow. Maybe I am missing something.

Thanks for your time.

EDIT: I am not making requests to the S/4 directly from Postman. I have an app deployed which is using the Cloud SDK to make the requests to S/4. It works if I use the pre-request script to fetch the CSFR token and attach it to the request before I send it, but 403 if I don't. So, if we imagine I am not using Postman but some ui somewhere to fill a form and send this request my understanding is that I shouldn't, as you suggested, have to worry about this token, that my service in the middle which uses the SDK and the VDM should handle this for me. This is what I am struggling to understand. enter image description here

This is the servlet code:

@Override
protected void doPost(final HttpServletRequest request, final HttpServletResponse response)
        throws ServletException, IOException {

    String body = IOUtils.toString(request.getReader());
    JSONObject so = new JSONObject(body);
    String distributionChannel = so.get("DistributionChannel").toString();
    String salesOrderType = so.get("SalesOrderType").toString();
    String salesOrganization = so.get("SalesOrganization").toString();
    String soldToParty = so.get("SoldToParty").toString();
    String organizationDivision = so.get("OrganizationDivision").toString();
    String material = so.get("Material").toString();
    String requestedQuantityUnit = so.get("RequestedQuantityUnit").toString();

    SalesOrderItem salesOrderItem = SalesOrderItem.builder()
    .material(material)
    .requestedQuantityUnit(requestedQuantityUnit).build();

    SalesOrder salesOrder = SalesOrder.builder()
    .salesOrderType(salesOrderType)
    .distributionChannel(distributionChannel)
    .salesOrganization(salesOrganization)
    .soldToParty(soldToParty)
    .organizationDivision(organizationDivision)
    .item(salesOrderItem)
    .build();

    try {
        final ErpHttpDestination destination = DestinationAccessor.getDestination(DESTINATION_NAME).asHttp()
                .decorate(DefaultErpHttpDestination::new);
        final SalesOrder storedSalesOrder = new CreateSalesOrderCommand(destination, new DefaultSalesOrderService(),
                salesOrder).execute();
        response.setStatus(HttpServletResponse.SC_CREATED);
        response.setContentType("application/json");
        response.getWriter().write(new Gson().toJson(storedSalesOrder));
        logger.info("Succeeded to CREATE {} sales order", storedSalesOrder);

    } catch (final Exception e) {
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        logger.error(e.getMessage(), e);
        logger.error("Failed to CREATE sales order", e);
    }
}

And the CreateSalesOrder command:

public SalesOrder execute() {
    return ResilienceDecorator.executeSupplier(this::run, myResilienceConfig);
}

protected SalesOrder run() {
    try {
        return salesOrderService.createSalesOrder(salesOrder).execute(destination);
    } catch (final ODataException e) {
        throw new ResilienceRuntimeException(e);
    }
}

I am using the version 3.16.1 of the SDK and have set logging level to DEBUG for the SDK in the manifest:

SET_LOGGING_LEVEL: '{ROOT: INFO, com.sap.cloud.sdk: DEBUG}'

and logging level to DEBUG in logback

If I remove the pre-request script from the request and send it I get the 403 response and logs shows the following messages:

"logger":"com.sap.cloud.sdk.service.prov.api.security.AuthorizationListener","thread":"http-nio-0.0.0.0-8080-exec-4","level":"DEBUG","categories":[],"msg":"Reading user principal"

"logger":"com.sap.cloud.sdk.service.prov.api.security.AuthorizationListener","thread":"http-nio-0.0.0.0-8080-exec-4","level":"DEBUG","categories":[],"msg":"Destroying Authorization as it is end of request." }

"logger":"com.sap.cloud.sdk.service.prov.api.security.AuthorizationService","thread":"http-nio-0.0.0.0-8080-exec-4","level":"DEBUG","categories":[],"msg":"Destroying Authorization JWT Token." }


Solution

  • As the other answers focus on the app to S/4 communication and you adjusted your question to make clear that you mean the User (e.g. Postman) to app communication I'll provide some additional information.

    As mentioned by the other answers the CSRF handling to the S/4 system (or any OData endpoint) is automatically handled on side of the OData VDM.

    What you are now encountering is the secure default configuration of the SAP Cloud SDK Maven Archetypes, which have the RestCsrfPreventionFilter activated by default. This filter automatically protects all non-GET endpoints from CSRF by requiring you to fetch a CSRF Token prior to your request which you then provide. This is completely unrelated to the OData VDM call to the S/4 system in the background.

    To remedy your problems there are now three next steps:

    • Use a GET endpoint instead of POST
      • Probably only as a temporary workaround
    • Remove the RestCsrfPreventionFilter temporarily from your web.xml
      • This should not be done for productive uses, but might make your life in the initial evaluation easier.
    • "Live with it"
      • As this is a commonly used pattern to protect your application against CSRF it's advised to keep the filter in place and do the CSRF-Token "flow" as required.

    Further Reading