Call Google Cloud APIs from SAP BTP ABAP Environment using a signed JWT (+ Kubernetes/Kyma serverless)
Introduction
This is the 4th part of my series about calling Google Cloud APIs from ABAP. It started from on-prem ABAP then went into ABAP cloud with the help of SAP BTP Cloud Foundry. Next, the JWT signing part was moved to Kubernetes hosted by Kyma Environment as a Deployment/Service. But I wanted to check also...
Serverless Function
A Function is another Kubernetes Custom Resource for serverless in Kyma way. They usually contain simple code snippets which can be then triggered by events or exposed via API Rules. JWT signing functionality seems to suit well into such category. My demo case is very simple and I map my solution 1:1 from a "regular", Node.js backend exposed via k8s to a serverless function, but in real life you can use such approach for more granular pull of some functionalities, make them independent and leverage the benefits of handling them via k8s/Kyma. If you like to read more about serverless you can read for example the articles how BBC is using them. More about Kyma Functions can be found here.
Most of the steps for preparation of k8s/Kyma artifacts are almost the same like in the
previous approach so I am not explaining them again here.
The repository with sample code is here,
folder kyma-serverless
.
Node.js Function for signing JWT
I adjusted the app from the previous part to match the Kyma function specification. If you
compare it you would see that Express is no longer needed - required is to have a main
function with a specific signature. Inside we have
access to the request/response as well as other values injected during runtime.
Firstly I created a new namespace and initialized the function:
kubectl apply -f namespace.yaml
cd signer-function
kyma init function --name jwt-backend-function --namespace jwt-backend-serverless
Then I adjusted the created handler.js
file. The Google Cloud API endpoint should
be delivered as a request parameter endpoint
.
const jwt = require("jsonwebtoken");
module.exports = {
main: function (event, context) {
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) {
event.extensions.response.status(500);
return;
} else {
privateKey = privateKey.replace(/\\n/gm, "\n");
const payload = {
iss: clientEmail,
sub: clientEmail,
aud: event.extensions.request.query.endpoint,
};
const signed = jwt.sign(payload, privateKey, {
algorithm: "RS256",
expiresIn: 3600,
keyid: privateKeyId,
});
event.extensions.response.status(200).send(signed);
}
},
};
I added jsonwebtoken
dependency to the package.json
:
{
"name": "jwt-backend-function",
"version": "0.0.1",
"dependencies": {
"jsonwebtoken": "^8.5.1"
}
}
Function & APIRule + OAuth
Next I updated the config.yaml
file to provide the environment variables; I used the files
created in the previous part:
kubectl create secret generic jwt-backend-api-secret --from-file=CLIENT_EMAIL --from-file=PRIVATE_KEY_ID --from-file=PRIVATE_KEY
--namespace jwt-backend-serverless
A Function also need to be exposed via APIRule and I wanted to use also OAuth as a protection. The steps are identical as in the previous part, only the namespace and service name are different:
apiVersion: gateway.kyma-project.io/v1beta1
kind: APIRule
metadata:
name: jwt-backend-function-apirule
namespace: jwt-backend-serverless
spec:
gateway: kyma-system/kyma-gateway
host: jwt-backend-function.c-531ecbb.kyma.ondemand.com
rules:
- accessStrategies:
- config:
handler: oauth2_introspection
methods:
- GET
path: /.*
service:
name: jwt-backend-function
port: 80
apiVersion: hydra.ory.sh/v1alpha1
kind: OAuth2Client
metadata:
name: jwt-backend-api-client
namespace: jwt-backend-serverless
spec:
grantTypes:
- "client_credentials"
scope: "sign"
secretName: jwt-backend-api-client
Applying all the files:
kyma apply function
kubectl apply -f apirule.yaml
kubectl apply -f oauth-client
a quick check after creation:

To use it I needed to extract the OAuth client ID/secret:
kubectl get secret -n jwt-backend-serverless jwt-backend-api-client -o jsonpath='{.data.client_id}' | base64 --decode
kubectl get secret -n jwt-backend-serverless jwt-backend-api-client -o jsonpath='{.data.client_secret}' | base64 --decode
Calling the Function from ABAP
Similar like in the previous part
I created a new communication system with
the credentials from OAuth2Client and another communication arrangement using
this system. Then used my function, which is exposed via host
in apirule.yaml
under jwt-backend-function.c-531ecbb.kyma.ondemand.com
address.

As a sample I am targeting Google Cloud Resource Manager API to get details about my project.
CLASS zcl_google_api_via_kyma_srvlss DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PRIVATE SECTION.
METHODS:
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_via_kyma_srvlss IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
TRY.
" Call Google API using a communication arrangement
DATA(signed_jwt) = get_signed_jwt_by_arrangement( ).
DATA(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_signed_jwt_by_arrangement.
" Call Google API using a communication arrangement
DATA(communication_system) = 'JWT_SIGNER_KYMA_SERVERLESS'.
DATA(arrangement_factory) = cl_com_arrangement_factory=>create_instance( ).
DATA(comm_system_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_system_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( ).
request->set_query( 'endpoint=https://cloudresourcemanager.googleapis.com/' ).
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.
The result:
