Skip to Content
Author's profile photo Sudheer Tammana

How to handle CSRF tokens while consuming Gateway services using odata4j

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

Assigned Tags

      14 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Former Member
      Former Member

      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. }
      Author's profile photo Former Member
      Former Member

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

      Author's profile photo Former Member
      Former Member

      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.

      Author's profile photo Carlos Bernad
      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.

      Author's profile photo Former Member
      Former Member

      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;

      }

      Author's profile photo Carlos Bernad
      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.

      Author's profile photo Former Member
      Former Member

      Hi Carlos,

      I'm sorry to say, that Jersey and Android do not make a happy couple.

      Have a look at the answers at web services - Restlet android client / jersey java server - Stack Overflow.

      Maybe that giorgio-zamparelli/jersey-android · GitHub Library solves your problem.

      Greetings

      Hannes

      Author's profile photo Carlos Bernad
      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.

      Author's profile photo Stefan Heitzer
      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

      Author's profile photo Carlos Bernad
      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.

      Author's profile photo Former Member
      Former Member

      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.

      Author's profile photo Carlos Bernad
      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.

      Author's profile photo Former Member
      Former Member

      You could have used the BasicAuthenticationBehavior...



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

      Author's profile photo Carlos Bernad
      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.