Monday 10 February 2020

Create REST APIs secured with XSUAA in Cloud Foundry environment and call REST APIs in ABAP environment

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.

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.

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications, SAP ABAP Prep

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications, SAP ABAP Prep

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.

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications, SAP ABAP Prep

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 (.*).

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications, SAP ABAP Prep

◈ 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).

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications, SAP ABAP Prep

In order to access our business app, we need to use approuter route URL (as in below screenshot).

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications, SAP ABAP Prep

In user authentication screen, input user email and password to logon, then our application can be assessed successfully.

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications, SAP ABAP Prep

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 <XSUAA service URL>/oauth/token. We can find <XSUAA service URL> 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.

SAP ABAP Tutorial and Material, SAP ABAP Guides, SAP ABAP Learning, SAP ABAP Certifications, SAP ABAP Prep

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

No comments:

Post a Comment