javahttpsodatacsrfsap-gateway

HTTPS POST Request from Java Application to OData-Service in S/4HANA System with valid CSRF-Token


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?


Solution

  • 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();