0

The situation is as follows: On the one side I have created an OData-Service which should create an entry when it receives a POST-Request. The Service is created in an S/4HANA System and is reachable via the SAP-Gateway.

On the other hand I have a Java Application (OpenJDK 11) which does essentially a loop and must issue every loop a POST-Request to the OData-Service.

I'm using IntelliJ IDEA Community Edition and OpenJDK 11. Also this is my first time using OData both with Java and SAP.

At first I tried the following:

private static void postRequest() throws IOException {
        //Setting authenticator needed for login
        Authenticator authenticator = new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, password.toCharArray());
            }
        };
        Authenticator.setDefault(authenticator);
        
        //Creating the connection
        URL url = new URL("<my_service_link>");
        HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
        con.setRequestMethod("POST");
        con.setRequestProperty("Content-Type", "application/json; utf-8");
        con.setRequestProperty("Accept", "application/json");
        con.setDoOutput(true);
        try(OutputStream os = con.getOutputStream()) {
            byte[] input = this.getJsonRequest().getBytes("utf-8");
            os.write(input, 0, input.length);
        }

        //Reading response
        int status = con.getResponseCode();

        Reader streamReader = null;

        if (status > 299) {
            streamReader = new InputStreamReader(con.getErrorStream());
        } else {
            streamReader = new InputStreamReader(con.getInputStream());
        }

        BufferedReader in = new BufferedReader(streamReader);
        String inputLine;
        StringBuffer content = new StringBuffer();
        while ((inputLine = in.readLine()) != null) {
            content.append(inputLine);
        }
        in.close();
        con.disconnect();
        System.out.println(content.toString());
    }

But I got the error, that my CSRF-Token is invalid.

So after googling to find out what an CSRF-Token is I tried to create a GET-Request first with its own HttpsURLConnection:

private static String getRequest() {
        //Setting authenticator needed for login
        Authenticator authenticator = new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, password.toCharArray());
            }
        };
        Authenticator.setDefault(authenticator);
        
        //Creating the connection
        URL url = new URL("<my_service_link>");
        HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
        con.setRequestMethod("GET");
        con.setRequestProperty("Content-Type", "application/json; utf-8");
        con.setRequestProperty("X-CSRF-Token","fetch");
        con.connect();
        return con.getHeaderField("x-csrf-token").toString();
}

Then I would issue the actual POST-Request to the same URL and set the previous X-CSRF-Token into the HTTPS-Header with

con.setRequestProperty("X-CSRF-Token",theGETToken); in postRequest()

But I still got the same error.

What am I doing wrong?

Fabian D.
  • 37
  • 7
  • You don't indicate what response you get when you fetch the CSRF token. Are you sure you get a token? Could you indicate the exact response? (not just "got the error, that my CSRF-Token is invalid") – Sandra Rossi Feb 19 '22 at 12:00
  • The GET-Request "issues" an 501 error, because the GET-Method on my ODATA-Service isn't implemented because the service shouldn't really be used to get any information. When I run the program, IntelliJ actually prints me out the message "Validation of the CSRF-Token failed" and no normal XML- or JSON-Response. Do I need to actually implement a GET-Method so the CSRF-Token is valid? – Fabian D. Feb 20 '22 at 18:33
  • Ok, I just implemented a dummy GET-Method that returns nothing. However the error at the POST-Request is still the same: "Validation of the CSRF-Token failed" – Fabian D. Feb 20 '22 at 18:51

1 Answers1

2

After some more googling I eventually understood what I was missing.

The CSRF-Token is only valid for a specific session of a user. The session is identified by the cookies passed in the HTTPS-Header.

What needs to be done is the following (also see: https://blogs.sap.com/2021/06/04/how-does-csrf-token-work-sap-gateway/):

  1. Open a session by issuing a non-modification request and specify the header to fetch a CSRF-Token and session-cookies
    HTTP-Request:
    Type: GET
    Header-Fields: x-csrf-token = fetch
                   set-cookie = fetch
    
  2. Save the CSRF-Token and session-cookies as you need them for the POST-Request
  3. Issue a POST-Request and set the session-cookies and CSRF-Token from the saved values
    HTTP-Request:
    Type: POST
    Header-Fields: x-csrf-token = <tokenFromGet>
                   cookie = <allSessionCookies>
    

Beware that the header field of a request is named cookie instead of set-cookie and to pass all values of the HeaderField of set-cookie to the POST-Request-Header.

It is also important to mention, that the CSRF-Token as well as the session-cookies expire after a provided or adjusted timeframe or any changes to the session are made and both must be fetched anew (see https://blogs.sap.com/2021/06/04/how-does-csrf-token-work-sap-gateway/#comment-575524).

Example of my Working Code:

import javax.net.ssl.HttpsURLConnection;
import java.io.*;
import java.net.Authenticator;
import java.net.PasswordAuthentication;
import java.net.URL;

public class ODataLogger {

    private String sessionCookies;
    private String csrfToken;

    public ODataLogger() {}

    public void logOdata (String user, String pass, String jsonBody) throws IOException {
        this.setDefaultAuthenticator(user, pass);
        fetchSessionHeaderFields();
        postRequest(jsonBody);
    }

    private void setDefaultAuthenticator (String user, String pass) {
        Authenticator authenticator = new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(user, pass.toCharArray());
            }
        };
        Authenticator.setDefault(authenticator);
    }

    private void fetchSessionHeaderFields() throws IOException {
        URL url = new URL("<my-service-link>");
        HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
        con.setRequestMethod("GET");
        con.setRequestProperty("Content-Type", "application/json");
        con.setRequestProperty("x-csrf-token", "fetch");
        con.setRequestProperty("set-cookie","fetch");

        //Reading Response
        int status = con.getResponseCode();

        Reader streamReader = null;

        if (status < 299) {
            StringBuffer sb = new StringBuffer(con.getHeaderFields().get("set-cookie").toString());
            //Delete leading [ and trailing ] character
            sb.deleteCharAt(this.sessionCookies.length()-1);
            sb.deleteCharAt(0);
            this.sessionCookies = sb.toString();
            this.csrfToken = con.getHeaderField("x-csrf-token");
            return;
        }
    }

    private void postRequest(String jsonBody) throws IOException {
        //Creating the connection
        URL url = new URL("<my-service-link>");
        HttpsURLConnection con = (HttpsURLConnection) url.openConnection();
        con.setRequestMethod("POST");
        con.setRequestProperty("Content-Type", "application/json");
        con.setRequestProperty("x-csrf-token", this.csrfToken);
        con.setRequestProperty("Cookie", this.sessionCookies);
        con.setRequestProperty("Accept", "application/json");

        //Setting JSON Body
        con.setDoOutput(true);
        try(OutputStream os = con.getOutputStream()) {
            byte[] input = jsonBody.getBytes("utf-8");
            os.write(input, 0, input.length);
        }

        //Reading response
        int status = con.getResponseCode();

        Reader streamReader = null;

        if (status > 299) {
            streamReader = new InputStreamReader(con.getErrorStream());
        } else {
            streamReader = new InputStreamReader(con.getInputStream());
        }

        BufferedReader in = new BufferedReader(streamReader);
        String inputLine;
        StringBuffer content = new StringBuffer();
        while ((inputLine = in.readLine()) != null) {
            content.append(inputLine);
        }
        in.close();
        con.disconnect();
Sandra Rossi
  • 11,934
  • 5
  • 22
  • 48
Fabian D.
  • 37
  • 7