Call Google Cloud APIs from SAP BTP ABAP Environment using a signed JWT (+ BTP Cloud Foundry app)
Introduction
Presented below is a sample approach for calling Google Cloud APIs using signed JWTs. The method is described here with code for on-prem ABAP. Here I use ABAP environment in SAP BTP (aka Steampunk). To get the understanding how it works read the previous post and continue here.
So, what has changed for ABAP Steampunk - in the cloud there is no STRUST transaction to create a PSE
and import the private key. I was also not able to find released objects for signing etc.
I tried to check SAP Business API if there is an API available which might help, but no luck.
My own API for signing using Node app with
jsonwebtoken
seemed to be a quite quick
approach to check.
Cloud Foundry part - API endpoint for signing JWTs
I prepared a simple API using Express with the help of sap/xssec
, sap/xsenv
and passport
modules to use OAuth/client credentials as a protection. My jwt-backend
has one route - /sign
and accepts a JSON with a Google API endpoint which I would like to call. For simplicity of PoC
all key details - private key id, private key and client email - are taken from environment variables.
For production use
BTP "Credential Store"
service might be used.
[I use SAP BTP Free Tier, Cloud Foundry CLI v8] The code presented here is also available in the GitHub repo.
const express = require("express");
const jwt = require("jsonwebtoken");
const passport = require("passport");
const { JWTStrategy } = require("@sap/xssec");
const xsenv = require("@sap/xsenv");
let PORT = process.env.PORT || 5000;
passport.use(new JWTStrategy(xsenv.getServices({ uaa: { tag: "xsuaa" } }).uaa));
const app = express();
app.use(express.json());
app.use(passport.initialize());
app.use(passport.authenticate("JWT", { session: false }));
app.get("/sign", sign);
function sign(req, res, next) {
const privateKeyId = process.env.JWT_BACKEND_KEY_ID;
const clientEmail = process.env.JWT_BACKEND_CLIENT_EMAIL;
let privateKey = process.env.JWT_BACKEND_PRIVATE_KEY;
if (!privateKey || !clientEmail || !privateKeyId) {
res.status(500).send("Missing values from environment variables");
} else {
privateKey = privateKey.replace(/\\n/gm, "\n");
const payload = {
iss: clientEmail,
sub: clientEmail,
aud: req.body.endpoint,
};
const signed = jwt.sign(payload, privateKey, {
algorithm: "RS256",
expiresIn: 3600,
keyid: privateKeyId,
});
res.status(200).send(signed);
}
}
app.listen(PORT, () => {
console.log(`Server is up and running on ${PORT} ...`);
});
xs-security.json
for this app is simple:
{
"xsappname": "jwt-backend",
"tenant-mode": "dedicated"
}
Creating a XSUUA service instance and a service key for it:
cf create-service xsuaa application jwt-backend-xsuaa -c xs-security.json
cf create-service-key jwt-backend-xsuaa jwt-backend-key
manifest.yml
for the app:
---
version: 1
applications:
- name: jwt-backend
memory: 128M
path: jwt-backend
routes:
- route: jwt-backend.cfapps.us10.hana.ondemand.com
instances: 1
command: node index.js
buildpacks:
- nodejs_buildpack
services:
- jwt-backend-xsuaa
cf push
the app and all pieces are in BTP:
The next step - setting the environment variables required by the code. The values are taken from the Google Cloud service account JSON file.
cf set-env jwt-backend JWT_BACKEND_KEY_ID "4df......................d3e"
cf set-env jwt-backend JWT_BACKEND_CLIENT_EMAIL "abap-backend@jwt-call-sample.iam.gserviceaccount.com"
cf set-env jwt-backend JWT_BACKEND_PRIVATE_KEY "-----BEGIN PRIVATE KEY-----\nMIIEvAI......."
cf restage jwt-backend

A quick test in browser - Unauthorized is the right response...
...because we need an OAuth token. Additionally, a JSON body with the Google API endpoint
need to be provided.
Before going to ABAP, a quick check via Postman with a token generated via client
credentials flow. A client id/secret/token URL are
taken from the service key generated for the jwt-backend-xsuaa
service instance in the
previous steps.

The body of the request:
{
"endpoint": "https://cloudresourcemanager.googleapis.com/"
}
The response contains my signed token:
ABAP Cloud part
Now the ABAP in the cloud side. The first, simple approach for testing (method
get_signend_jwt_by_url
) uses just URLs
for destinations and perform an OAuth client credentials flows explicitly. At the beginning the
token endpoint is called (url
taken from the service key of my jwt-backend-key
for the created
XSUAA service instance), next the token is attached as a bearer header and the final
request to the Google API endpoint (Cloud Resource Manager in this case) is performed.
The second approach (method get_signed_jwt_by_arrangement
) uses a
communication arrangement.
This method requires design and runtime artifacts.
Firstly, in Eclipse I created my outbound service:
Next, a communication scenario which uses my service:
Now the runtime part. In ABAP cloud dashboards there is a group for communication management:
I first created a communication system pointing to my backend, JWT signing app:
Here I need to provide client id/client secret in the "Users for Outbound Communication"
tab:
Next I created a communication arrangement, based on my design-time communication scenario:
All the puzzles together allows to use them in code to perform an OAuth client credentials flow - firstly the scenario is read, then the arrangement. No need to code OAuth calls.
The whole class with both methods:
CLASS zcl_google_api_jwt DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PRIVATE SECTION.
METHODS:
get_signend_jwt_by_url RETURNING VALUE(result) TYPE string
RAISING cx_http_dest_provider_error
cx_web_http_client_error,
get_signed_jwt_by_arrangement RETURNING VALUE(result) TYPE string
RAISING cx_http_dest_provider_error
cx_web_http_client_error,
get_project_details_json IMPORTING signed_jwt TYPE string
RETURNING VALUE(result) TYPE string
RAISING cx_web_http_client_error
cx_http_dest_provider_error.
ENDCLASS.
CLASS zcl_google_api_jwt IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
DATA: signed_jwt TYPE string,
project_details TYPE string.
TRY.
" Approach no 1: Call Google API using direct URLs
signed_jwt = get_signend_jwt_by_url( ).
project_details = get_project_details_json( signed_jwt ).
" Approach no 2: call Google API using a communication arrangement
signed_jwt = get_signed_jwt_by_arrangement( ).
project_details = get_project_details_json( signed_jwt ).
out->write( project_details ).
CATCH cx_root INTO DATA(exception).
out->write( exception->get_text( ) ).
ENDTRY.
ENDMETHOD.
METHOD get_signend_jwt_by_url.
" Call Google API using direct URLs
TYPES:
BEGIN OF ty_token_json,
access_token TYPE string,
token_type TYPE string,
id_token TYPE string,
refresh_token TYPE string,
expires_in TYPE i,
scope TYPE string,
jti TYPE string,
END OF ty_token_json.
" 1. Fetch token from XSUUA jwt-backend-xsuaa, details are in the service key
DATA(destination) = cl_http_destination_provider=>create_by_url(
'https://main-2f80y5al.authentication.us10.hana.ondemand.com/oauth/token' ).
DATA(http_client) = cl_web_http_client_manager=>create_by_http_destination( destination ).
DATA(request) = http_client->get_http_request( ).
DATA(body) = |grant_type=client_credentials|.
request->set_header_fields( VALUE #(
( name = 'Content-Type'
value = 'application/x-www-form-urlencoded; charset=UTF-8' )
( name = 'Accept'
value = 'application/json' )
( name = 'Content-Length'
value = strlen( body ) ) ) ).
request->set_authorization_basic(
i_username = 'sb-jwt-backend...'
i_password = 's......' ).
request->append_text( body ).
DATA(token_response) = http_client->execute( if_web_http_client=>post ).
DATA(token) = VALUE ty_token_json( ).
/ui2/cl_json=>deserialize(
EXPORTING
json = token_response->get_text( )
CHANGING
data = token ).
" 2. Get signed JWT using deployed API
destination = cl_http_destination_provider=>create_by_url(
'https://jwt-backend.cfapps.us10.hana.ondemand.com/sign' ).
http_client = cl_web_http_client_manager=>create_by_http_destination( destination ).
request = http_client->get_http_request( ).
DATA(bearer) = |Bearer { token-access_token }|.
request->set_header_field(
i_name = 'Authorization'
i_value = bearer ).
DATA(json_body) = '{ "endpoint": "https://cloudresourcemanager.googleapis.com/" }'.
request->set_header_fields( VALUE #(
( name = 'Content-Type'
value = 'application/json' )
( name = 'Content-Length'
value = strlen( json_body ) ) ) ).
request->set_text( i_text = CONV #( json_body )
i_length = strlen( json_body ) ).
DATA(response) = http_client->execute( if_web_http_client=>get ).
result = response->get_text( ).
ENDMETHOD.
METHOD get_signed_jwt_by_arrangement.
DATA(communication_system) = 'JWT_SIGNER'.
DATA(arrangement_factory) = cl_com_arrangement_factory=>create_instance( ).
DATA(comm_arrangement_range) = VALUE if_com_arrangement_factory=>ty_query-cs_id_range(
( sign = 'I' option = 'EQ' low = communication_system ) ).
arrangement_factory->query_ca(
EXPORTING
is_query = VALUE #( cs_id_range = comm_arrangement_range )
IMPORTING
et_com_arrangement = DATA(arrangements) ).
DATA(arrangement) = arrangements[ 1 ].
DATA(destination) = cl_http_destination_provider=>create_by_comm_arrangement(
comm_scenario = 'ZGOOGLE_API'
service_id = 'ZGOOGLE_API_REST'
comm_system_id = arrangement->get_comm_system_id( ) ).
DATA(http_client) = cl_web_http_client_manager=>create_by_http_destination( destination ).
DATA(request) = http_client->get_http_request( ).
DATA(json_body) = '{ "endpoint": "https://cloudresourcemanager.googleapis.com/" }'.
request->set_header_fields( VALUE #(
( name = 'Content-Type'
value = 'application/json' )
( name = 'Content-Length'
value = strlen( json_body ) ) ) ).
request->set_text( i_text = CONV #( json_body )
i_length = strlen( json_body ) ).
DATA(response) = http_client->execute( if_web_http_client=>get ).
result = response->get_text( ).
ENDMETHOD.
METHOD get_project_details_json.
DATA(destination) = cl_http_destination_provider=>create_by_url(
'https://cloudresourcemanager.googleapis.com/v1/projects/jwt-call-sample' ).
DATA(http_client) = cl_web_http_client_manager=>create_by_http_destination( destination ).
DATA(request) = http_client->get_http_request( ).
request->set_header_field(
i_name = 'Authorization'
i_value = |Bearer { signed_jwt }| ).
DATA(response) = http_client->execute( if_web_http_client=>get ).
result = response->get_text( ).
ENDMETHOD.
ENDCLASS.
Both methods produces a JSON with my Google Cloud project details:
Next steps
The next part of this series moves this signing app to Kubernetes using SAP BTP Kyma. The last part takes the Kyma approach but with the use of a serverless Function.