There are various ways to call Google Cloud APIs. The method presented here is based on the one presented by Dimitri Seifmann here), but it uses a signed JSON Web Token (JWT) for authorization, as described here.

Summarizing this method - you use your service account details to prepare and sign a JWT, which next is attached as a bearer to the request. This method is available for Google Cloud APIs published in Google APIs GitHub repository which means PubSub, App Engine, Cloud Function and a lot more.

👷‍♀️ Preparations

Here is an example for my sample project, for which I have created a service account. You will also need the keys in JSON and P12 format - both can be created and downloaded from the Google Console in the "Keys" tab. Note that the default password for P12 is "notasecret" - this password will be required later.

Next, the ABAP side. I am using ABAP 1909 Platform. Follow the instructions from Dimitri's blog post about importing the service account details (point 3.1) in STRUST:

  • create and a new SSF application. Mine is called "GAPI".
  • import the P12 service account file to the new app in STRUST. Here you will be asked to provide the P12 file password.
  • get the SSL certificate and add it to the SSL Anonymous node.
  • finally create a new destination in SM59 with SSL Anonymous enabled. In this example I am using Cloud Resource Manager APIs.

👩‍💻 ABAP code

Having all prepared, let use it to make a call to to Google Cloud Resource Manager API to get the details of my project. The steps to use signed JWT are very well documented on the Google page. We need to prepare a JWT with details specific from our service account JSON file we downloaded in preparations. We will two fields from it: client_email and private_key_id plus our endpoint URL, UNIX timestamps for validity and algorithm type. For my service account: in ABAP we will create such JWT: which will be signed with the private key we imported in STRUST from P12 file. Next, such signed JWT will be added to the HTTP request headers as an authorization bearer.

For simplicity I have bundled everything in one class and hardcoded what can be provided from a configuration table, DDIC types; no exception handling etc. Note that the signed JWT has a validity period you set (in my example it is 1h), so you can buffer and reuse it (in real life I use SHMA enabled class + autobuild each 1h to regenerate the token).

Available also as an abapGit repo.

CLASS zcl_google_api_jwt DEFINITION PUBLIC FINAL CREATE PUBLIC.
  PUBLIC SECTION.
    INTERFACES if_oo_adt_classrun.

  PRIVATE SECTION.
    METHODS:
      create_jwt_token RETURNING VALUE(result) TYPE string,

      sign_jwt IMPORTING jwt_base64    TYPE string
               RETURNING VALUE(result) TYPE string,

      get_project_details_json IMPORTING signed_jwt    TYPE string
                               RETURNING VALUE(result) TYPE string.
ENDCLASS.

CLASS zcl_google_api_jwt IMPLEMENTATION.

  METHOD if_oo_adt_classrun~main.

    DATA(jwt_base64) = create_jwt_token( ).
    DATA(signature) = sign_jwt( jwt_base64 ).
    DATA(signed_jwt) = |{ jwt_base64 }.{ cl_http_utility=>encode_base64( unencoded = signature ) }|.
    DATA(final_jwt) = translate( val = signed_jwt from = '+/=' to = '-_' ). " we need base64-url
    DATA(project_json) = get_project_details_json( final_jwt ).
    out->write( project_json ).

  ENDMETHOD.

  METHOD create_jwt_token.
    " JWT timestamp
    GET TIME STAMP FIELD DATA(timestamp).
    CONVERT TIME STAMP timestamp TIME ZONE 'UTC' INTO DATE DATA(date) TIME DATA(time).

    cl_pco_utility=>convert_abap_timestamp_to_java(
      EXPORTING
        iv_date      = date
        iv_time      = time
        iv_msec      = 0
      IMPORTING
        ev_timestamp = DATA(unix_timestamp)
    ).

    DATA(iat) = substring( val = unix_timestamp off = 0 len = strlen( unix_timestamp ) - 3 ).

    " Prepare JWT claims and header.
    " We are using private_key_id and client_email from the service account JSON file.
    TYPES: BEGIN OF jwt_payload,
             iss TYPE string,
             sub TYPE string,
             " Audience - is the API endpoint, in this example it is https://cloudresourcemanager.googleapis.com
             aud TYPE string,
             iat TYPE int4, " Issued at
             exp TYPE int4, " Expires
           END OF jwt_payload.

    TYPES: BEGIN OF jwt_header,
             alg TYPE string,
             typ TYPE string,
             kid TYPE string, " private key id
           END OF jwt_header.

    " In this example we are calling Cloud Resource Manager API.
    DATA(jwt_payload) = VALUE jwt_payload(
      " Both iss and sub (issuer and subject) point to the client_email field from the service account JSON file
      iss = 'abap-backend@jwt-call-sample.iam.gserviceaccount.com'
      sub = 'abap-backend@jwt-call-sample.iam.gserviceaccount.com'
      " Audience - API endpoint, pay attention to the traling "/" - it is required
      aud = 'https://cloudresourcemanager.googleapis.com/'
      iat = iat
      exp = iat + 3600
    ).

    DATA(jwt_header) = VALUE jwt_header(
      typ = 'JWT'
      alg = 'RS256'
      " private key id from the service account JSON file
      kid =  '4d...' ).

    DATA(jwt_payload_json) = /ui2/cl_json=>serialize(
      data  = jwt_payload
      pretty_name = /ui2/cl_json=>pretty_mode-low_case ).

    DATA(jwt_header_json) = /ui2/cl_json=>serialize(
      data = jwt_header
      pretty_name = /ui2/cl_json=>pretty_mode-low_case ).

    DATA(jwt_header_base64) = cl_http_utility=>encode_x_base64(
      unencoded = cl_abap_codepage=>convert_to( jwt_header_json ) ).

    DATA(jwt_payload_base64) = cl_http_utility=>encode_x_base64(
      unencoded = cl_abap_codepage=>convert_to( jwt_payload_json ) ).

    DATA(jwt_base64) = |{ jwt_header_base64 }.{ jwt_payload_base64 }|.
    result = translate( val = jwt_base64 from = '+/=' to = '-_' ). " we need base64-url
  ENDMETHOD.

  METHOD sign_jwt.
    DATA(jwt_base64_xstring) = cl_abap_codepage=>convert_to( source = jwt_base64 ).
    DATA jwt_base64_xstring_tab TYPE STANDARD TABLE OF ssfbin WITH KEY table_line.

    CALL FUNCTION 'SCMS_XSTRING_TO_BINARY'
      EXPORTING
        buffer     = jwt_base64_xstring
      TABLES
        binary_tab = jwt_base64_xstring_tab.

    DATA signer TYPE STANDARD TABLE OF ssfinfo.
    " The name of the PSE is showed when you are creating a SSF application
    signer = VALUE #( ( id = '<implicit>' profile = 'SAPGAPI001.pse' result = 28 ) ).

    DATA(input_length) = strlen( jwt_base64 ).
    DATA output_length TYPE ssflen.
    DATA jwt_signature_xstring_tab TYPE STANDARD TABLE OF ssfbin.

    CALL FUNCTION 'SSF_KRN_SIGN'
      EXPORTING
        str_format                   = 'PKCS1-V1.5'
        b_inc_certs                  = abap_false
        b_detached                   = abap_false
        b_inenc                      = abap_false
        ostr_input_data_l            = input_length
        str_hashalg                  = 'SHA256'
      IMPORTING
        ostr_signed_data_l           = output_length
      TABLES
        ostr_input_data              = jwt_base64_xstring_tab
        signer                       = signer
        ostr_signed_data             = jwt_signature_xstring_tab
      EXCEPTIONS
        ssf_krn_error                = 1
        ssf_krn_noop                 = 2
        ssf_krn_nomemory             = 3
        ssf_krn_opinv                = 4
        ssf_krn_nossflib             = 5
        ssf_krn_signer_list_error    = 6
        ssf_krn_input_data_error     = 7
        ssf_krn_invalid_par          = 8
        ssf_krn_invalid_parlen       = 9
        ssf_fb_input_parameter_error = 10.

    ASSERT sy-subrc = 0.

    CALL FUNCTION 'SCMS_BINARY_TO_STRING'
      EXPORTING
        input_length = output_length
        encoding     = '4110'
      IMPORTING
        text_buffer  = result
      TABLES
        binary_tab   = jwt_signature_xstring_tab
      EXCEPTIONS
        failed       = 1
        OTHERS       = 2.

    ASSERT sy-subrc = 0.
  ENDMETHOD.

  METHOD get_project_details_json.

    cl_http_client=>create_by_destination(
      EXPORTING
        destination              = 'GAPI'
      IMPORTING
        client                   = DATA(http_client)
      EXCEPTIONS
        argument_not_found       = 1
        destination_not_found    = 2
        destination_no_authority = 3
        plugin_not_active        = 4
        internal_error           = 5
        OTHERS                   = 6 ).

    ASSERT sy-subrc = 0.

    http_client->request->set_method( if_http_request=>co_request_method_get ).

    http_client->request->set_header_field(
      name  = 'Authorization'
      value = |Bearer { signed_jwt }| ).

    cl_http_utility=>set_request_uri(
      request = http_client->request
      uri     = 'projects/jwt-call-sample' ).

    http_client->send(
      EXCEPTIONS
        http_communication_failure = 1
        http_invalid_state         = 2
        http_processing_failed     = 3
        http_invalid_timeout       = 4
        OTHERS                     = 5 ).

    ASSERT sy-subrc = 0.

    http_client->receive(
      EXCEPTIONS
        http_communication_failure = 1
        http_invalid_state         = 2
        http_processing_failed     = 3 ).

    ASSERT sy-subrc = 0.
    result = http_client->response->get_cdata( ).

  ENDMETHOD.
ENDCLASS.

The output is a JSON result about the Google Cloud project:

Pay attention to the trailing "/" in the aud JWT field, without it you would get response that authentication is not valid; also the API you want to use have to be enabled:

Next steps

In another post I describe the same method, but used from SAP BTP ABAP environment with the help of a Cloud Foundry app for signing. The third part 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.