Skip to Content

Odata4j is a popular OData tool kit for Java. One of the reasons for using “popular” is the library’s constant evolvement with every new release. So, when we set out to try the library for consuming SAP Gateway services, everything was green until CSRF (Cross-Site Request Forgery) is met.

CSRF protection is an additional validation feature enabled in SAP Gateway since Gateway 2.0/SP03 for all data modifying requests (e.g. Create, Update and Delete). This means that a valid CSRF token must first be retrieved using a non-modifying request (e.g. using the GET method). Then it can be sent along with the subsequent modifying request and validated before normal processing continues. With the helm of internet frauds, mandating such a validation is nice and welcome.

For more information about CSRF in the context of SAP Gateway, proceed here.

Now, not every library complies or provides mechanisms to handle these CSRF tokens. Odata4j isn’t an exception. Like any other library, odata4j also sets focus on the business part which is ODATA while encapsulating the actual request and response building part safely. So, there is no easy way the request and response headers can be influenced to send and receive these CSRF tokens.

Just when we gave up, this provided little solace. The documentation is outdated. With release 0.5, Jersey is used as a client. But, this gave the much needed pointer and the objective has been achieved just by making few changes and utilizing the extension points provided by odata4j.

We had to implement the interface “JerseyClientBehavior” of package (org.odata4j.jersey.consumer.behaviors) and override the methods with our own implementation handling the CSRF tokens. Below is the code-snippet explaining this further:

private static class myJerseyBehavior implements JerseyClientBehavior {

 private String xsrfCookieName;
 private String xsrfCookieValue;
 private String xsrfTokenValue;

 private String user;
 private String password;

 myJerseyBehavior(String user, String password) {
  this.user = user;
  this.password = password;
 }
    
 @Override
 public ODataClientRequest transform(ODataClientRequest request) {
         String userPassword = user + ":" + password;
         String encoded = Base64.encodeBase64String(userPassword.getBytes());
         encoded = encoded.replaceAll("\r\n?", "");
        
         if (request.getMethod().equals("GET")){
         request = request.header("X-CSRF-Token", "Fetch")
        .header("Authorization", "Basic " + encoded);       
   return request;
         }else {
          request = request.header("X-CSRF-Token", this.xsrfTokenValue).header("Cookie", xsrfCookieName + "=" + xsrfCookieValue)
        .header("Authorization", "Basic " + encoded);
   return request;              
         }
 }
 
 @Override
 public void modifyWebResourceFilters(Filterable arg0) {

 }

 @Override
 public void modifyClientFilters(Filterable client) {
 client.addFilter(new ClientFilter(){
         
 @Override
 public ClientResponse handle(ClientRequest clientRequest) throws ClientHandlerException {
        ClientResponse response = getNext().handle(clientRequest);
              
        List<NewCookie> cookies = response.getCookies();
              
   for (NewCookie cookie:cookies) {
    if (cookie.getName().startsWith("sap-XSRF")) {
     xsrfCookieName = cookie.getName();
     xsrfCookieValue = cookie.getValue();
     break;
    }
    
   }
  
   MultivaluedMap<String, String> responseHeaders = response.getHeaders();
   xsrfTokenValue = responseHeaders.getFirst("X-CSRF-Token");
   return response;
  }});
 }
 
 @Override
 public void modify(ClientConfig arg0) {
 }
}

A step to step description of what is done:

  • Create a client behavior as shown in the above code-snippet.

  • The transform method is overridden so that the CSRF token can be fetched for “GET” and the CSRF token can be set for any modifying requests such as “PUT”, “POST” etc.

  • Create new client filter enabling to retrieve the CSRF token and the CSRF cookie values from response headers.

  • Note: Both the CSRF token and the cookie are to be set for the modifying requests to work.

  • Finally while instantiating a new jersey consumer for odata4j use the client behavior as created in the above steps. In this way, this holds good for all the requests.

After this, everything is green again

To report this post you need to login first.

14 Comments

You must be Logged on to comment or reply to a post.

  1. Hannes Angst

    This code is a very good starting point for getting the CSRF-Token to work.

    But there are two things, you want to improve:

    1. The Session-Cookie is wrong. It starts with “SAP_SESSIONID”.

    2. Why did you merge basic authentication  and CSRF? You can use org.odata4j.consumer.behaviors.BasicAuthenticationBehavior to do this.

    So here is a – hopefully improved version agains odata 0.7.x

    1. package com.excelsisnet.base.odata;
    2. import java.util.List;
    3. import javax.ws.rs.core.MultivaluedMap;
    4. import javax.ws.rs.core.NewCookie;
    5. import org.odata4j.consumer.ODataClientRequest;
    6. import org.odata4j.jersey.consumer.behaviors.JerseyClientBehavior;
    7. import com.sun.jersey.api.client.ClientHandlerException;
    8. import com.sun.jersey.api.client.ClientRequest;
    9. import com.sun.jersey.api.client.ClientResponse;
    10. import com.sun.jersey.api.client.config.ClientConfig;
    11. import com.sun.jersey.api.client.filter.ClientFilter;
    12. import com.sun.jersey.api.client.filter.Filterable;
    13. public class SAPCSRFBehaviour implements JerseyClientBehavior {
    14.           private static final String CSRF_HEADER = “X-CSRF-Token”;
    15.           private static final String SAP_COOKIES = “SAP_SESSIONID”;
    16.           private String xsrfCookieName;
    17.           private String xsrfCookieValue;
    18.           private String xsrfTokenValue;
    19.           @Override
    20.           public ODataClientRequest transform(ODataClientRequest request) {
    21.                     if (request.getMethod().equals(“GET”)){
    22.                               request = request.header(CSRF_HEADER, “Fetch”);
    23.                               return request;
    24.                     }else {
    25.                               return request.header(CSRF_HEADER, xsrfTokenValue).header(“Cookie”, xsrfCookieName + “=” + xsrfCookieValue);
    26.                     }
    27.           }
    28.           @Override
    29.           public void modifyWebResourceFilters(final Filterable arg0) {
    30.           }
    31.           @Override
    32.           public void modifyClientFilters(final Filterable client) {
    33.                     client.addFilter(new ClientFilter(){
    34.                               @Override
    35.                               public ClientResponse handle(final ClientRequest clientRequest) throws ClientHandlerException {
    36.                                         ClientResponse response = getNext().handle(clientRequest);
    37.                                         List<NewCookie> cookies = response.getCookies();
    38.                                         for (NewCookie cookie:cookies) {
    39.                                                   if (cookie.getName().startsWith(SAP_COOKIES)) {
    40.                                                             xsrfCookieName = cookie.getName();
    41.                                                             xsrfCookieValue = cookie.getValue();
    42.                                                             break;
    43.                                                   }
    44.                                         }
    45.                                         MultivaluedMap<String, String> responseHeaders = response.getHeaders();
    46.                                         xsrfTokenValue = responseHeaders.getFirst(CSRF_HEADER);
    47.                                         return response;
    48.                               }});
    49.           }
    50.           @Override
    51.           public void modify(final ClientConfig arg0) {
    52.           }
    53. }
    (1) 
    1. Dominik Stork

      Hi Hannes,

      thanks for your post, it helped me out a lot. However, I suggest checking the return value of reponseHeaders.getFirst(CSRF_HEADER) for null, as it will overwrite the (valid) XSRF-Token with null if the response header does not contain such a field.

      As this is the case for a server response when POSTing a new entity, subsequent calls would have to be preceded by another GET (e.g. to $metadata). Otherwise, multiple CREATE-Statements would fail with the message “Validation of CSRF-Token failed”.

      (0) 
      1. Hannes Angst

        Absolutly right.

        In my implementation I always have a HTTP GET.

        When creating or updating entites, I always using a single request providing all data. Otherwise I must rollback manually if something goes wrong.

        (0) 
        1. Carlos Bernad

          Hi Hannes,

          First of all thank you for sharing your knowledge and your experience with us,
          I’m using your code, but I have one question:

          It is absolutely necessary to set SAP_SESSIONID cookie ? Because I have a problem in the execution of this line:

          List<NewCookie> cookies = response.getCookies();

          I get a nullpointer exception because the response of the request with GET does not contain any cookie.

          I am getting the same 403 (Not Fobidden) Error always.

          I can see in the tracelog of the HTTP Requests that I receive the token CSRF and then

          it set it on the PUT operation request header (I believe it).

          Please, can you help me ?

          Thank you very much,

          Carlos.

          (0) 
          1. Hannes Angst

            Hi Carlos,

            your NullPointerException is really bugging me.

            Do you use Jersey as Parser Implementation?

            Having a look at the com.sun.jersey.api.client.ClientResponse  classes source code, you see that getting NULL is not possible:

            public List<NewCookie> getCookies() {

                List<String> hs = getMetadata().get(“Set-Cookie”);

                if (hs == null) return Collections.emptyList();

                List<NewCookie> cs = new ArrayList<NewCookie>();

                for (String h : hs) {

                    cs.add(NewCookie.valueOf(h));

                }

                return cs;

            }

            (0) 
            1. Carlos Bernad

              Thank your for answer Hannes,

              This is the nullpointer Exception I am getting. nullpointer exception.PNG

              I’m instantiating the objects and setting the behaviour in this way:

              behaviour.PNG

              This is my project build path:
              buildpath.PNG

              These are the imports that I’m using:

              In SAPCSRFBehaviour class:
              behaviour libraries.PNG

              In my class where I instantiate the objects and I do CRUD operations:

              libs behaviour.PNG
              Thank you again.
              Carlos.

              (0) 
                1. Carlos Bernad

                  Thank you very much Hannes for your answers.

                  I can finally do PUT and CREATE Operations to SAP Netweaver Gateway from Android native app using odata4j:

                  I solved my problem following this SAP SCN thread:

                  SAP Fiori LL16 – http 403 Forbidden CSRF token error

                  After that, I put these headers at ODataClientRequest:

                  request = request.header(“X-Requested-With”, “XMLHttpRequest”)

                                           .header(“Authorization”, “Basic ” + encoded);  

                  Then, you don’t need to implement any SAPCSRFBehaviour class that get the csrf token from GET requests. Only with that two headers is enough.

                  With this configuration I didn’t receive again the 403 Forbidden error.

                  Hope this can help anyone.

                  Thank you again.

                  Carlos.

                  (0) 
  2. Stefan Heitzer

    Hi everybody,

    thx a lot for the blogpost but it seems that it isn’t working for me …

    The part with receiving the token seems to work quite good but as soon as I try to get the response cookies I get a NullPointerException … does anybody know why this error is thrown?

    EDIT:

    After checking the request and the response I found out that I even do not have any cookies supplied by the response …

    Hope that somebody can help me 🙂

    Greetings

    Stefan

    (0) 
    1. Carlos Bernad

      Hi Stefan,

      did you solve your issue ? I’m in the same situation as you, I receive a 403 Forbidden error, I pass the CSRF Token and I get a nullpointer exception when I try to retrieve the response cookies (because there are not any cookies).

      Hope you can help me.

      Thank you very much.

      Carlos.

      (0) 
    2. Hannes Angst

      Hi Stefan,

      maybe I got you wrong, but the CSRF-Token is located in the HTTP header section.

      I am not aware of any implementation using cookies since REST Services tend to be stateless.

      (0) 
    3. Carlos Bernad

      I can finally do PUT and CREATE Operations to SAP Netweaver Gateway from Android native app using odata4j:

      I solved my problem following this SAP SCN thread:

      SAP Fiori LL16 – http 403 Forbidden CSRF token error

      After that, I put these headers at ODataClientRequest:

      request = request.header(“X-Requested-With”, “XMLHttpRequest”)

                               .header(“Authorization”, “Basic ” + encoded); 

      Then, you don’t need to implement any SAPCSRFBehaviour class that get the csrf token from GET requests. Only with that two headers is enough.

      With this configuration I didn’t receive again the 403 Forbidden error.

      Hope this can help anyone.

      Carlos.

      (0) 
      1. Hannes Angst

        You could have used the BasicAuthenticationBehavior…



        builder.setClientBehaviors(new BasicAuthenticationBehavior(serviceLocation.getUserName(), serviceLocation.getPassword()), new SAPCSRFBehaviour());

        (0) 
        1. Carlos Bernad

          Yes Hannes, you are right, but for me it is easy to set the headers inside OClientBehavior instance.

          Now, I don’t need to implement and set SAPCSRFBehaviour, PUT and POST Operations.

          It works without that class.

          Thank you very much.

          Have a nice day.

          Carlos.

          (0) 

Leave a Reply