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
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
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".
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.
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.
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;
}
Thank your for answer Hannes,
This is the nullpointer Exception I am getting.
I'm instantiating the objects and setting the behaviour in this way:

This is my project build path:

These are the imports that I'm using:
In SAPCSRFBehaviour class:

In my class where I instantiate the objects and I do CRUD operations:
Thank you again.
Carlos.
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
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.
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
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.
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.
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.
You could have used the BasicAuthenticationBehavior...
builder.setClientBehaviors(new BasicAuthenticationBehavior(serviceLocation.getUserName(), serviceLocation.getPassword()), new SAPCSRFBehaviour());
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.