ABAP programming is still the most powerful development tool in SAP core ERP, while we will find some cloud native features (ex. JWT relevant techniques) are not fully supported in ABAP environment in near future. If developers work on NetWeaver as well as Cloud Foundry environment, a workaround is to deploy REST APIs with cloud native functions in Cloud Foundry, and use ABAP programming to access the REST APIs.
Also Read: SAP ABAP 7.4 Certification Preparation Guide
In this blog post, I will introduce:
1. How to develop an XSUAA secured REST API (using Spring Boot) in Cloud Foundry environment.
2. How to use ABAP programming to call XSUAA secured REST API.
The images in this post are screenshots from real development systems.
Development Tools
Java SDK: 1.8.0_181
Maven: 3.5.4
NodeJS: v12.14.1
Cloud Foundry CLI: 6.49.0+d0dfa93bb.2020-01-07
Basic Concepts
Approuter is a NodeJS app which works as the single entrance to business apps. Technically business users always access a approuter, and approuter works as a reverse service to interact with underlying authentication service (XSUAA) and business apps.
XSUAA service is responsible for authentication and authorization. Both of approuter and business app need to be bound to a XSUAA instance.
JWT is provided by XSUAA service when client is authenticated as valid. This blog post will explain how client use token to access REST API deployed in Cloud Foundry environment.
Develop REST API in Cloud Foundry Environment
1. Create a XSUAA Service Instance
Logon global account and subaccount in Cloud Cockpit. In development space under subaccount, choose menu Service Marketplace, then create a Authorization & Trust Management instance.
XSUAA service is used for both authentication and authorization. This blog post only discusses how to apply XSUAA in securing REST API. Topics of authorization roles won’t be covered in this blog post. So here we create a simple XSUAA instance:
- Choose application as service plan.
- On Specify Parameters screen, we can configure scopes and role templates if more authorization requirements are needed. Here we simply leave it empty.
- On Assign Application screen, we leave it empty. Afterwards we will bound XSUAA to Spring Boot app in application deployment.
- Give a name to this instance. Here we name it as sapccp-bankscrtysrv-uaa.
2. Create and Deploy a Spring Boot Application
Create a Spring Boot application with below controller class. When end user accesses <App URL>/handlescrty, application will print ‘Handle security’ to client.
package com.sap.ccpbankscrtysrv.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BankSecurityController {
@GetMapping("/handlescrty")
public String handleScrty(){
return "Handle security";
}
}
Create below deployment configuration file manifest.xml in root folder of above application.
---
applications:
- name: sapccp-bankscrtysrv
routes:
- route: sapccp-bankscrtysrv-cpcanary01.cfapps.sap.hana.ondemand.com
memory: 800M
timeout: 300
#random-route: true
path: target/sapccpbankscrtysrv-0.0.1-SNAPSHOT.jar
#buildpacks:
# - sap_java_buildpack
env:
JBP_CONFIG_SPRING_AUTO_RECONFIGURATION: '{enabled: false}'
JAVA_OPTS: -Djava.security.egd=file:///dev/./urandom
services:
- sapccp-bankscrtysrv-uaa
- Route section specifies the application access URL.
- sapccp-bankscrtysrv-uaa in services section refers to the XSUAA instance we created previously. After this application is deployed in Cloud Foundry environment, the XSUAA instance will provide authentication & authorization services for our app.
Now we can build and push our application to Cloud Foundry environment.
mvn clean install
cf push
After this app is successfully deployed, we can see this app in Cloud Cockpit.
3. Create and Deploy a Approuter
Create a new file folder, and put a package.json file with below content in the folder.
{
"name": "sapccp-bankscrtysrv-approuter",
"dependencies": {
"@sap/approuter": "3.0.1"
},
"scripts": {
"start": "node node_modules/@sap/approuter/approuter.js"
},
"engines": {
"node": "8.16.1"
}
}
Run command npm i in this folder to install dependencies.
Create an xs.app.json file with below information. Destination dest-sapccp-bankscrtysrv will be referenced by approuter manifest.xml file.
{
"routes": [
{
"source": "/",
"target": "/",
"destination": "dest-sapccp-bankscrtysrv"
}
]
}
Create below manifest.xml file for approuter deployment.
---
applications:
- name: sapccp-bankscrtysrv-approuter
routes:
- route: sapccp-bankscrtysrv-approuter-<subdomain>.cfapps.sap.hana.ondemand.com
path: /
memory: 128M
buildpacks:
- nodejs_buildpack
env:
TENANT_HOST_PATTERN: 'sapccp-bankscrtysrv-approuter-(.*).cfapps.sap.hana.ondemand.com'
destinations: '[{"name":"dest-sapccp-bankscrtysrv", "url" :"https://sapccp-bankscrtysrv-<subdomain>.cfapps.sap.hana.ondemand.com", "forwardAuthToken": true}]'
services:
- sapccp-bankscrtysrv-uaa
- Route section states the approuter app access URL.
- Cloud Foundry environment uses subdomain (can be found on Subaccount Details as in below screenshot) to locate correct tenant, and TENANT_HOST_PATTERN tells approuter how to find subdomain information. We should always keep route section and TENANT_HOST_PATTERN section consistent, therefore subdomain can be extracted via regular expression (.*).
- destinations section specifies the target app shadowed by this approuter. In our case, the approuter routes client requests to app sapccp-bankscrtysrv. The destinations-name (dest-sapccp-bankscrtysrv) should be exactly the same as the destination in xs-app.json.
- The XSUAA instance sapccp-bankscrtysrv-uaa configured in services section will be bound to approuter in deployment.
Then we can run cf push in root folder, to deploy this approuter to Cloud Foundry environment.
4. Testing
Application sapccp-bankscrtysrv is now hidden behind approuter and protected by XSUAA service; it can’t be accessed directly. Clicking the route URL of this application (as in below screenshot) will trigger a 401 error (authorization issue).
In order to access our business app, we need to use approuter route URL (as in below screenshot).
In user authentication screen, input user email and password to logon, then our application can be assessed successfully.
Call REST API in ABAP Environment
This section demonstrates how to use ABAP programming to call REST API deployed in Cloud Foundry environment. Because our REST API is secured by XSUAA, we need to 1) Call XSUAA service to get access token, 2) Call REST API using the access token.
1. Get Access Token From XSUAA
TYPES:
BEGIN OF ty_token_json,
access_token TYPE string,
token_type TYPE string,
id_token TYPE string,
refresh_token TYPE string,
expires_in TYPE i,
scope TYPE string,
jti TYPE string,
END OF ty_token_json.
DATA lv_req_body TYPE string.
DATA lv_user TYPE string.
DATA lv_pwd TYPE string.
DATA lv_req_body_len_str TYPE string.
DATA lo_json_deserializer TYPE REF TO cl_trex_json_deserializer.
DATA ls_abap_response TYPE ty_token_json.
DATA lv_bearer_token TYPE string.
CALL METHOD cl_http_client=>create_by_url
EXPORTING
url = 'https://<subdomain>.authentication.sap.<region>.ondemand.com/oauth/token'
IMPORTING
client = DATA(lo_http_client)
EXCEPTIONS
argument_not_found = 1
plugin_not_active = 2
internal_error = 3
pse_not_found = 4
pse_not_distrib = 5
pse_errors = 6
OTHERS = 7.
IF sy-subrc <> 0.
* Implement suitable error handling here
RETURN.
ENDIF.
lo_http_client->propertytype_logon_popup = lo_http_client->co_disabled.
lv_user = '<client id>'.
lv_pwd = '<client secret>'.
CALL METHOD lo_http_client->authenticate
EXPORTING
username = lv_user
password = lv_pwd.
CALL METHOD lo_http_client->request->set_header_field
EXPORTING
name = '~request_method'
value = 'POST'.
CALL METHOD lo_http_client->request->set_header_field
EXPORTING
name = 'Content-Type'
value = 'application/x-www-form-urlencoded; charset=UTF-8'.
CALL METHOD lo_http_client->request->set_header_field
EXPORTING
name = 'Accept'
value = 'application/json'.
lv_req_body = |grant_type=password| &&
|&username=<email bound to subaccount>| &&
|&password=<password>| &&
|&client_id=<client id>| &&
|&client_secret=<client secret>| &&
|&response_type=token|.
DATA(lv_req_body_len) = strlen( lv_req_body ).
MOVE lv_req_body_len TO lv_req_body_len_str.
CALL METHOD lo_http_client->request->set_header_field
EXPORTING
name = 'Content-Length'
value = lv_req_body_len_str.
CALL METHOD lo_http_client->request->set_cdata
EXPORTING
data = lv_req_body
offset = 0
length = lv_req_body_len.
CALL METHOD lo_http_client->send
EXCEPTIONS
http_communication_failure = 1
http_invalid_state = 2.
CALL METHOD lo_http_client->receive
EXCEPTIONS
http_communication_failure = 1
http_invalid_state = 2
http_processing_failed = 3.
DATA(lv_http_status) = lo_http_client->response->get_header_field( '~status_code' ).
DATA(lv_json_response) = lo_http_client->response->get_cdata( ).
REPLACE '"access_token"' IN lv_json_response WITH 'access_token'.
REPLACE '"token_type"' IN lv_json_response WITH 'token_type'.
REPLACE '"id_token"' IN lv_json_response WITH 'id_token'.
REPLACE '"refresh_token"' IN lv_json_response WITH 'refresh_token'.
REPLACE '"expires_in"' IN lv_json_response WITH 'expires_in'.
REPLACE '"scope"' IN lv_json_response WITH 'scope'.
REPLACE '"jti"' IN lv_json_response WITH 'jti'.
CREATE OBJECT lo_json_deserializer.
lo_json_deserializer->deserialize(
EXPORTING
json = lv_json_response
IMPORTING
abap = ls_abap_response ).
- The URL passed to cl_http_client=>create_by_url is /oauth/token. We can find in sensitive data of XSUAA service (url highlighted as in below screenshot). There we can also find clientid and clientsecret, which are used to suppress authentication popup, and are filled in HTTP request body.
- Content-Type in HTTP header should be set as application/x–www–form–urlencoded, and the parameters in request body are structured as key-value string.
- After HTTP response is returned, cl_trex_json_deserializer-> deserialize is used to transform data format from json to ABAP structure (ty_token_json). We have to use REPLACE statement to remove double quotes from json field name, otherwise cl_trex_json_deserializer-> deserialize runs into dump. Finally we get access token (in ty_token_json- access_token ) for later use.
2. Call REST API using access token
CALL METHOD cl_http_client=>create_by_url
EXPORTING
url = 'https://sapccp-bankscrtysrv-<subdomain>.cfapps.sap.<region>.ondemand.com/handlescrty'
IMPORTING
client = lo_http_client
EXCEPTIONS
argument_not_found = 1
plugin_not_active = 2
internal_error = 3
pse_not_found = 4
pse_not_distrib = 5
pse_errors = 6
OTHERS = 7.
IF sy-subrc <> 0.
* Implement suitable error handling here
RETURN.
ENDIF.
lo_http_client->propertytype_logon_popup = lo_http_client->co_disabled.
lv_user = '<email bound to subaccount>'.
lv_pwd = '<password>'.
CALL METHOD lo_http_client->authenticate
EXPORTING
username = lv_user
password = lv_pwd.
CALL METHOD lo_http_client->request->set_header_field
EXPORTING
name = '~request_method'
value = 'GET'.
CALL METHOD lo_http_client->request->set_header_field
EXPORTING
name = 'Accept'
value = 'application/json'.
CONCATENATE 'Bearer' ls_abap_response-access_token INTO lv_bearer_token SEPARATED BY space.
CALL METHOD lo_http_client->request->set_header_field
EXPORTING
name = 'Authorization'
value = lv_bearer_token.
CALL METHOD lo_http_client->send
EXCEPTIONS
http_communication_failure = 1
http_invalid_state = 2.
CALL METHOD lo_http_client->receive
EXCEPTIONS
http_communication_failure = 1
http_invalid_state = 2
http_processing_failed = 3.
lv_http_status = lo_http_client->response->get_header_field( '~status_code' ).
lv_json_response = lo_http_client->response->get_cdata( ).
CALL METHOD lo_http_client->close( ).
The URL passed to cl_http_client=>create_by_url is the REST API URL. We should use REST API URL instead of approuter URL here, because HTTP client can’t process redirecting approuter to REST API.