Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
former_member324164
Participant

1. Introduction


In our recent project we had the requirement to connect from an AS ABAP system to an API published on Google Cloud Platform App Engine. The API was secured through Googles Identity Aware Proxy (IAP) and Google Cloud Identity & Access Management (IAM) service accounts for authentication to prevent unauthorized use.

This blog describes how to solve the problem on ABAP side to consume a GCP App Engine resource protected through Google IAP. This includes how to generate a Json Web-Token(JWT) signed with service account credentials using RS256 encryption, how to exchange the JWT for Google-signed OpenID(OIDC) token and how to use the OIDC token to request the IAP-protected resource on Google App Engine.

2. Enabling Identity-Aware Proxy for App Engine


First thing to be considered before deploying an API to Googles App Engine, is to think about how to prevent it from being used unauthorized. Research on this topic gives two different solutions for securing endpoints published on App Engine: Cloud Endpoints and Identity-Aware Proxy.

With Cloud Endpoints it is possible to describe the authentication and authorization processes by using OpenAPI standard. This gives you many possibilities to define how an endpoint is secured with roles and which user or technical user (service accounts) are able to access different endpoints. I won`t go into details about Cloud Endpoints in this blog, if you are interested to learn more about it, you can progress here: https://cloud.google.com/endpoints/docs/.

One major drawback of Cloud Endpoints for our project was, that Cloud Endpoints only supports apps that were published with the environment parameter set to flex on Google App Engine. The flexible environment parameter in the app.yaml describes how google should set up the environment in which your app is running and flexible is using basically docker container as a wrapper for your application. For our use case, we did not want to have Docker, since this requires additional security steps for the application.

The second option is to use the Identity-Aware Proxy. Cloud Identity-Aware Proxy (Cloud IAP) controls access to your cloud applications and VMs running on Google Cloud Platform. Cloud IAP works by verifying user identity and context of the request to determine if a user should be allowed to access an application or a VM. It is less versatile as Cloud Endpoints but easier to use. It also supports apps that were deployed in the standard environment of GCP App Engine.

First step is to create a new IAP-secured Web App User. Navigate to IAM&Admin -> Service accounts and create a new service account. Type in a name and description and press create. On the next screen you have to select a role. Type in IAP in the search and select the role IAP-secured Web App User.



In the next window you can grant user access to the service account, which is not necessary for the scenario. Press the "Create Key" button and select the option P12. This is needed later for AS ABAP import to STRUST.



The P12 file download should start to your local machine. The service account allows access to the cloud resources, so store it securely. Note the shown secret for later use.

Next step is to activate the IAP for App Engine. Navigate to "Security -> Identity Aware Proxy"



In the list all your available HTTP resources are shown. There you can switch the IAP on for App Engine by moving the switch to the right. All your apps that run in App Engine are listed below.



Select the app in the list you want to add service account authentication to and add the new created service account on the right-hand side panel. Select "IAP-secured Web App User" as role.



Later, we also need the OAUTH Client ID for communication from AS ABAP. To get the ID navigate to IAP and click the three dots button on the right of "App Engine app" and select "Edit OAuth Client" from the menu.



In the next window you can find the Client ID of the OAUTH client on the top. Note down the ID for later use.

With this, we have created a new technical user (service account) and have enabled Identity Aware Proxy for our App Engine HTTP resources. Now whenever the resource is called the IAP is requesting an authentication and checks if the user is authorized to use the resource. We are done on Google side. Now we need to implement the access from an AS ABAP instance.

3. AS ABAP to GCP App Engine access


This chapter shows how to configure ABAP AS for access to GCP and how to write a access handler class in ABAP that will communicate with HTTP resources deployed on GCP App Engine.

Before we continue, we need to understand how the authentication process works with IAP. This flow diagram shows what steps are needed to access a protected resource:



(source: https://bravenewgeek.com/api-authentication-with-gcp-identity-aware-proxy/)

The API consumer is the ABAP AS. First, a JSON Web Token needs to be created and signed with service account private key. The private key can be found in the P12 file that we have downloaded when creating the service account. The signature needs to be RS256 encrypted. Next, we need to exchange the signed JWT with an Open ID token (OIDC). Google offers a service for JWT to OIDC exchange. For return, the service provides the OIDC that can be used to access the IAP protected resource.

3.1 Import service account certificate to STRUST


Before we can write ABAP code and consume our App Engine resource, we have to make sure two things: First, we need to import the P12 file into the AS ABAP system, second we have to make sure, google is a trusted source for communication.

3.1.1 Create new SSF Application


In ABAP AS the certificates are organized in SSF Applications. It is recommended to create a new SSF application for each new use case. Let’s create a new entry in table SSFAPPLIC. Go to transaction SE11 and open the table. Go to data browser and create a new entry.



Use JWT_SI for APPLIC and select everything except B_INCCERTS, B_DETACHED, B_ASKPWD. As Description we set JWT Signature. This entry will be later a new node in transaction STRUST where we can import certificates. Save the new entry.

Next open transaction SSFA. Press "New Entries". In the dropdown there should be a new option that we just created in table SSFAPPLIC. Select it and set the properties as shown in the screenshot.



This will give us a new node in transaction STRUST.

3.1.2 Import Certificates into STRUST


Open transaction STRUST and a new node should be available with name "SSF JWT Signature".



Go into "Edit" mode, right click the new node and select "Create" from the context menu. In the "Create PSE" window set Algorithm to "RSA" and Signature Algorithm to "SHA256".



Confirm the selection and the new node will now be available for imports. From the top menu select "PSE->Import" and select the service account P12 file you have downloaded. You might need to enter the secret that was shown when downloading the P12 file from GCP.

Now the P12 file is loaded into the "File" node in STRUST. Next, we need to move it from "File" node to the right SSF Application. On the top menu select "PSE->Save as". In the next window select "SSF Application" and select the SSF application we have created in the previous steps.



Confirm the selection and press save. With this, we have now imported the Service Account P12 file into the ABAP AS and can use it to sign our JWT for requests to GCP. With STRUST we have a secure place to store the service account private key and certificate information.

Next, we need to make Google to be a trusted source for communication. This can be achieved by importing the Google Root CA into STRUST.

Go to "https://www.googleapis.com" and download the certificate from the browser. In transaction STRUST, double click the node "SSL Client (Anonymous)". On the right-hand side panel, in the very bottom press the "Import certificate" button and select the downloaded certificate file. Then press the "Add to Certificate List" button. Confirm by pressing "Save" button at the very top.



With this we have achieved two things: First, we have now the GCP service account private key and certificate imported into the system and second, google is now a trusted source for communication. Now we can create the HTTP connections that will be used for communication in our ABAP code later.

3.2 The ABAP connector


This section covers the setup and code of a ABAP connector class, that will be used to consume the  App Engine Endpoints.

3.2.1 HTTP Connections to External System


As shown in the sequence diagram, we need to make two requests. The first request to exchange a signed Json-Web-Token (JWT) for an OpenID Connect-Token (OIDC) with Google. The second request to consume the protected resource on App Engine. For this, we create two "HTTP Connections to External System" in transaction SM59. Starting with the endpoint to exchange JWT with OIDC, press on "Create" button. In the next Window fill in "RFC Destination" name. Call this destination GCP_OAUTH2_TOKEN. Connection Type is "G" and will indicate that this is a connection to external server.



For target host enter "www.googleapis.com" and for path prefix "/ouath2/v4/token". This is an endpoint provided by google for JWT to OIDC exchange. For Service No enter 443 which is the HTTPS port number. On "Logon & Security" tab activate SSL and select ANONYM SSL Client. Remember the Google Root certificate we imported in STRUST in chapter 3.1.2? This will now be checked whenever a connection to www.googleapis.com is opened and www.googleapis.com will be set as a trusted source.



On "Special Options" tab set HTTP version to HTTP 1.1 and Accept Cookie to Yes(All).



Now the connection needs to be saved and can be tested with a press on "Connection Test", which should result in HTTP status code 200.

Next cerate another RFC destination which will be the endpoint running on App-Engine. Adjust the settings same as for the OIDC connection. Only thing that needs to be changed is the target host and path prefix on "Technical Settings" tab.



Next, we can start to write the ABAP connector class.

3.2.2 ABAP Connector


This chapter covers the ABAP code in form of an ABAP class that will create and sign a JWT with the private key of the service account that secures the App-Engine resource, exchange the JWT for a OIDC token and use the OIDC token to communicate with the App-Engine endpoint.

Before we start to code the class, create two new structures that will be used for the header and payload of our JWT. A JWT has a specific format, read more on JWT here: https://jwt.io/

Create two new structures: zgcp_jwt_header and zgcp_jwt_payload.



The zgcp_jwt_header is the JWT header and need two properties:

  • ALG stands for Algorithm and will include the algorithm that is used for encryption which is RS256

  • TYP stands for token type and will be JWT


The payload structure is zgcp_jwt_payload:



  • ISS stands for issuer and will be the name of our Google Service Account

  • AUD stands for audience, basically the consumer of the token

  • TARGET_AUDIENCE is the id of out OAUTH client at Google

  • IAT stands for issued at time and is a timestamp in UNIX time, when we created the token

  • EXP stands for expires and is a timestamp in UNIX time when the token will expire.


Both structures will be the JWT. Next, cerate a new ABAP class, in this example the class is called ZCL_GCP_API_HANDLER. We can start with the first step of the sequence diagram from chapter 3 which would be to

  • Create an JWT

  • Sign and encrypt the JWT with the private key of the into STRUST imported Service Account


Create a new class ZCL_GCP_API_HANDLER. In the private section of the class create two new local types which we will use later:
    TYPES:
ltty_tssfbin TYPE STANDARD TABLE OF ssfbin WITH KEY table_line WITHOUT FURTHER SECONDARY KEYS,

BEGIN OF oidc_token_json,
id_token TYPE string,
END OF oidc_token_json.


  • We will use ltty_tssfbin to define tables of type ssfbin which is needed for function module SSF_KRN_SIGN to sign the JWT.

  • oidc_token_json will be used to deserealize the OIDC json token to a abap structure after the exchange with Google.


Create a new method called create_rs256_signed_jwt. The methods signature looks as follows:
    CLASS-METHODS create_rs256_signed_jwt
IMPORTING
iv_jwt_header TYPE zgcp_jwt_header
iv_jwt_payload TYPE zgcp_jwt_payload
iv_ssf_profilename TYPE string
iv_ssf_id TYPE string
iv_ssf_result TYPE i
RETURNING VALUE(rv_signed_jwt_base64) TYPE string
RAISING zcx_gcp_api_handler.

As already mentioned, the encryption for JWT signature needs to be RS256. Importing parameters are a JWT header and JWT payload in form of the structures we have created, SSF profile name that we created in chapter 3.1.1, the SSF id and an SSF result. Returning value of the method should be a signed JWT base64 encoded in string format.

The full methods implementation looks as follows:
METHOD create_rs256_signed_jwt.
DATA lt_input_bin TYPE STANDARD TABLE OF ssfbin.
DATA lt_output_bin TYPE STANDARD TABLE OF ssfbin.
DATA lv_input_length TYPE ssflen.
DATA lv_output_length TYPE ssflen.
DATA lv_output_crc TYPE ssfreturn.
DATA lt_signer TYPE STANDARD TABLE OF ssfinfo.
DATA lv_unix_iat TYPE string.

DATA(lv_jwt_payload) = /ui2/cl_json=>serialize(
data = iv_jwt_payload
pretty_name = /ui2/cl_json=>pretty_mode-low_case
).
DATA(lv_jwt_header) = /ui2/cl_json=>serialize(
data = iv_jwt_header
pretty_name = /ui2/cl_json=>pretty_mode-low_case
).

DATA(lv_jwt_header_base64) = cl_http_utility=>encode_base64( unencoded = lv_jwt_header ).
DATA(lv_jwt_payload_base64) = cl_http_utility=>encode_base64( unencoded = lv_jwt_payload ).

DATA(lv_data_base64) = |{ lv_jwt_header_base64 }.{ lv_jwt_payload_base64 }|.
base64_url_encode(
CHANGING
iv_base64 = lv_data_base64
).

TRY.
lt_input_bin = string_to_binary_tab( iv_string = lv_data_base64 ).
CATCH zcx_gcp_api_handler INTO DATA(lo_cx).
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = lo_cx->textid.
ENDTRY.

lt_signer = VALUE #( ( id = iv_ssf_id profile = iv_ssf_profilename result = iv_ssf_result ) ).

lv_input_length = strlen( lv_data_base64 ).

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 = lv_input_length
str_hashalg = 'SHA256'
IMPORTING
ostr_signed_data_l = lv_output_length
crc = lv_output_crc " SSF Return code
TABLES
ostr_input_data = lt_input_bin
signer = lt_signer
ostr_signed_data = lt_output_bin
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.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_signature_failed.
ENDIF.

TRY.
DATA(lv_signature) = binary_tab_to_string(
it_bin_tab = lt_output_bin
iv_length = lv_output_length
).
CATCH zcx_gcp_api_handler INTO DATA(lo_zcx).
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = lo_zcx->textid.
ENDTRY.

DATA(lv_jwt) = |{ lv_data_base64 }.{ cl_http_utility=>encode_base64( unencoded = lv_signature ) }|.
base64_url_encode(
CHANGING
iv_base64 = lv_jwt
).

rv_signed_jwt_base64 = lv_jwt.
ENDMETHOD.

Code Explanation:

First, the imported ABAP structures for JWT header and payload are serialized as json string. For json handling in ABAP we can use the class /ui2/cl_json which has many handy options for json processing. Next, we need to base64 encode the json strings and concatenate the header and payload using the "." (dot) separator:
DATA(lv_jwt_payload) = /ui2/cl_json=>serialize(
data = iv_jwt_payload
pretty_name = /ui2/cl_json=>pretty_mode-low_case
).
DATA(lv_jwt_header) = /ui2/cl_json=>serialize(
data = iv_jwt_header
pretty_name = /ui2/cl_json=>pretty_mode-low_case
).

DATA(lv_jwt_header_base64) = cl_http_utility=>encode_base64( unencoded = lv_jwt_header ).
DATA(lv_jwt_payload_base64) = cl_http_utility=>encode_base64( unencoded = lv_jwt_payload ).

DATA(lv_data_base64) = |{ lv_jwt_header_base64 }.{ lv_jwt_payload_base64 }|.

Next, the string needs to be base64 URL encoded. For that we write our own helper function base64_url_encode:
    CLASS-METHODS base64_url_encode
CHANGING
iv_base64 TYPE string.

  METHOD base64_url_encode.
REPLACE ALL OCCURRENCES OF '=' IN iv_base64 WITH ''.
REPLACE ALL OCCURRENCES OF '+' IN iv_base64 WITH '-'.
REPLACE ALL OCCURRENCES OF '/' IN iv_base64 WITH '_'.
ENDMETHOD.

With this function we can call
    base64_url_encode(
CHANGING
iv_base64 = lv_data_base64
).

in the create_rs256_signed_jwt method. Now we need to sign the JWT with the private key of the service account that we have imported into STRUST. For signature we use function module SSF_KRN_SIGN. The function module expects a binary table as input and gives a binary table as output. This means, we need first to convert our string into a binary table, import it to the function module that will give us another binary table, which we again need to convert back into a string.

Let’s write two another methods that will do the conversion: string_to_binary_tab and binary_tab_to_string.
    CLASS-METHODS string_to_binary_tab
IMPORTING
iv_string TYPE string
RETURNING VALUE(rt_bin_tab) TYPE ltty_tssfbin
RAISING zcx_gcp_api_handler.

CLASS-METHODS binary_tab_to_string
IMPORTING
it_bin_tab TYPE ltty_tssfbin
iv_length TYPE ssflen
RETURNING VALUE(rv_string) TYPE string
RAISING zcx_gcp_api_handler.

And the implementation is:
  METHOD string_to_binary_tab.
DATA lv_xstring TYPE xstring.
CALL FUNCTION 'SCMS_STRING_TO_XSTRING'
EXPORTING
text = iv_string
encoding = '4110'
IMPORTING
buffer = lv_xstring
EXCEPTIONS
failed = 1
OTHERS = 2.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_strtobin_conversion_failed.
ENDIF.

CALL FUNCTION 'SCMS_XSTRING_TO_BINARY'
EXPORTING
buffer = lv_xstring
TABLES
binary_tab = rt_bin_tab.
ENDMETHOD.

  METHOD binary_tab_to_string.
CALL FUNCTION 'SCMS_BINARY_TO_STRING'
EXPORTING
input_length = iv_length
encoding = '4110'
IMPORTING
text_buffer = rv_string
TABLES
binary_tab = it_bin_tab
EXCEPTIONS
failed = 1
OTHERS = 2.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_bintostr_conversion_failed.
ENDIF.
ENDMETHOD.

This code converts the string into the binary table and the binary table back to a string. As already Next, convert the JWT string into a binary table with the call of:
    TRY.
lt_input_bin = string_to_binary_tab( iv_string = lv_data_base64 ).
CATCH zcx_gcp_api_handler INTO DATA(lo_cx).
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = lo_cx->textid.
ENDTRY.

Next, we can add a new entry into the lt_signer table, which will hold information about our SSF profile and application we have created, we also need to determine the string length of our encoded JWT token. Last, we can call the SSF_KRN_SIGN function module to sign the JWT with the private key of our service account. The output will be given in table lt_output_bin:
    lt_signer = VALUE #( ( id = iv_ssf_id profile = iv_ssf_profilename result = iv_ssf_result ) ).

lv_input_length = strlen( lv_data_base64 ).

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 = lv_input_length
str_hashalg = 'SHA256'
IMPORTING
ostr_signed_data_l = lv_output_length
crc = lv_output_crc " SSF Return code
TABLES
ostr_input_data = lt_input_bin
signer = lt_signer
ostr_signed_data = lt_output_bin
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.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_signature_failed.
ENDIF.

Now convert the lt_output_bin table back to a string with the use of the helper function binary_tab_to_string:
    TRY.
DATA(lv_signature) = binary_tab_to_string(
it_bin_tab = lt_output_bin
iv_length = lv_output_length
).
CATCH zcx_gcp_api_handler INTO DATA(lo_zcx).
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = lo_zcx->textid.
ENDTRY.

Only thing left is to concatenate the JWT with the signature and the "."(dot) separator. The signature needs to be base64 encoded too:
    DATA(lv_jwt) = |{ lv_data_base64 }.{ cl_http_utility=>encode_base64( unencoded = lv_signature ) }|.
base64_url_encode(
CHANGING
iv_base64 = lv_jwt
).

rv_signed_jwt_base64 = lv_jwt.

The return value of the create_rs256_signed_jwt function can now be set. As we have the signed JWT we need to exchange it with an OIDC with Google. For this create a new function called exchange_jwt_with_oidc_token. The function signature looks as follows:
    CLASS-METHODS exchange_jwt_with_oidc_token
IMPORTING
iv_exchange_destination TYPE c
iv_jwt_token TYPE string
RETURNING VALUE(rv_oidc_base64) TYPE string
RAISING zcx_gcp_api_handler.

The input parameters are the destination we have created in SM59 for exchanging the token and the signed JWT token itself. The returning value of the function is the OIDC token. Full method implementation looks as follows:
  METHOD exchange_jwt_with_oidc_token.
DATA lo_client TYPE REF TO if_http_client.
DATA ls_response TYPE oidc_token_json.

CALL METHOD cl_http_client=>create_by_destination
EXPORTING
destination = iv_exchange_destination
IMPORTING
client = lo_client
EXCEPTIONS
argument_not_found = 1
destination_not_found = 2
destination_no_authority = 3
plugin_not_active = 4
internal_error = 5
OTHERS = 6.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_oauth_dest_not_found.
ENDIF.

IF lo_client IS BOUND.
lo_client->request->set_method( if_http_request=>co_request_method_post ).
lo_client->request->set_formfield_encoding( formfield_encoding = if_http_entity=>co_formfield_encoding_encoded ).

lo_client->request->set_form_field(
EXPORTING
name = 'grant_type'
value = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
).

lo_client->request->set_form_field(
EXPORTING
name = 'assertion'
value = iv_jwt_token
).

lo_client->send( ).
lo_client->receive(
EXCEPTIONS
http_communication_failure = 1
http_invalid_state = 2
http_processing_failed = 3
).
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_oauth_token_receive_fail.
ENDIF.

DATA(lv_response_json) = lo_client->response->get_cdata( ).

/ui2/cl_json=>deserialize(
EXPORTING
json = lv_response_json
pretty_name = /ui2/cl_json=>pretty_mode-camel_case
CHANGING data = ls_response ).

IF ls_response-id_token IS INITIAL.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_oauth_token_receive_fail.
ENDIF.

rv_oidc_base64 = ls_response-id_token.
ENDIF.
ENDMETHOD.

Code Explanation:

First, we create a new HTTP client from our SM59 destination. Once the client is created, we need to set some form properties:

  • grant_type is 'urn:ietf:params:oauth:grant-type:jwt-bearer'

  • assertion is the given signed JWT token


Next, we can call send and receive. The OIDC token can now be received from the response in json format and parsed into a variable of our local type oidc_token.
      DATA(lv_response_json) = lo_client->response->get_cdata( ).

/ui2/cl_json=>deserialize(
EXPORTING
json = lv_response_json
pretty_name = /ui2/cl_json=>pretty_mode-camel_case
CHANGING data = ls_response ).

Now the function returning value can be set with:
rv_oidc_base64 = ls_response-id_token.

With this, we have signed a JWT with the private key of our Google Service Account and exchanged it for an OIDC token. Only thing left is to make the request to our API using the new OIDC token for authentication.

In the next step we can create a method to do the API request called "do_api_request". The signature of the method looks as follows:
    CLASS-METHODS do_api_request
IMPORTING
iv_destination TYPE c
iv_oidc_token TYPE string
iv_method TYPE string
iv_xcontent TYPE xstring OPTIONAL
iv_content TYPE string OPTIONAL
iv_sub_uri TYPE string OPTIONAL
it_header_fields TYPE tihttpnvp OPTIONAL
it_cookies TYPE tihttpcki OPTIONAL
iv_content_type TYPE string DEFAULT 'application/json'
RETURNING VALUE(rs_response) TYPE zgcp_response
RAISING zcx_gcp_api_handler.

Importing values are:

  • The destination we have created in SM59 to access our API

  • the OIDC token string

  • the method type for the request that could be GET/POST etc. (optional)

  • content as string and xstring if we need to post something (optional)

  • sub uri if we have multiple sub uris for our destination (optional)

  • header fields if we need any (optional)

  • a table to save cookies if needed (optional)

  • and the content type that will be default set to application/json (default application/json)


Full method implementation looks as follows:
METHOD do_api_request.
DATA lo_client_api TYPE REF TO if_http_client.
DATA lv_response TYPE string.
DATA lv_oidc TYPE string.

CALL METHOD cl_http_client=>create_by_destination
EXPORTING
destination = iv_destination
IMPORTING
client = lo_client_api
EXCEPTIONS
argument_not_found = 1
destination_not_found = 2
destination_no_authority = 3
plugin_not_active = 4
internal_error = 5
OTHERS = 6.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_api_dest_not_found.
ENDIF.

IF lo_client_api IS BOUND.
lv_oidc = |Bearer { iv_oidc_token }|.

lo_client_api->request->set_header_fields( fields = it_header_fields ).

lo_client_api->request->set_content_type( content_type = iv_content_type ).
lo_client_api->request->set_method( method = iv_method ).

* set jwt token auth
lo_client_api->request->set_header_field(
name = 'Authorization' ##NO_TEXT
value = lv_oidc
).
lo_client_api->request->set_header_field(
name = 'content-type'
value = iv_content_type
).


IF iv_sub_uri IS NOT INITIAL.
cl_http_utility=>set_request_uri(
request = lo_client_api->request
uri = iv_sub_uri
).
ENDIF.

IF iv_xcontent IS NOT INITIAL.
lo_client_api->request->set_data( data = iv_xcontent ).
ENDIF.

IF iv_content IS NOT INITIAL.
lo_client_api->request->set_cdata( data = iv_content ).
ENDIF.

LOOP AT it_cookies ASSIGNING FIELD-SYMBOL(<cookie>).
lo_client_api->request->set_cookie(
EXPORTING
name = <cookie>-name " Name of cookie
path = <cookie>-path " Path of Cookie
value = <cookie>-value " Cookie value
domain = <cookie>-xdomain " Domain Name of Cookie
expires = <cookie>-expires " Cookie expiry date
secure = <cookie>-secure " 0: unsaved; 1:saved
).
ENDLOOP.

lo_client_api->send( ).
lo_client_api->receive(
EXCEPTIONS
http_communication_failure = 1
http_invalid_state = 2
http_processing_failed = 3
).
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_api_receive_failed.
ENDIF.

rs_response-content = lo_client_api->response->get_data( ).
lo_client_api->response->get_status( IMPORTING code = rs_response-code
reason = rs_response-reason ).
lo_client_api->response->get_cookies( CHANGING cookies = rs_response-cookies ).
ENDIF.
ENDMETHOD.

First, we create a http client from the SM59 destination we have created for our API endpoint. Next, we have to set some request header properties:

Authorization field has to be in format "Bearer + OIDC token". We can create a string simply by concatenating the string 'Bearer ' with our OIDC token. We must set the content type, method type, sub uri and the content if available. We can add some cookies if needed too. The we can send the request and receive a response!

If everything went right, our GCP App Engine API should now respond with whatever response we have defined. In the next step a test class can be created to test the new GCP ABAP connector.

3.2.3 Testing the new ABAP connector class


A new class "ZCL_GCP_JWT_AUTH_TEST" can be created containing one method "test_api".
  METHOD test_api.

DATA(lv_iat) = zcl_gcp_api_handler=>get_iat_unixtime( ).
DATA(ls_jwt_payload) = VALUE zgcp_jwt_payload( iss = 'service account email'
aud = 'https://www.googleapis.com/oauth2/v4/token'
target_audience = 'OAuth client ID'
iat = lv_iat
exp = lv_iat + 30 ).

DATA(ls_jwt_header) = VALUE zgcp_jwt_header( typ = 'JWT'
alg = 'RS256' ).

TRY.
DATA(lv_signed_jwt) = zcl_gcp_api_handler=>create_rs256_signed_jwt(
EXPORTING
iv_jwt_header = ls_jwt_header
iv_jwt_payload = ls_jwt_payload
iv_ssf_profilename = 'SAPJWT_SI001.pse'
iv_ssf_id = '<implicit>'
iv_ssf_result = 28
).
CATCH zcx_gcp_api_handler.
ENDTRY.

TRY.
DATA(lv_oidc) = zcl_gcp_api_handler=>exchange_jwt_with_oidc_token(
iv_exchange_destination = 'GCP_OAUTH2_TOKEN'
iv_jwt_token = lv_signed_jwt
).
CATCH zcx_gcp_api_handler.
ENDTRY.

TRY.
DATA(lv_response) = zcl_gcp_api_handler=>do_api_request(
iv_destination = 'GCP_SA_AUTH_TEST'
iv_oidc_token = lv_oidc
iv_method = if_http_request=>co_request_method_post
iv_content = |\{"message": "hello world"\}|
).
CATCH zcx_gcp_api_handler.
ENDTRY.

ENDMETHOD.

Following steps needs to be performed:

  • Create JWT header and payload (read more on JWT here: https://jwt.io/

  • sign the JWT with private GCP service account key

  • exchange signed JWT with OIDC token

  • do secured App Engine api request


A JWT consists of a header and a payload. ABAP structures zgcp_jwt_payload and zgcp_jwt_header have been created to map the needed JWT data.

In payload, issuer which is the service account email needs to be provided, audience is the endpoint for OIDC token exchange from Google, target audience is the OAuth client ID (we took a note in chapter 2). iat and exp is the time when we issued the token and when it will expire in Unix time format (there is a helper function to get the Unix time of the system in the ABAP connector class ZCL_GCP_API_HADLER). Once the values are maped to the ABAP structures, we cann call the functions of the connector class as follows:

  • create_rs256_signed_jwt, which will return the signed JWT in string format

  • exchange_jwt_with_oidc_token, which will return the OIDC token in exchange for the JWT

  • do_api_request, which is the request to the App Engine endpoint and will return whatever was defined for the endpoint


With this code and configurations of the ABAP AS it is now possible to consume a Google App Engine API secured with IAP, from ABAP applications in a hybrid scenario.

4. Appendix


Full ZCL_GCP_API_HADLER class implementation:
CLASS zcl_gcp_api_handler DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .

PUBLIC SECTION.

CLASS-METHODS create_rs256_signed_jwt
IMPORTING
iv_jwt_header TYPE zgcp_jwt_header
iv_jwt_payload TYPE zgcp_jwt_payload
iv_ssf_profilename TYPE string
iv_ssf_id TYPE string
iv_ssf_result TYPE i
RETURNING VALUE(rv_signed_jwt_base64) TYPE string
RAISING zcx_gcp_api_handler.

CLASS-METHODS exchange_jwt_with_oidc_token
IMPORTING
iv_exchange_destination TYPE c
iv_jwt_token TYPE string
RETURNING VALUE(rv_oidc_base64) TYPE string
RAISING zcx_gcp_api_handler.

CLASS-METHODS do_api_request
IMPORTING
iv_destination TYPE c
iv_oidc_token TYPE string
iv_method TYPE string
iv_xcontent TYPE xstring OPTIONAL
iv_content TYPE string OPTIONAL
iv_sub_uri TYPE string OPTIONAL
it_header_fields TYPE tihttpnvp OPTIONAL
it_cookies TYPE tihttpcki OPTIONAL
iv_content_type TYPE string DEFAULT 'application/json'
RETURNING VALUE(rs_response) TYPE zgcp_response
RAISING zcx_gcp_api_handler.

CLASS-METHODS get_iat_unixtime RETURNING VALUE(rv_iat) TYPE int4.

PROTECTED SECTION.

PRIVATE SECTION.

TYPES:
ltty_tssfbin TYPE STANDARD TABLE OF ssfbin WITH KEY table_line WITHOUT FURTHER SECONDARY KEYS,

BEGIN OF oidc_token_json,
id_token TYPE string,
END OF oidc_token_json.

CLASS-METHODS string_to_binary_tab
IMPORTING
iv_string TYPE string
RETURNING VALUE(rt_bin_tab) TYPE ltty_tssfbin
RAISING zcx_gcp_api_handler.

CLASS-METHODS binary_tab_to_string
IMPORTING
it_bin_tab TYPE ltty_tssfbin
iv_length TYPE ssflen
RETURNING VALUE(rv_string) TYPE string
RAISING zcx_gcp_api_handler.

CLASS-METHODS base64_url_encode
CHANGING
iv_base64 TYPE string.
ENDCLASS.



CLASS ZCL_GCP_API_HANDLER IMPLEMENTATION.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Private Method ZCL_GCP_API_HANDLER=>BASE64_URL_ENCODE
* +-------------------------------------------------------------------------------------------------+
* | [<-->] IV_BASE64 TYPE STRING
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD base64_url_encode.
REPLACE ALL OCCURRENCES OF '=' IN iv_base64 WITH ''.
REPLACE ALL OCCURRENCES OF '+' IN iv_base64 WITH '-'.
REPLACE ALL OCCURRENCES OF '/' IN iv_base64 WITH '_'.
ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Private Method ZCL_GCP_API_HANDLER=>BINARY_TAB_TO_STRING
* +-------------------------------------------------------------------------------------------------+
* | [--->] IT_BIN_TAB TYPE LTTY_TSSFBIN
* | [--->] IV_LENGTH TYPE SSFLEN
* | [<-()] RV_STRING TYPE STRING
* | [!CX!] ZCX_GCP_API_HANDLER
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD binary_tab_to_string.
CALL FUNCTION 'SCMS_BINARY_TO_STRING'
EXPORTING
input_length = iv_length
encoding = '4110'
IMPORTING
text_buffer = rv_string
TABLES
binary_tab = it_bin_tab
EXCEPTIONS
failed = 1
OTHERS = 2.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_bintostr_conversion_failed.
ENDIF.
ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZCL_GCP_API_HANDLER=>CREATE_RS256_SIGNED_JWT
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_JWT_HEADER TYPE ZGCP_JWT_HEADER
* | [--->] IV_JWT_PAYLOAD TYPE ZGCP_JWT_PAYLOAD
* | [--->] IV_SSF_PROFILENAME TYPE STRING
* | [--->] IV_SSF_ID TYPE STRING
* | [--->] IV_SSF_RESULT TYPE I
* | [<-()] RV_SIGNED_JWT_BASE64 TYPE STRING
* | [!CX!] ZCX_GCP_API_HANDLER
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD create_rs256_signed_jwt.
DATA lt_input_bin TYPE STANDARD TABLE OF ssfbin.
DATA lt_output_bin TYPE STANDARD TABLE OF ssfbin.
DATA lv_input_length TYPE ssflen.
DATA lv_output_length TYPE ssflen.
DATA lv_output_crc TYPE ssfreturn.
DATA lt_signer TYPE STANDARD TABLE OF ssfinfo.
DATA lv_unix_iat TYPE string.

DATA(lv_jwt_payload) = /ui2/cl_json=>serialize(
data = iv_jwt_payload
pretty_name = /ui2/cl_json=>pretty_mode-low_case
).
DATA(lv_jwt_header) = /ui2/cl_json=>serialize(
data = iv_jwt_header
pretty_name = /ui2/cl_json=>pretty_mode-low_case
).

DATA(lv_jwt_header_base64) = cl_http_utility=>encode_base64( unencoded = lv_jwt_header ).
DATA(lv_jwt_payload_base64) = cl_http_utility=>encode_base64( unencoded = lv_jwt_payload ).

DATA(lv_data_base64) = |{ lv_jwt_header_base64 }.{ lv_jwt_payload_base64 }|.
base64_url_encode(
CHANGING
iv_base64 = lv_data_base64
).

TRY.
lt_input_bin = string_to_binary_tab( iv_string = lv_data_base64 ).
CATCH zcx_gcp_api_handler INTO DATA(lo_cx).
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = lo_cx->textid.
ENDTRY.

lt_signer = VALUE #( ( id = iv_ssf_id profile = iv_ssf_profilename result = iv_ssf_result ) ).

lv_input_length = strlen( lv_data_base64 ).

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 = lv_input_length
str_hashalg = 'SHA256'
IMPORTING
ostr_signed_data_l = lv_output_length
crc = lv_output_crc " SSF Return code
TABLES
ostr_input_data = lt_input_bin
signer = lt_signer
ostr_signed_data = lt_output_bin
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.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_signature_failed.
ENDIF.

TRY.
DATA(lv_signature) = binary_tab_to_string(
it_bin_tab = lt_output_bin
iv_length = lv_output_length
).
CATCH zcx_gcp_api_handler INTO DATA(lo_zcx).
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = lo_zcx->textid.
ENDTRY.

DATA(lv_jwt) = |{ lv_data_base64 }.{ cl_http_utility=>encode_base64( unencoded = lv_signature ) }|.
base64_url_encode(
CHANGING
iv_base64 = lv_jwt
).

rv_signed_jwt_base64 = lv_jwt.
ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZCL_GCP_API_HANDLER=>DO_API_REQUEST
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_DESTINATION TYPE C
* | [--->] IV_OIDC_TOKEN TYPE STRING
* | [--->] IV_METHOD TYPE STRING
* | [--->] IV_XCONTENT TYPE XSTRING(optional)
* | [--->] IV_CONTENT TYPE STRING(optional)
* | [--->] IV_SUB_URI TYPE STRING(optional)
* | [--->] IT_HEADER_FIELDS TYPE TIHTTPNVP(optional)
* | [--->] IT_COOKIES TYPE TIHTTPCKI(optional)
* | [--->] IV_CONTENT_TYPE TYPE STRING (default ='application/json')
* | [<-()] RS_RESPONSE TYPE ZGCP_RESPONSE
* | [!CX!] ZCX_GCP_API_HANDLER
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD do_api_request.
DATA lo_client_api TYPE REF TO if_http_client.
DATA lv_response TYPE string.
DATA lv_oidc TYPE string.

CALL METHOD cl_http_client=>create_by_destination
EXPORTING
destination = iv_destination
IMPORTING
client = lo_client_api
EXCEPTIONS
argument_not_found = 1
destination_not_found = 2
destination_no_authority = 3
plugin_not_active = 4
internal_error = 5
OTHERS = 6.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_api_dest_not_found.
ENDIF.

IF lo_client_api IS BOUND.
lv_oidc = |Bearer { iv_oidc_token }|.

lo_client_api->request->set_header_fields( fields = it_header_fields ).

lo_client_api->request->set_content_type( content_type = iv_content_type ).
lo_client_api->request->set_method( method = iv_method ).

* set jwt token auth
lo_client_api->request->set_header_field(
name = 'Authorization' ##NO_TEXT
value = lv_oidc
).
lo_client_api->request->set_header_field(
name = 'content-type'
value = iv_content_type
).


IF iv_sub_uri IS NOT INITIAL.
cl_http_utility=>set_request_uri(
request = lo_client_api->request
uri = iv_sub_uri
).
ENDIF.

IF iv_xcontent IS NOT INITIAL.
lo_client_api->request->set_data( data = iv_xcontent ).
ENDIF.

IF iv_content IS NOT INITIAL.
lo_client_api->request->set_cdata( data = iv_content ).
ENDIF.

LOOP AT it_cookies ASSIGNING FIELD-SYMBOL(<cookie>).
lo_client_api->request->set_cookie(
EXPORTING
name = <cookie>-name " Name of cookie
path = <cookie>-path " Path of Cookie
value = <cookie>-value " Cookie value
domain = <cookie>-xdomain " Domain Name of Cookie
expires = <cookie>-expires " Cookie expiry date
secure = <cookie>-secure " 0: unsaved; 1:saved
).
ENDLOOP.

lo_client_api->send( ).
lo_client_api->receive(
EXCEPTIONS
http_communication_failure = 1
http_invalid_state = 2
http_processing_failed = 3
).
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_api_receive_failed.
ENDIF.

rs_response-content = lo_client_api->response->get_data( ).
lo_client_api->response->get_status( IMPORTING code = rs_response-code
reason = rs_response-reason ).
lo_client_api->response->get_cookies( CHANGING cookies = rs_response-cookies ).
ENDIF.
ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZCL_GCP_API_HANDLER=>EXCHANGE_JWT_WITH_OIDC_TOKEN
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_EXCHANGE_DESTINATION TYPE C
* | [--->] IV_JWT_TOKEN TYPE STRING
* | [<-()] RV_OIDC_BASE64 TYPE STRING
* | [!CX!] ZCX_GCP_API_HANDLER
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD exchange_jwt_with_oidc_token.
DATA lo_client TYPE REF TO if_http_client.
DATA ls_response TYPE oidc_token_json.

CALL METHOD cl_http_client=>create_by_destination
EXPORTING
destination = iv_exchange_destination
IMPORTING
client = lo_client
EXCEPTIONS
argument_not_found = 1
destination_not_found = 2
destination_no_authority = 3
plugin_not_active = 4
internal_error = 5
OTHERS = 6.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_oauth_dest_not_found.
ENDIF.

IF lo_client IS BOUND.
lo_client->request->set_method( if_http_request=>co_request_method_post ).
lo_client->request->set_formfield_encoding( formfield_encoding = if_http_entity=>co_formfield_encoding_encoded ).

lo_client->request->set_form_field(
EXPORTING
name = 'grant_type'
value = 'urn:ietf:params:oauth:grant-type:jwt-bearer'
).

lo_client->request->set_form_field(
EXPORTING
name = 'assertion'
value = iv_jwt_token
).

lo_client->send( ).
lo_client->receive(
EXCEPTIONS
http_communication_failure = 1
http_invalid_state = 2
http_processing_failed = 3
).
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_oauth_token_receive_fail.
ENDIF.

DATA(lv_response_json) = lo_client->response->get_cdata( ).

/ui2/cl_json=>deserialize(
EXPORTING
json = lv_response_json
pretty_name = /ui2/cl_json=>pretty_mode-camel_case
CHANGING data = ls_response ).

IF ls_response-id_token IS INITIAL.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_oauth_token_receive_fail.
ENDIF.

rv_oidc_base64 = ls_response-id_token.
ENDIF.
ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZCL_GCP_API_HANDLER=>GET_IAT_UNIXTIME
* +-------------------------------------------------------------------------------------------------+
* | [<-()] RV_IAT TYPE INT4
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD get_iat_unixtime.
DATA lv_unix_iat TYPE string.

GET TIME STAMP FIELD DATA(lv_timestamp).

CONVERT TIME STAMP lv_timestamp TIME ZONE 'UTC' INTO DATE DATA(lv_date) TIME DATA(lv_time).

cl_pco_utility=>convert_abap_timestamp_to_java(
EXPORTING
iv_date = lv_date
iv_time = lv_time
iv_msec = 0
IMPORTING
ev_timestamp = lv_unix_iat
).

rv_iat = substring( val = lv_unix_iat off = 0 len = strlen( lv_unix_iat ) - 3 ).
ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Private Method ZCL_GCP_API_HANDLER=>STRING_TO_BINARY_TAB
* +-------------------------------------------------------------------------------------------------+
* | [--->] IV_STRING TYPE STRING
* | [<-()] RT_BIN_TAB TYPE LTTY_TSSFBIN
* | [!CX!] ZCX_GCP_API_HANDLER
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD string_to_binary_tab.
DATA lv_xstring TYPE xstring.
CALL FUNCTION 'SCMS_STRING_TO_XSTRING'
EXPORTING
text = iv_string
encoding = '4110'
IMPORTING
buffer = lv_xstring
EXCEPTIONS
failed = 1
OTHERS = 2.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_gcp_api_handler
EXPORTING
textid = zcx_gcp_api_handler=>zcx_strtobin_conversion_failed.
ENDIF.

CALL FUNCTION 'SCMS_XSTRING_TO_BINARY'
EXPORTING
buffer = lv_xstring
TABLES
binary_tab = rt_bin_tab.
ENDMETHOD.
ENDCLASS.

 
40 Comments