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: