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: 
philippeaddor
Active Participant
As an integration consultant at BTM Consulting AG in Switzerland I recently got the requirement to connect an SAP system using SAP Process Orchestration (PI/PO) to a Google API ("Google BigQuery" in this case). I realized that the task is less simple as it sounds...

I ended up loading third-party libraries into PI/PO and playing with private keys in different formats. Eventually I replicated the scenario on Cloud Integration (ex. CPI) just to see if it would work there in the same way.

Update 04/2023: For PI/PO, SAP has released the JWT flow as feature for the REST receiver adapter with 7.5 SP26 (note 3265768).

But let me first explain the challenge:

OAuth the more Secure Way


When I looked at the API documentation I understood that Google requests API calls to be authenticated by Oauth 2.0, which is not surprising and state of the art. OAuth in a nutshell: This protocol enables an application to authorize at another application using a so called token. That token gets issued beforehand by an authorization server (of the API provider) based on authorization grants provided by the user during the user authentication. The OAuth 2.0 standard includes several mechanisms to transfer these so called assertions (see RFC 7521 [1]), depending on whether a user (which enters his credentials) is involved in the flow or not, or depending on security requirements.

For API calls issued by a server (server-to-server communication without involving user interaction) the token request to the authorization server does often contain a client id and secret, which are two strings (credentials) that were generated by the API provider. Many of us are used to this and this is called the Client Credentials Flow. My further researche however showed that, for enhanced security, Google requires for their API an OAuth assertion in the form of a signed JWT token instead of the Client Credentials. This is called the OAuth JWT Bearer Flow (see RFC 7523 [2]) (which is similar to the SAML Assertion flow).

CPI supports "Client Credentials", "Authorization Code" and "SAML Bearer Assertion" flows. Neither CPI nor PI/PO support the JWT Bearer flow out-of-the-box at the time being. UPDATE: Actually there is support on PI/PO since v7.50 SP18 (patch 2892050) for the JWT Grant Type, but this does not yet include the usage of the JWT for client authentication (signed token). It is on the roadmap however for Q1/2022. And got released now in Q1/2023 (see update at the top).

Signed JWT Tokens


The trust between JWT issuer and API provider is established by the use of a PKI. The client uses a private key to sign the assertion (JWT token). The signed JWT token is presented to the Google API (or others which implement the same grant type) in the http request parameter ”assertion”. The OAuth server validates the token/signature and on success returns a base64 encoded string in exchange. This will serve as Bearer token in the subsequent requests to the actual API endpoint (as long as the bearer token is valid). The private key can be created and downloaded in the Google Cloud Console while linking it to a Service Account.

The Flow


The authorization and grant flow in the end looks as follows:


(Source: Google [4])


 

Approaches to Implement the JWT Bearer Token Flow on CPI and PI/PO


In this post I will show you how to develop this scenario with help of an open source Java library. Like this, we can easily implement a JWT token flow without being restricted by the integration platform's capabilities. The approach therefore is very similar on PI/PO as well as on CPI. I have built the solution on both platforms but mainly show here how to do it on CPI. You should be able to easily adapt it and use it in a UDF on PI/PO if necessary. I will give some hints regarding modifications for the two target systems.

I assume that you're an experienced SAP integration developer and therefore will not go into every detail of the development process. There is an excellent blog by santhosh.kumarv which explains every implementation detail of a very similar requirement for a Salesforce API, mostly using CPI's built-in functionality [3]). I decided to go another way since I implemented this first on PI/PO an since I wanted to make use of libraries to create the JWT.

My description here is tailored to the Google Cloud API, but generally the Oauth Bearer Flow standard is used by other API providers and the approach is therefore very similar with slight adaptations related to the target application.

Usage of Java Libraries


I basically implemented what Google describes on their corresponding help page under "Preparing to make an authorized API call" [4]. To achieve this in practice, we’re going to use the jjwt Java library [6] to create an JWT assertion token.

Google suggests to use their own client library instead to avoid mistakes. I have in fact successfully used this library on PI/PO in a User Defined Function (UDF), however on CPI it seems not allowed (or is at least not best practice) to issue http calls directly out of a Groovy script - and that's what the Google library tries to do. (Update: The best practice part was an assumption. In the meantime, I have seen Groovy scripts written by SAP which issue HTTP calls, so it IS possible, you can give it a try if the Google libs dont' work for you). So I've implemented the JWT creation with help of the generic jjwt [6] library which I believe is as safe as using the Google libs. Especially so when we use the Cloud Integration's key store to store the private key.

I then use the created JWT assertion token to perform the authentication at the token endpoint and receive the Bearer token in exchange. The Google library would issue the token request by itself and return the Bearer token as a result. But we can easily implement the token request with Cloud Integration's own means.

Step-by-Step Implementation


The process in short: 

  1. Create and download a private key on Google Cloud.

  2. Define and generate a JWT assertion using the jjwt library.

  3. Sign the assertion using Java built-in classes to produce the JWT token.

  4. Use the JWT token to fetch the Bearer token with the authorization server.

  5. Use the Bearer token in the API requests.


 

Let's begin with creating the private key in the Google Cloud console:

  1. Create a service account in the Google console. If needed, you can find more details in the Google help pages [5]. Go to “APIs and Services”, click “Credentials” and then in the menu “Create Credentials” the entry “Service account”.


 

 

  1. After creation, click on the created service account and then on the “Keys” tab. Click “Add key” and then “Create new Key” and choose the P12 format – remember the password.
    (If you only got a json file from your GCP administrator, don’t despair but read on).

  2. Skip this whole step if you have your P12 key file (continue with step 4). In case you have only a JSON or a PEM private key file for some reason, do the following: Open the JSON and copy the private key string (value of JSON field "private_key"). This is the key in PEM-encoded (base64) format. Compared to a usual PEM file it contains the "\n" symbol for the line breaks. In both cases, do the following to get a P12 file that you can load afterwards into the CPI key store.
    Copy/paste the string to a text editor (or open the PEM file) and use the search/replace function to remove the line break symbol “\n” (replace it with empty string) and also remove the "-----BEGIN PRIVATE KEY-----" and "-----END  PRIVATE KEY-----" header and footer. In case of a PEM file, there are no \n symbols but actual visible line breaks. Just delete these line breaks in order to get a one line string.Now you have your private key as a base64 encoded string on one line. Next you have two options on CPI and one on PI/PO (as far as I know at the time being):

    a) Use a PEM to P12 converter (hint: openssl) in order to get a binary P12 file that can directly be uploaded to CPI and used to sign the JWT assertion (I suggest to use this (more secure and best practice) way on CPI). Besides the key, you'll need the X.509 client certificate to create a P12 key store. You can download it using the link in the JSON file, normally it has the following format:  https://www.googleapis.com/robot/v1/metadata/x509/[service-account-name]%40[project-id].iam.gserviceaccount.com. Extract the second (or both) certificates similar as described above and copy them into a new (pem) file. Use the PEM to P12 converter with the key and certificate files to create a P12. Enter a password that you will need when uploading the key store.


     

    b) Use this PEM string as mapping parameter (PI/PO) or externalized property (CPI). This was my approach on PI/PO. I suggest to use the prefix “pwd.” for the parameter name to make it an asterisk-masked password field - like this, the key is at least not visible anymore in the UI after entering it once. I have not (yet) looked into ways of programmatically access the key store in PI/PO.


    By the way, the Google JSON file contains some information like the token endpoint (field “token_uri”) or the project ID. You will need this later. If you don’t have the JSON file, the information is of course no secret and should be found in the API documentation. For Google the token endpoint is (at the time of writing): https://oauth2.googleapis.com/token. And the project ID you find in the Cloud Console, e.g. on the dashboard:


  3. Next, on Cloud Integration, upload the P12 file in Security Material –> Add –> Key Store. Give it a meaningful name (all lowercase) so that you remember what it is for. If you want to use the PEM (base64) string instead, as in step 3b (e.g. if you're on PI/PO), keep reading.

  4. Now that we have our private key that will be used to sign the JWT assertion, we can start with the interface implementation. But first we need the jjwt libraries. You can either download the project from the GitHub repo [6] and build it using Maven, or if you’re not familiar with that, download the ready-built jar files from a repository like MVNRepository [7]. Choose the latest version and then click the link to the jar file next to “Files”.

    Do this for all the dependent jars (use the search function on the mvnrepository), which are:


    - jjwt (link above)
    - jackson-annotations
    - jackson-core
    - jackson-databind
    - slf4j-api



  5. Upload these files either as resource into your Iflow, or better, as a Script Collection in your package (the jars and the Groovy script (see below) will have to be in the same location, either both in the Iflow resources or both in one Script Collection):At the end, it will look as follows:


    In PI/PO you would upload the single jar files into Imported Archives and then reference them in the Message Mapping UDF.



  6. Now we add the code to generate the JWT assertion token using jjwt. Find the script below with some inline comments. Replace the strings containing square brackets with the actual details of your Google account and project. The details about the header and claims are part of Google's documentation (see [4]).

    Note: I have not made use of Groovy syntax and notation and instead used plain Java in most cases (where code is not Cloud Integration specific like the messageLog). This is that you can simply use the (slightly adapted) code in PI/PO for example in a UDF (just remove the SAP imports at the top and the message log references). As you probably know, Groovy is built on Java and hence the script runs perfectly in a Groovy script step.




 
import com.sap.gateway.ip.core.customdev.util.Message;
import com.sap.it.api.ITApiFactory;
import com.sap.it.api.keystore.KeystoreService;

import java.security.cert.Certificate;
import java.security.Key;
import java.util.HashMap;
import java.io.File;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPrivateKeySpec;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Base64;
import java.util.Date;
import java.util.Map;
import java.util.Scanner;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.DefaultClaims;

def Message processData(Message message) throws NoSuchAlgorithmException {

/*
* Returns an signed assertion token, that can be used for Bearer token requests
* to the Google API token end point. The request body will look like (before
* URL encoding): grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer"
* assertion: "[assertion token]"
*/

def messageLog = messageLogFactory.getMessageLog(message);
Map props = message.getProperties();
byte[] keyBytes;
String assertion = "";

//Get the Service Instance of the KeystoreService API
KeystoreService service = ITApiFactory.getService(KeystoreService.class, null);

String projectId = "[project-id]";
String tokenUrl = "https://oauth2.googleapis.com/token";
LocalDateTime now = LocalDateTime.now();
Date nowDate = Date.from(now.atZone(ZoneId.systemDefault()).toInstant());
// Claim assertion (and thus the Bearer Token) to be valid for 60 minutes - that's the max. Google allows.
Date expDate = Date.from(now.plusMinutes(60).atZone(ZoneId.systemDefault()).toInstant());

/* Define the assertion header and body */
Header header = Jwts.header().setType("JWT");
Claims claims = new DefaultClaims();
// adjust scope to match your desired Google service:
claims.put("scope", "https://www.googleapis.com/auth/bigquery");
claims.put("aud", tokenUrl);
claims.put("iss", "[service-account-name]@[project-id].iam.gserviceaccount.com");
claims.put("sub", "[service-account-name]@[project-id].iam.gserviceaccount.com");

/* Create JWT json with JwtBuilder */
JwtBuilder jwtBuilder = Jwts.builder().setClaims(claims).setIssuedAt(nowDate).setExpiration(expDate)
.setHeader((Map<String, Object>) header);

/* Sign the assertion with the private key (alias from property) */
try {
Key key = service.getKey(props.get("keyAlias"));
KeyFactory kf = KeyFactory.getInstance(key.getAlgorithm());
// Google expects RS256 signatures
RSAPrivateKeySpec spec = kf.getKeySpec(key, RSAPrivateKeySpec.class);
assertion = (jwtBuilder.signWith(SignatureAlgorithm.RS256, kf.generatePrivate(spec)).compact());
} catch (InvalidKeySpecException e) {
log.addAttachmentAsString("private key error", "Error ocurred during signing of JWT assertion.");
}

/* Set body and URL encode the grant_type string. */
body = "grant_type=" + URLEncoder.encode("urn:ietf:params:oauth:grant-type:jwt-bearer") + "&assertion=" + assertion
message.setHeader("Content-Type", "application/x-www-form-urlencoded")
message.setBody(body);

return message;
}

 

Variant:

In case you use the approach of step 3b above (you use the PEM string (e.g. on PI/PO) in a parameter instead of a key in a key store), you have to replace the code in the try block by the code below:
// add your code to read the string from the parameter
// (e.g. from a mapping parameter in PI/PO) and save it in variable keyString
...
// decode the PEM string into a Byte array
byte[] keyBytes = Base64.getDecoder().decode(keyString);
KeyFactory kf = KeyFactory.getInstance("RSA");
EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
PrivateKey privateKey = kf.generatePrivate(keySpec);
String jwtToken = (jwtBuilder.signWith(SignatureAlgorithm.RS256, privateKey).compact());

Update: Correction to above code as per Sunil's comment:
keyBytes = Base64.getDecoder().decode(keyString);
KeyFactory kf = KeyFactory.getInstance("RSA");
RSAPrivateKeySpec spec = kf.getKeySpec(key , RSAPrivateKeySpec.class);
assertion = (jwtBuilder.signWith(SignatureAlgorithm.RS256, kf.generatePrivate(spec)).compact());

 

The Cloud Integration Iflow


A simple Iflow on Cloud Integration could look like this (this is just for demonstration purposes - in practice you would better create these steps as local integration process and call it from the main process when you need a Bearer token):



 

  1. A content modifier to set the alias of the private key (as you've named it in the key store) or the key itself as PEM (base64) string (not recommended, see above):



 

  1. "Generate JWT assertion" script (see code above).After the script execution, the body contains the required parameters (the signed assertion and a grant_type) to fetch the token.Means the body now looks something like this:
    grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&assertion=*******koZxlI1vQuNQ-w5WucHM19fB4aL6JG-****************************


  2. To exchange this assertion with the bearer token, simply use the RequestReply step that points to the Token endpoint. No authorization is needed since that is the job of the JWT token (the assertion) in the body.In Postman, the same would look like this:

  3. For a quick test, send a GET request to this IFlow's http end point, the response contains your bearer token! In practice, the iflow instead will use this token to facilitate the subsequent requests to the actual API endpoint.Response in Postman (for demonstration purposes):

  4. On Cloud Integration, to fetch and save this token, simply use JsonSlurper like this:
    Reader reader = message.getBody(Reader)
    def jsonSlurper = new JsonSlurper().parse(reader)
    def json = jsonSlurper.parseText(body)
    def token = json.access_token

     

  5. And lastly (after the Rrequest-reply step to the token endpoint) set the token as Authorization header with prefix Bearer. On Cloud Integration with a line of code:

    message.setHeader("Authorization: Bearer ", token)"

     

  6. All subsequent http calls to the API will carry this header and therefore be authenticated! To allow the header, whitelist it in the http adapter (field “Request Headers”):


 

PI/PO Specifics


In PI/PO you would have to write some additional code in the UDF to fetch the Bearer token providing the JWT Assertion token (which is what we above performed with a Request/Reply step on Cloud Integration). Then you would extract the token from the response and save it for example into an ASMA (Dynamic Configuration for setting it as HTTP Header in the REST Receiver Adapter).

To keep this blog concise, I don't go into detail how to do an HTTP lookup or how to set the ASMA within a UDF. There are many other blogs which explain this, please refer to them.

Actually, if you're dealing with a Google API on PI/PO, I suggest you use the Google library in a UDF and feed it with the content of the Json key file [5]. You just have to make sure to upload all dependencies as jars as Imported Archives. Find sample code to fetch the Bearer token using Google's lib below.

Import the jar of com.google.auth.oauth2.GoogleCredentials and all its dependencies into Imported Archives and reference them in the Message Mapping UDF section.



 

UDF Code (needed imports are com.google.auth.oauth2.GoogleCredentials and com.google.auth.oauth2.AccessToken):
		// sample content of the key file generated in the GCP console. Here as string to make it easy:

final String key = "{\r\n" + " \"type\": \"service_account\",\r\n"
+ " \"project_id\": \"***-***\",\r\n"
+ " \"private_key_id\": \"5e525df87b38f7eb*******14f6a80fb9f6\",\r\n"
+ " \"private_key\": \"-----BEGIN PRIVATE KEY-----\\*************************\\n*************************\n******==\\n-----END PRIVATE KEY-----\\n\",\r\n"
+ " \"client_email\": \"sap-bw@*****-***.iam.gserviceaccount.com\",\r\n"
+ " \"client_id\": \"106*****436\",\r\n"
+ " \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\r\n"
+ " \"token_uri\": \"https://oauth2.googleapis.com/token\",\r\n"
+ " \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\r\n"
+ " \"client_x509_cert_url\": \"https://www.googleapis.com/robot/v1/metadata/x509/***-***%40******-******.iam.gserviceaccount.com\"\r\n"; + " }";

InputStream stream = new ByteArrayInputStream(key.getBytes());
ArrayList<String> scope = new ArrayList();
String token = "";
scope.add("https://www.googleapis.com/auth/bigquery");

GoogleCredentials credentials = null;
try {
credentials = GoogleCredentials.fromStream(stream).createScoped(scope);
} catch (IOException e) {
e.printStackTrace();
}
try {
credentials.refreshIfExpired();
token = credentials.getAccessToken().getTokenValue();
System.out.print(token);
} catch (IOException e) {
e.printStackTrace();
}

 

Further Thoughts


Of course you can be more creative with storing the token and reusing it. Some ideas: store it in a (global) variable, use it from the same or other Iflows the next time, and fetch it only once it is expired (by time, or when the API returns a 401 Unauthorized - then only fetch the a new token, e.g. in an exception sub process).

Please let me know if you found this helpful or insightful, or if you have any inputs or need more information. I am sure there are other ways to achieve this (maybe by avoiding this OAuth grant flow completely in some way) or in the future perhaps SAP is going to release a feature to do the same out-of-the-box, who knows. 😊

UPDATE: Actually there is support on PI/PO since v7.50 SP18 (patch in support note 2892050) for the JWT Grant Type, but this does not yet include the usage of the JWT for client authentication (signed token). It is on the roadmap however for Q1/2023.

 

 

References:


[1] Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants: https://datatracker.ietf.org/doc/html/rfc7521

[2] JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants: https://datatracker.ietf.org/doc/html/rfc7523

[3] SAP CPI – Salesforce Rest API Integration using OAUTH JWT Bearer Flow – Part 2: https://blogs.sap.com/2019/04/30/sap-cpi-salesforce-rest-api-integration-using-oauth-jwt-bearer-flow...

[4] Preparing to make an authorized API call:  https://developers.google.com/identity/protocols/oauth2/service-account#httprest

[5] Using OAuth 2.0 for Server to Server Applications:  https://developers.google.com/identity/protocols/oauth2/service-account

[6] jjwt Library (Java JWT: JSON Web Token for Java and Android):  https://github.com/jwtk/jjwt

[7] Maven Repository to download jars: https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt
17 Comments
Labels in this area