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: 
_Dimitri_
Employee
Employee

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
daniel_humberg
Contributor
Very cool. Build bridges!
0 Kudos

Hi Dmitri ,

Hope you are doing well .

I have exactly similar requirement where i need to setup service account for google sheets.

I have done the steps exactly similar to you . Uploaded the P12 file in STRUST and i am getting a return code 30 from SSF_KRN_SIGN .

I am attaching the system log for SSF_KRN_SIGN from SM21 . Please help . It would be much appreciated .

 

Thanking you in anticipation

Akshay

 

 

 

_Dimitri_
Employee
Employee
Hi Akshay,

 

it is hard to say without knowing the code and how the P12 file was imported, but it seems the signer table is not filled correctly.
 lt_signer = VALUE #( ( id = iv_ssf_id profile = iv_ssf_profilename result = iv_ssf_result ) ).

here you have to set the correct values id should be '<implicit>', for ssf result value 28 should be used. profile should be ssf profilename, in my case was 'SAPJWT_SI001.pse', where the P12 file was uploaded.
0 Kudos

Hi Dimitri ,

 

Thanks a lot for your quick response , I am much obliged for your help . But i think i will need to bother you again.

yes , I have created the signer as you suggested . attaching the screenshots for your reference . Changed the profile name as per my system..

 

I have been able to generate the JWT signature with the following changes  in the function call but the api call returns me the error of INVALID JWT signature.



CALL FUNCTION ‘SSF_KRN_SIGN’
EXPORTING
str_format                   ‘PKCS1’    ( if i give the value as u suggested it dont even generate a output BIN file , even if i comment out this paramter , Signature  gets generated but fails , default is ‘PKCS7’ )
b_inc_certs                  abap_false
b_detached                   abap_false
*        b_inenc                      = abap_false  (If i make it as abap_false , it it dont even generate a output BIN file )
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 sysubrc <> 0.

ENDIF.

Hereby attaching the screenshots of the entry in table

 

 

Do we need to make some changes in this function call?.. P12 file is imported successfully . its was imported and replaced the existing PSE file .

 

Kindly help again .

 

Thanks

Akshay

_Dimitri_
Employee
Employee
0 Kudos
Hi,

 

so you generate succefully a signed jwt? Do you exchange JWT for OIDC and this fails?
0 Kudos
Hi Dmitri ,

 

Yes , i am able to generate a signed JWT with the changes in the FM call as mentioned above. But exchange fails as the response says : INVALID JWT signature.

 

If i call the FM as u suggested , response code is 30 :

 



 

Thanks

Akshay
_Dimitri_
Employee
Employee
0 Kudos
many things could be wrong:

  1. do you encode the signature base64 before making the request to oidc exchange as described in the blog?

  2. is your issuer and target audience set up correctly when filling the jwt properties? issuer is the service account, target audience is the oauth client id in gcp.

  3. does your service account have the right permissions to be the issuer (user of IAP secured webapps)?


i dont think the problem is with the signature itself, but i could be wrong since you are using different input parameters for function module.
0 Kudos
Hi Dimitri ,

 

Thanks a lot for your Patience.

I more or less copied your piece of code only , so yes , I encoded the signature .

target audience and issuer are setup correctly . i cross checked it .

Just to highlight one point here , i am not exchanging it for OIDC , i am exchanging it for OAuth token , as i didn't set up the IAP  .  I want it to exchange directly to OAuth and use it .

 

Thanks

Akshay
former_member679314
Discoverer
0 Kudos
Hi,

Very helpful, but I'm facing a problem when importing the P12 file generated by Google API Console (I need to integrate Google calendar API). I follow your instructions as you wrote:

"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. "

SAP asks me for the password of the p12 file, which i submit, but then appears TRUST067 message which says it can not open the PSE file.

Any suggestion?

 

Many thanks!

Jordi
_Dimitri_
Employee
Employee
0 Kudos
Hi,

 

i never faced that issue but the password you provide must be wrong. Maybe you had a typo or saved the wrong password when downloading the P12 file?

 

Best Regards
former_member679314
Discoverer
0 Kudos
Hi Dimitri,

thanks for responding.

I'm sure I'm writing the password correctly, even I tried to trnasform the p12 from google to pem files and then back to p12 with no password and the same error (Could not open PSE).

Maybe p12 from google is diferent now and is not recognized by SAP? Or maybe we miss something in our SAP system?

 

Again, many thanks.

 
_Dimitri_
Employee
Employee
0 Kudos

Hi Jordi,

 

it works with NW 7.51, and no need to convert the file to pem. Seems to be autorization problem, found this one: https://answers.sap.com/questions/2571694/trust-manager-error-trust067.html

please check SU53 for missing auth.

 

Best regards

former_member679314
Discoverer
Hi Dimitri,

I've solved our problem by using OS command sapgenpse with import_p12 option.

It created a PSE file that could be loaded with no further problems. Now we can sign our JWT as you described.

Many many and again, many thanks for your support.

Best regards,

Jordi
martinkoch
Active Participant
0 Kudos
Thanks for the great blog!

I have the issue of integrating an api via JWT that only offers me a RSA Private Key + Public Key instead of a pkcs12 file.

Do you have a tip on how to handle this?

Thanks!

 

BR

Martin
_Dimitri_
Employee
Employee
0 Kudos
Hi, sorry for the late response, i have never tried this but i would suggest to import the private key of service account from the json key file into you AS ABAP system and use it then when creating the JWT instead of what is described in the blog with RS256 signature?
0 Kudos
Hello Dimitri,

Thanks for sharing the above info. I see that you have used P12 Service Key. Could you please also share how we can use the JSON key from SAP.  Since, P12 is not recommended approach. Awaiting for your response. Thank you.

Regards,

Santosh

 

 
_Dimitri_
Employee
Employee
0 Kudos
Hi Santosh,

 

this is not possible on ABAP AS side, since STRUST does not support json.

 

Best Regards

 

Dimitri
michaelgottfrie
Explorer
Hi Dimitri,

really good blog! I was able to adapt it to call the Google Analytics API with a Google service account. Works perfect!

Regards,
Michael
frank_berwanger
Explorer
0 Kudos
Hi Dimitri

I really like your work here.

I stumbled over it in my search for a way to check the signature of a JWT (RS256). Since RS256 uses a public/private key infrastructure, I haven't been able to find a way in SAP ABAP to check the signature because SAP offers only SHA256 for symmetric keys. And of course I don't know the private key of the authentication issuer.

Do you have any tip for me, how to achieve this?

Best regards,

Frank.
_Dimitri_
Employee
Employee
0 Kudos

Hi Frank,

i do not know if that is even possible without knowing the private key. You could check Package SECF_BCE. There are some FM that may help like SSFI_VERIFY_SIGNATURE. Also package SECF could be interesting. Hope that helps.

 

Best Regards

 

Dimitri

frank_berwanger
Explorer
Hi Dimitri

of course this is possible;-) You can check the signature with the public key. This is the beauty of asymmetrical algorithms. But it seems that SAP ABAP has only symmetrical methods like HMAC.

I will open a message with SAP in the hope that they have a proposition. If I have a solution I will post it here.
Former Member
0 Kudos
frank.berwanger I would be highly interested in the outcome. Have you already messaged SAP?
frank_berwanger
Explorer
0 Kudos
Yes, I have. Unfortunately they couldn't find a way to do it with the ABAP Stack:-(
S0021341264
Newcomer
0 Kudos
Hi Dimitri, excellent blog.

We currently have a project where I need to send a response to a GCP Pub / Sub from SAP Abap,

I would like to know if this would help me to make the connection, or if you have information on how to do it, or what would be the difference if it exists.

 
Very grateful for your answer
_Dimitri_
Employee
Employee

Hi Maria, yes this is possible, we use this approach also for Pub/Sub API communication. The difference is, you will work with Google PubSub API and for that the JWT payload need to have scope instead of target audience.

 

 DATA(ls_jwt_payload_scope) = VALUE zgcp_jwt_payload_scope( 

                                                                    iss = 'service account email that has the pub sub api rights and was imported into strust for signature'

                                                                    aud = 'https://www.googleapis.com/oauth2/v4/token'

                                                                    iat = lv_iat

                                                                    exp = lv_iat + 3600

                                                                    scope = 'https://www.googleapis.com/auth/pubsub').

Sending this to auth endpoint will grant you a token that you can use for Google PubSub API requests.

simonharrer
Discoverer
0 Kudos
Hi Dimitri,

thank you for this great blog post. Your solution looks really nice, but requires a lot of manually crafted ABAP code. Do you know whether there is a way to get this behavior through standard SAP logic instead? Like configuring an OAuth2 client for a HTTP destination? I had a look and it seems this isn't possible, but you might have more insights.

Best

Simon
_Dimitri_
Employee
Employee

Hi Simon, sadly this is not possible and this is why we had to come up with an own solution 🙂

simonharrer
Discoverer
0 Kudos
Hi Dimitri,

thanks for the quick response! We feared so as well.

We are currently trying to adopt your code, and we are missing the structure "ZGCP_RESPONSE" and the exception "ZCX_GCP_API_HANDLER". Would you mind sharing those here as well? Currently, we are reverse engineering those missing things, but sharing it here would help others as well.

Best,

Simon
_Dimitri_
Employee
Employee
0 Kudos


ZGCP_RESPONSE


 

ZCX_GCP_API_HANDLER is just our custom exception class. You can use CX_STATIC_CHECK instead as this is the superclass of our custom exception class.
0 Kudos
Hi Dimitri,

Thanks for detailed blog. In our case we want to call an API given by our middleware team (web methods) and from there Google API will be called. We have a url to call given by integration team, however we need to take access token before calling that url. We also have certificate and private key. se tried approach given above however we are not getting a valid token.

Could you please guide which place we should that certificate and which steps will not be applicable for us?

Regards,

SS
0 Kudos
Hi Dimitri,

Very useful blog. I’m following every step. Just after call function module and try to convert the binary to string. I received a string not readable, which contains #. Do you know what can be the issue?
When debug into this function, it is calling a C function for sign which is not visible to us what is doing.
SSF_KRN_SIGN
Hello Dimitri,

I am trying to do a GET on the Token passing the parameters and after getting the TOKEN would do a PUT on a service that DocuSign.

But when trying to run the class I am running into the following error (5).


 

ERROR


I made the same POSTMAN call successfully

 

Do you have any tips to give me on what is going on?

 

Thank you in advance
quyen_sap_viet
Explorer
0 Kudos
I got the same CRC code. how to resolve it?
former_member822090
Discoverer
0 Kudos
Beautiful Blog.
aelgendi
Explorer
0 Kudos
Hi Dimitri,

Thanks for detailed blog.

In your case you uploaded a private key using strust. In my case the private key is on token ( usb card ) and i can't extract the private key from it. what should i do ?
quyen_tran
Explorer
0 Kudos
THanks for sharing. it's too helpful.
fneto
Discoverer
0 Kudos
Dimitri,
congratulations for the blog.

Help me if possible: When am I going to use the KEY.PEM file? . I already imported CERT.PEM in STRUST. The authentication platform is asking for username and password. And this is in the KEY.PEM file.
fneto
Discoverer
0 Kudos
Here's an example from KEY.PEM:

 

-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDTTEBBnomvLQDl
........................
........................
........................
-----END PRIVATE KEY-----
_Dimitri_
Employee
Employee
0 Kudos
you can generate a new key in GCP on service account screen and can import that
0 Kudos

Hi
I have same Kind of requirment when i trying reuest for JWT Token Response

Any one please help me