Skip to Content
Technical Articles

How to set X-CSRF-Token and Cookie as header parameters in REST adapter for POST Call?

Requirement – 

I had a scenario where I have to post data to C4COdataAPI through REST adapter. The C4C Odata accepts 3 types of authentication which are OAuth 2.0, Certificate and Basic. If you go with OAuth 2.0 you do not have to pass x-csrf-token and session id as header parameters. I had to use basic authentication so I had to pass csrf token and session id to the POST call of my receiver REST adapter.

Design – 

  1. Use Receiver REST GET channel to get the HTTP header parameters. The recent release of the REST adapter will return the header parameters along with the payload.
  2. UDF is implemented to call the REST GET channel and set the dynamic configuration attributes which will be later used in the POST call of the receiver REST adapter.
  3. Custom Adapter Module is implemented to retrieve the token and session id. The module is used in the REST GET operation. We need a module as the GET and POST operation has to happen in the same session so that we can read and set the header parameters simultaneously.

Module Development  –

The code below is used for extracting the token and session id and pass it as input payload using the Module Data process method.

package com.sap.module;

import javax.ejb.Stateless;
import java.rmi.RemoteException;
import javax.ejb.EJBException;
import javax.ejb.Local;
import javax.ejb.LocalHome;
import javax.ejb.Remote;
import javax.ejb.RemoteHome;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import com.sap.aii.af.lib.mp.module.Module;
import com.sap.aii.af.lib.mp.module.ModuleContext;
import com.sap.aii.af.lib.mp.module.ModuleData;
import com.sap.aii.af.lib.mp.module.ModuleException;
import com.sap.aii.af.lib.mp.module.ModuleHome;
import com.sap.aii.af.lib.mp.module.ModuleLocal;
import com.sap.aii.af.lib.mp.module.ModuleLocalHome;
import com.sap.aii.af.lib.mp.module.ModuleRemote;
import com.sap.engine.interfaces.messaging.api.Message;
import com.sap.engine.interfaces.messaging.api.MessageKey;
import com.sap.engine.interfaces.messaging.api.MessagePropertyKey;
import com.sap.engine.interfaces.messaging.api.PublicAPIAccessFactory;
import com.sap.engine.interfaces.messaging.api.TextPayload;
import com.sap.engine.interfaces.messaging.api.auditlog.AuditAccess;
import com.sap.engine.interfaces.messaging.api.auditlog.AuditLogStatus;
import com.sap.tc.logging.Location;

/**
 * Session Bean implementation class SetTokenModule
 */
@Stateless(name = "SetTokenModule")
@Local(value = { ModuleLocal.class })
@Remote(value = { ModuleRemote.class })
@LocalHome(value = ModuleLocalHome.class)
@RemoteHome(value = ModuleHome.class)

public class SetTokenModule implements Module, SessionBean {

	private static final long serialVersionUID = -14L;
	private static final Location LOC = Location.getLocation(SetTokenModule.class);
	private AuditAccess audit;

	public ModuleData process(ModuleContext context, ModuleData inputmoduleData) throws ModuleException {
		try {
			Message msg = (Message) inputmoduleData.getPrincipalData();
			MessageKey key = msg.getMessageKey();

			// Audit Trace is used in Module to trace the value of the string.
			audit = PublicAPIAccessFactory.getPublicAPIAccess().getAuditAccess();
			audit.addAuditLogEntry(key, AuditLogStatus.SUCCESS, "TokenModule: Module called");

			// Read Token and Set-Cookie from the dynamic message property.

			MessagePropertyKey tokenKey = new MessagePropertyKey("x-csrf-token", "http://sap.com/xi/XI/System/REST");
			MessagePropertyKey setCookie = new MessagePropertyKey("set-cookie", "http://sap.com/xi/XI/System/REST");

			String token = msg.getMessageProperty(tokenKey);
			audit.addAuditLogEntry(key, AuditLogStatus.SUCCESS, "TokenModule: Tokenvalue " + token);

			String cookie = msg.getMessageProperty(setCookie);
			audit.addAuditLogEntry(key, AuditLogStatus.SUCCESS, "TokenModule: Tokenvalue " + cookie);

			String headerData = token + cookie;

			audit.addAuditLogEntry(key, AuditLogStatus.SUCCESS, "TokenModule: headerData " + headerData);

			TextPayload tokenPayload = msg.createTextPayload();
			tokenPayload.setText(headerData);
			msg.setMainPayload(tokenPayload);

			audit.addAuditLogEntry(key, AuditLogStatus.SUCCESS, "TokenModule: tokenPayload " + tokenPayload);

		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return inputmoduleData;
	}

	@Override
	public void ejbActivate() throws EJBException, RemoteException {
		// TODO Auto-generated method stub

	}

	@Override
	public void ejbPassivate() throws EJBException, RemoteException {
		// TODO Auto-generated method stub

	}

	@Override
	public void ejbRemove() throws EJBException, RemoteException {
		// TODO Auto-generated method stub

	}

	@Override
	public void setSessionContext(SessionContext ctx) throws EJBException, RemoteException {
		// TODO Auto-generated method stub

	}

}

Deploy the EAR File in the PO server.

 

PO ESR – 

Create a UDF for setting the dynamic attributes and map the same to a root node in your message mapping.

UDF to be used in Message Mapping –

This UDF will call the GET REST channel, read the dynamic properties i.e payload content returned from the module and sets the dynamic attributes token and cookie.

package *****_demo;
/* If using NWDS for UDF, otherwise these imports will be available in PO ESR */
import com.sap.aii.mapping.lookup.*;
import java.util.*;
import com.sap.aii.mappingtool.tf7.rt.*;
import java.io.*;
import com.sap.aii.mapping.api.*;
import java.lang.reflect.*;
import com.sap.ide.esr.tools.mapping.core.ExecutionType;
import com.sap.ide.esr.tools.mapping.core.Cleanup;
import com.sap.ide.esr.tools.mapping.core.LibraryMethod;
import com.sap.ide.esr.tools.mapping.core.Init;
/*****************************/

/*Put this method under attributes and methods if using ESR for UDF otherwise this will be a private method as a part of UDF class in NWDS. */

private String readbytes(InputStream inputStream) throws IOException {
		ByteArrayOutputStream result = new ByteArrayOutputStream();
		byte[] buffer = new byte[1024];
		int length;
		while ((length = inputStream.read(buffer)) != -1) {
			result.write(buffer, 0, length);
		}
		return result.toString("UTF-8");
	}

// You can also directly use String methods to convert the payload content to string and use it in the below UDF. 

	@Init(description="") 
	public void init (
		 GlobalContainer container)  throws StreamTransformationException{

	}

	@Cleanup 
	public void cleanup (
		 GlobalContainer container)  throws StreamTransformationException{

	}

/*** UDF to set token and cookie as dynamic attributes**/
@LibraryMethod(title="lookupTokenAndSession", description="Get Token and Session ID for C4C", category="RestAPILookup", type=ExecutionType.SINGLE_VALUE) 

	public String lookupTokenAndSession(
		 Container container)  throws StreamTransformationException{
	AbstractTrace trace = container.getTrace();
	Map<String, Object> all = container.getInputHeader().getAll();

// Call the REST channel
	Channel restChannel = LookupService.getChannel("<<BC or BS name>>", "CC_REST_GET_CSRFToken");
	SystemAccessor restAccessor = LookupService.getSystemAccessor(restChannel);
	trace.addInfo("got channel ");
	XmlPayload payload = LookupService.getXmlPayload(new ByteArrayInputStream("<root/>".getBytes()));
	Payload response = restAccessor.call(payload);
	trace.addInfo("got response");

	String payloadContent = "";
	String payloadContentToken = "";
	String payloadContentCookie = "";
	String sapSessionId = "";
try
	{
	payloadContent = readbytes(response.getContent());
	trace.addInfo(payloadContent);
// CSRF token comes up as the first 24 characters of the Payload Content. Use Substring to read the same.
	payloadContentToken = payloadContent.substring(0, 24);
	trace.addInfo(payloadContentToken);

// Cookie is the last few characters of payload content. Token, cookie, and session are separated by ", "in the payload content. Use lastIndexOf separated by, to read the SAP SESSION ID.
	payloadContentCookie = payloadContent.substring(24);
	int index = payloadContentCookie.lastIndexOf(',');
	sapSessionId = payloadContentCookie.substring(index +1);	
	trace.addInfo(sapSessionId);

	}catch(
	IOException e1)
	{
		trace.addWarning("IO Exception " + e1.getMessage());
	}

	try
	{
// Set the Dynamic Configuration attributes Token and Cookie
		DynamicConfiguration config = (DynamicConfiguration) all
				.get(StreamTransformationConstants.DYNAMIC_CONFIGURATION);

		// Create Dynamic Configuration parameters for token and cookie
		DynamicConfigurationKey key1 = DynamicConfigurationKey.create("http://sap.com/xi/XI/System/REST", "token");
		DynamicConfigurationKey key2 = DynamicConfigurationKey.create("http://sap.com/xi/XI/System/REST", "cookie");
		// Pass the values of token and session id to the configurable parameters key 1 and key2
                config.put(key1, payloadContentToken);
		config.put(key2, sapSessionId);

	}catch(
	Exception e)
	{
		trace.addWarning("Unable to set dynamic configuration");
	}


	return"";
	}

 

PO ID – Channel Configuration – 

Create two channels one each for GET and POST operation.

Configuration for REST GET Channel – 

  • REST URL – API url.
  • Authentication – Basic/Certificate
  • Operation – GET
  • Data Format – JSON/XML (any)
  • HTTP Header – x-csrf-token = Fetch (required to fetch the token)
  • Module – Call the module (SetTokenValue) after the standard REST adapter call. No module parameters are required.

Configuration for REST POST Channel – 

  • REST URL Tab – Maintain the configuration as mentioned in the screenshot below. Attribute name token and cookie denotes the name provided in the UDF for setting the values as dynamic configuration attributes. You should read the values here and set in the HTTP header.

  • Authentication – Basic/Certificate
  • Operation – POST
  • Data Format – JSON/XML (any)
  • HTTP Header 
    • x-csrf-token = {token}  -This is the value from the dynamic configuration.
    • cookie = {cookie} – This is the value from the dynamic configuration.
    • Content-Type = application/json

Unit Testing –

I tried this scenario for ticket update using c4codata API ServiceRequestTextCollection. Run this scenario using PO test tool. You should see the dynamic configuration in the message monitoring. Refer screenshots below,

Post this step the REST adapter will process the message to be posted to c4c odata api.

Conclusion – 

You can follow this approach to call any REST/OData API if it requires a token and session id to be set in the header for POST operation. SAP is going to come up with a patch for REST adapter later this year which would include the CSRF protection as adapter parameter.

References – 

  • Refer  – https://blogs.sap.com/2015/01/29/create-sap-pi-adapter-modules-in-ejb-30-standard/ for module development.
  • CSRF token handling – https://blogs.sap.com/2018/05/29/handling-x-csrf-token-with-sap-pipo/comment-page-1/#comment-462449
1 Comment
You must be Logged on to comment or reply to a post.