Technical Articles
SAP Cloud Integration (CPI) and PI/PO – Implement an OAuth JWT Bearer Flow on the Example of the Google API
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 Kumar Vellingiri 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:
- Create and download a private key on Google Cloud.
- Define and generate a JWT assertion using the jjwt library.
- Sign the assertion using Java built-in classes to produce the JWT token.
- Use the JWT token to fetch the Bearer token with the authorization server.
- Use the Bearer token in the API requests.
Let’s begin with creating the private key in the Google Cloud console:
- 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”.
- 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).
- 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):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:
- 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.
- 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”.
- 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:
- 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]).
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):
- 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):
- “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-****************************
- 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:
- 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):
- 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
- 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)"
- 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-part-2/
[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
Unfortunaly when I use this grant_type = urn:ietf:params:oauth:grant-type:jwt-bearer I get error
Can you explain your scenario and what exactly you have done a bit more? Which API are you trying to connect to (in case it's a public one)? I assume it's not a Google API. Are you on Cloud Integration or PO?
Hm, the error message is strange, it mentions the string "jwt_token", but that is not the grant_type value that you passed, right? That parameter should look something like this (it has to be URL encoded, and there is no space before and after the equal sign):
Thanx alot. My mistakes about grant_type parameter value
Great work.
I applied the instructions and worked beautifully. Only minor cosmetic changes are related to our Google account. One was the scope, in my case the scope was: https://www.googleapis.com/auth/devstorage.read_write
I was using the Google Upload API using https://storage.googleapis.com/bucket/ContanetName
I attached a screen print that is specific to utilizing GCS.
Happy to hear that it worked and thanks for sharing your solution!
Indeed great work Philippe!
I have adapted it to be used for JWT authentication for DocuSign.
As background DocuSign supports both OAuth Authorization code grant as well as JWT. Since in CPI you can create OAuth Code grant credentials that was my first choice.
But to my surprise I couldn't use these credentials in the http adapter. Apparently if I want to authenticate that way CPI needs the OpenConnectors license which has a DocuSign connection.
So onwards to JWT.
I could not get the Docusign RSA keys to work. I did not succeed in creating a .pfx from the public & private key, so I used another keypair and uploaded the public key to DocuSign.
The implementation only differs slightly from the google API. The header required two parameters, both algorithm & type. The payload contained a few more parameters, most notably epoch timestamps for current time and expiration time. I've used 10 position timestamps for this as that was in the DocuSign specs.
So all in all pretty happy with this!
Regards
Tom
Thank for sharing your adapted solution Tom! Good to know that the approach works flexibly.
Hi Tom,
I made it work in CPI with just RSA Private Key!
With this adjustmen and the RSA Private Key (without line-breaks and annotations) it successfully generates access-tokens.
I also switche assertion with grant_type in the request body. I dont know if that makes any differences.
DocuSign provides a huge package of postman sample requests and there they also put assertion first.
BR,
Johannes
Hi Johannes Schneider
I am implementing the docusign integration with the JWT bearer. Please provide the import class details and jar files required for your code.
Thank you!
Great work Philippe Addor
The code provided for 3b variant did not work.
I would like to add the code which worked.
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).
I hope this helps.
Regards,
Sunil
Thanks Sunil for letting me know and adding a working code snippet for that case!
Regards,
Philippe
Hi Philippe,
I try to use your code using the scenario 3b; I installed jjwt-api-0.11.5 as jar (maven say it is the new one library) with Sunil variant
But when executed the flo I got the following error
Shall I install other libraries in order to get my scenario works?
Hi Marco
Have you uploaded all required dependencies to the IFlow? See steps 5 and 6.
And do you have all the import statements at the top of your groovy script, as shown in code at step 7? Especially look for the ones beginning with java.security.spec...
Maybe your version of jjwt has different requirements. You need to figure them out and upload all necessary jars to the IFlow resources.
Hope it works!
Philippe
Good blog! Thanks, it helped.
Great blog!
Thanks! I implement it in integration suite and it is working.
Unfortunately I get 401. In the payload I see I have access_token. I noticed I need for the successful connection to the endpoint not the access_token but the id_token.
Do you know how I may get it?
Best wishes
Rares