1

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

Inkers
  • 219
  • 7
  • 27

3 Answers3

2

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

Christoph Schubert
  • 1,089
  • 1
  • 8
  • 16
1

Inkers

You're correct, with an API tool like Postman you have to make a HEAD request first to get a CSRF token.

However, in Cloud SDK for Java, we take care of getting and refreshing CSRF token for you when you're making any CRUD request.

Here's an example of reading a Saler Oder item and updating it afterward:

// Create a new sales order item
SalesOrderItem item = new SalesOrderItem();
item.setSalesOrder(SALES_ORDER);
item.setNetAmount(new BigDecimal(NET_VALUE));
item = service.createSalesOrderItem(item).execute(destination).getResponseEntity().get();

// Modify it with a PATCH update to 9000 net value
item.setNetAmount(new BigDecimal(NET_VALUE_UPDATED));
ModificationResponse<SalesOrderItem> response = service.updateSalesOrderItem(item).modifyingEntity().execute(destination);

Try it and let up know if it works fine for you. We're happy to assist if you'll encounter any difficulties.

Artyom Kovalyov
  • 374
  • 3
  • 11
  • Ah ok.So if I have a real client application the SDK handles this for me out of the box and I don't have to worry about adding headers or anything like that right?. Good to know. – Inkers Apr 20 '20 at 13:37
  • Exactly, Inkers, after you have generated a client from your Sales Order service definition, all the required headers like `CSRF` and `Etag` will be handled for you. I added a small code snipped example on how you would create and afterward update an entity. – Artyom Kovalyov Apr 20 '20 at 16:05
  • Of course, you'll have to take care of any errors returned by the server in case of non-matching `Etag` if an `Entity` was updated by someone else in between your requests or similar situations. I also recommend taking a look into our tutorials explaining things in more details: https://developers.sap.com/tutorials/s4sdk-odata-service-cloud-foundry.html – Artyom Kovalyov Apr 20 '20 at 16:08
  • Hi Artyom. I am not sure I explained my original question well. Please see my edit with further detail. Hops this helps to clarify further. – Inkers Apr 27 '20 at 14:32
0

The SDK makes an attempt to fetch a CSRF token automatically within execute(destination). This happens before issuing the actual request. If the attempt is successful the token will be included in the request. If not, the request will be send regardless.

Please increase the log level to debug for all com.sap.cloud.sdk packages if you think this is not happening correctly. Also it would be great to see the actual HTTP requests that go in and out which you can enable by setting the log level of org.apache.http.wire also to debug. Then attach the stack trace here together with the SDK version you are using and the exact code you are invoking.

MatKuhr
  • 505
  • 4
  • 13