Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
masjo
Explorer
SAP provides standard content for Concur Integration via the SAP Financials with SAP Concur package but what if we need a Concur integration that is not covered by the standard content?  This blog aims to assist with the development of custom integration scenarios with Concur particularly using the Concur APIs.

Let’s take a sample scenario to demonstrate all the relevant catch-alls and hints and tips when building an interface. Scenario:  An interface that retrieves all un-submitted expense reports in the Concur system.  From there, obviously the scenario can do further with the reports e.g. update data etc. but we’ll simplify things here.

Before starting, the most important reference when designing and building an integration with Concur using APIs is https://developer.concur.com/.  This contains documentation as well as a testing tool for all the available APIs.

So let’s go through building the scenario. Here is the main integration process:



























Step 1: Concur Authentication


Type = Local Integration Process

This process handles the call to the Concur Authentication API to retrieve the access token which is required in the header for all other Concur API calls.   This will be outlined further in the blog.
Step 2: Initialize targetURL for reports


Type = Content Modifier

This step sets the URL for the call to the next API- the Reports API.

targetURLReports = https://us.api.concursolutions.com/api/v3.0/expense/reports?limit=100&user=ALL&approvalStatusCode=A_...



 

This URL will retrieve all un-submitted expense reports in batches of 100 (which is the maximum allowed by the Reports API).

 
Step 3: Get Concur Expense Reports and Loop each


Type = Looping Process Call

This process handles the call to the Get Reports API and will repeatedly call this process till there are no reports to retrieve. The Reports API returns a URL in the NextPage field to retrieve the next batch of reports if there are more to retrieve.  This field is used in the Condition Expression in the Looping Process Call.



 


 

Let’s further outline the 2 local integration process steps i.e. Step 1 Authentication and Step 3 Get Expense Reports.

 

Main Integration Process Step 1: Concur Authentication Local Integration Process:

 















































Step 1: Initialize message


Type = Content Modifier

This step sets all the required parameters for the Authentication API


Message Header:

targetURL = https://us.api.concursolutions.com/oauth2/v0/token



Message Body:

client_id=<ID supplied by Concur>&client_secret=<secret supplied by Concur>&grant_type=password

&username=<username supplied by Concur>&password=<password supplied by Concur>

Step 2: Prepare Authentication API call


Type = Groovy Script

This step encodes the message body into “x-www-form-urlencoded” as required by the Concur API. This is important as the Authentication API does not work without this coding.
Script:
import com.sap.gateway.ip.core.customdev.util.Message;
import java.util.HashMap;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Callable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URLEncoder;

def Message processData(Message message) {

Logger log = LoggerFactory.getLogger(this.getClass());

def pmap = message.getProperties();
def props = message.getProperties();
def body_string = message.getBody(String.class);

//encode POST Body as x-www-form-urlencoded
def body_keyvalue_string = body_string.split("&");

def boolean first = true;
def StringBuilder result = new StringBuilder();

for (int i = 0; i < body_keyvalue_string.length; i++) {

if (first)
first = false;
else
result.append("&");

result.append(URLEncoder.encode(body_keyvalue_string[i].split("=")[0], "UTF-8"));
result.append("=");
result.append(URLEncoder.encode(body_keyvalue_string[i].split("=")[1], "UTF-8"));

}

message.setBody(result.toString());

def headers = message.getHeaders();
def target_uri = headers.get("targetURL");
def splitIdx = target_uri.indexOf("?");

if (splitIdx != -1){

message.setProperty('concurAddress',target_uri.substring(0,splitIdx));
message.setProperty('concurQuery',target_uri.substring(splitIdx+1,target_uri.length()));

} else {

message.setProperty('concurAddress',target_uri);
message.setProperty('concurQuery','');

}


message.setHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
message.setHeader("Connection", "close");

return message;

}

 
Step 3: Call Authentication API


Type = Request Reply

This step invokes the Concur Authentication API


Connection:



 
Step 4: Extract access token


Type = Groovy Script

This script extracts the access token and places it in the message header for subsequent API calls.
Script:
import com.sap.gateway.ip.core.customdev.util.Message;
import java.util.HashMap;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Callable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URLEncoder;



def Message processData(Message message) {

Logger log = LoggerFactory.getLogger(this.getClass());

def pmap = message.getProperties();

def props = message.getProperties();

def access_token = new String ("");

def body_string = message.getBody(String.class);


//extract Concur access_token

def body_keyvalue_string = body_string.split(",");

for (int i = 0; i < body_keyvalue_string.length; i++) {

if (body_keyvalue_string[i].indexOf("access_token") != -1 ) {

def access_token_field = body_keyvalue_string[i].split(":");
if (access_token_field.length > 1) {
access_token = access_token_field[1].replace("\"", "");

}
}
}

message.setBody("Bearer " + access_token);
message.setHeader("Authorization", "Bearer " + access_token);
return message;

}


 


 

 

 

Main Integration Process Step 3: Get Concur Expense Reports and Loop each Looping Process:



 































Step 1: Prepare HTTP URL


Type = Groovy Script

This step prepares the request to the Get Reports API and is a copy of the groovy script from the SAP standard Concur integration content.
Script:
import com.sap.gateway.ip.core.customdev.util.Message;
import java.util.HashMap;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Callable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


def Message processData(Message message) {

Logger log = LoggerFactory.getLogger(this.getClass());

def pmap = message.getProperties();
def props = message.getProperties();
def body_string = message.getBody(String.class);

message.setBody(body_string);

def headers = message.getHeaders();
def target_uri = headers.get("targetURLReports");
def splitIdx = target_uri.indexOf("?");

if (splitIdx != -1){

message.setProperty('concurAddress',target_uri.substring(0,splitIdx));
message.setProperty('concurQuery',target_uri.substring(splitIdx+1,target_uri.length()));

} else {

message.setProperty('concurAddress',target_uri);
message.setProperty('concurQuery','');

}

return message;

}

 
Step 2: Call Reports API


Type = Request Reply

This step invokes the Get Reports API


Connection:



 
Step 3: Extract access token


Type = Content Modifier

This step updates the target URL for the next loop (if there is more reports to retrieve). As mentioned, if there are more reports to retrieve, then the NextPage field will contain a URL to retrieve the next batch.



 


 

So that is the end of this scenario. It demonstrates the Concur authentication step which is required to call any of the other Concur APIs.  It also demonstrates the call to another Concur API with the access token and the concept of the looping process call based on the NextPage field.  This field is similarly used in a number of Concur APIs.  From here, this scenario can be extended further to retrieve further information, update fields etc. Basically anything that Concur allows via APIS.  Hopefully this has provided enough information to your scenarios going!

 
4 Comments
engswee
Active Contributor
0 Kudos
Hi Joy

 

Thanks for sharing this.

 

I did some Concur integration few years back. Back then, it used a non-standard hybrid OAuth 2 implementation, and I wrote about how this could be achieved in HCI (that's what it was called back then) here.

From the Concur developer portal, it looks like they have now moved to a more standard approach of OAuth 2. The approach you mentioned above uses the Password grant type, but they also support Client Credentials grant type, which is also supported natively in CPI (see here) and also the recommended grant type used for system-to-system integration.

 

I don't have access to a Concur tenant to check this out, but I'd suggest you do because it could be as simple as just creating and deploying an OAuth2 Credentials security material in CPI rather than implementing it in the iFlow.

 

A few more comments regarding your approach above should you still prefer to proceed with it:-

  • Your credentials are hardcoded and therefore a security risk - an alternative would be to store them in as User Credentials and retrieve them using the SecureStoreService API in a Groovy step - this is something I've covered in my post.

  • There seems to be a lot of extraneous lines in your Groovy scripts (unused imports, unused logger, etc) - you might want to see where these came from and clean them up as they are not necessary.


 

Regards

Eng Swee

 
masjo
Explorer
0 Kudos
Thanks Eng!

A few things in response:

  • I did ask Concur about the client credentials grant type as I did see that blog that you have linked to.  Their response was "At this time we can’t use those types of credentials. The way they are implemented here is that they work with the partner apps that are found in the app store which have a more restricted role."  Therefore, it is not an option at this time.

  • Regarding the extra import statements in the groovy scripts, I actually got these scripts from the standard SAP content for Concur integration.  So they actually have the extra unused imports in their scripts.


Anyway, I won't proceed with this blog after your review as I didn't come across your blog when I was doing a search to ensure there wasn't an existing one.  Although, the Concur APIs have been updated since your blog, I don't think my blog adds that much extra value.

 
engswee
Active Contributor
0 Kudos
Hi Joy

 

I think it's still fine to keep this blog since it does highlight how to handle some of the differences with regards to the updated OAuth implementation in Concur. Anyway, I don't think you can delete the blog once there are comments.

 

I think it's fine to use password grant type if that is the recommendation from Concur, but I'd still suggest having a more secure way to store and retrieve the passwords rather than hardcoding it.

 

I had a look at the standard content for Concur integration, and it does have all those extraneous lines! Obviously, someone didn't do a proper clean up or review! LOL!

 

Regards

Eng Swee

 
0 Kudos
Hi Joy,

This is a very nice blog.

Just wanted to know if there would be any major change in the iflow in case we do this for POST api  method instead of GET?

In my scenario, I am first doing the token authentication part and after retrieving the token, providing the target PO URL in content modifier and then calling the API directly.

I I tried doing that but getting an "Internal server error - 500".

 

P.S: I am able to do a GET scenario successfully with same configurations.

Regards,

Shweta
Labels in this area