Introduction

Presented below are different approaches to call OAuth protected resources from ABAP on-premise (I am using ABAP 1909 Developer edition) using the client credentials. This OAuth flow does not involve user interaction, as it has place for example in the authorization code flow.

Sample Backend API + Auth0 as an OAuth server

To have a working sample I am using Auth0 free features for registering an OAuth protected backend, which I will call from ABAP. Using their Quick Samples I also prepared a dumb, but working API which will be protected and requires a valid JWT we obtain from Auth0 (OAuth server). The API is deployed to Vercel and it is quite simple - if we are authorized, a call to /authorized path should return Secured Resource text.

var express = require("express");
var app = express();
var { expressjwt: jwt } = require("express-jwt");
var jwks = require("jwks-rsa");

var port = process.env.PORT || 8080;

var jwtCheck = jwt({
  secret: jwks.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: "https://dev-ycxqrx2b.us.auth0.com/.well-known/jwks.json",
  }),
  audience: "https://samples-oauth-cc-backend.vercel.app",
  issuer: "https://dev-ycxqrx2b.us.auth0.com/",
  algorithms: ["RS256"],
});

app.use(jwtCheck);

app.get("/authorized", function (req, res) {
  res.send("Secured Resource");
});

app.listen(port);

The API is deployed under https://samples-oauth-cc-backend.vercel.app. Accessing without an access token is giving correctly "Unauthorized" response:

Now I can add it to Auth0. In the API tab, I created a new API.

In the Machine To Machine Applications tab there is an entry pointing to an app created for the API. In this app I can get the details like client id & client secret.

The token URL for my app is dev-ycxqrx2b.us.auth0.com/oauth/token. All endpoints can be seen in the app settings - scroll down to the Advanced section, then the tab Endpoints.

That is, now Auth0 acts as an OAuth server for my API and I can call the token endpoint to obtain an access token.

ABAP code

  • I am using ABAP Developer edition 1909
  • for simplicity exception handling in the code is very basic
  • please remember to import the SSL certificate to STRUST for OAuth0 site - head to the token URL in your browser, then download the certs & import. Check the abapGit docs for instructions

Hint: you can use jwt.io to decode the obtained access token, which in my case looks like:

Approach 1: all in code

This approach is based 100% on code, which might be good for quick dev & check purposes, but not for production due to hardcoded credentials. The first step is to obtain the access token, which next is attached to the API call (as a Bearer header).

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

    TYPES:
      BEGIN OF access_token,
        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 access_token.

  PRIVATE SECTION.
    CLASS-DATA:
      out TYPE REF TO if_oo_adt_classrun_out.
    CLASS-METHODS:
      get_access_token RETURNING VALUE(result) TYPE access_token,
      call_backend IMPORTING access_token TYPE access_token.
ENDCLASS.

CLASS zcl_oauth_direct IMPLEMENTATION.
  METHOD if_oo_adt_classrun~main.
    zcl_oauth_direct=>out = out.

    " Step 1: get access token
    DATA(access_token) = get_access_token( ).

    " Step 2: use it to call the protected resource
    call_backend( access_token ).

  ENDMETHOD.

  METHOD get_access_token.
    cl_http_client=>create_by_url(
     EXPORTING
       url                = 'https://dev-ycxqrx2b.us.auth0.com/oauth/token'
       ssl_id             = 'ANONYM'
     IMPORTING
       client             = DATA(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.
      out->write( |http_client create error { sy-subrc }| ).
      RETURN.
    ENDIF.

    http_client->request->set_header_field(
      name  = '~request_method'
      value = 'POST' ).

    http_client->request->set_header_field(
      name  = 'Content-Type'
      value = 'application/json' ).

    http_client->request->set_header_field(
      name  = 'Accept'
      value = 'application/json' ).

    DATA(request_body) = `{
      "audience": "https://samples-oauth-cc-backend.vercel.app", "grant_type": "client_credentials" }`.

    http_client->request->set_cdata(
      data   = request_body
      offset = 0
      length = strlen( request_body ) ).

    http_client->request->set_authorization(
      auth_type = ihttp_auth_type_basic_auth
      username = 'xi9m2QU7wfGIsEEvyGBchhM066QBj59y'
      password = 'K0Cu1................SPi' ).

    http_client->send(
     EXCEPTIONS
      http_communication_failure = 1
      http_invalid_state         = 2 ).

    IF sy-subrc <> 0.
      out->write( |http_client send error { sy-subrc }| ).
      http_client->get_last_error( IMPORTING message = DATA(message) ).
      out->write( message ).
      RETURN.
    ENDIF.

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

    IF sy-subrc <> 0.
      out->write( |http_client receive error { sy-subrc }| ).
      http_client->get_last_error( IMPORTING message = message ).
      out->write( message ).
      RETURN.
    ENDIF.

    DATA(http_status) = http_client->response->get_header_field( '~status_code' ).
    DATA(json_response) = http_client->response->get_cdata( ).
    out->write( http_status ).

    cl_fdt_json=>json_to_data(
      EXPORTING iv_json = json_response
      CHANGING ca_data = result ).
  ENDMETHOD.


  METHOD call_backend.
    cl_http_client=>create_by_url(
     EXPORTING
       url                = 'https://samples-oauth-cc-backend.vercel.app/authorized'
       ssl_id             = 'ANONYM'
     IMPORTING
       client             = DATA(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.
      out->write( |http_client create error { sy-subrc }| ).
      RETURN.
    ENDIF.

    " Use the token as Bearer in the authorization header
    CONCATENATE 'Bearer' access_token-access_token INTO DATA(bearer_token) SEPARATED BY space.

    http_client->request->set_header_field(
      name  = 'Authorization'
      value = bearer_token ).

    " Configure the rest of your call
    http_client->request->set_header_field(
      name  = '~request_method'
      value = 'GET' ).

    http_client->send(
     EXCEPTIONS
      http_communication_failure = 1
      http_invalid_state         = 2 ).

    IF sy-subrc <> 0.
      out->write( |http_client send error { sy-subrc }| ).
      http_client->get_last_error( IMPORTING message = DATA(message) ).
      out->write( message ).
      RETURN.
    ENDIF.

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

    IF sy-subrc <> 0.
      out->write( |http_client receive error { sy-subrc }| ).
      http_client->get_last_error( IMPORTING message = message ).
      out->write( message ).
      RETURN.
    ENDIF.

    DATA(response) = http_client->response->get_cdata( ).
    out->write( response ).

  ENDMETHOD.
ENDCLASS.

The response is "Secured Resource", as expected:

If I comment out the lines of code responsible for attaching the token to my backend call, then - also as expected - I am not authorized:

Approach 2: use destinations

A modified version uses destinations - both for OAuth token endpoint and the target backend calls.

In the OAuth token destination - the client id and client secret are stored using user/password fields; SSL should be enabled (anonymous):

The backend API destination is quite straightforward, just the host + SSL enabled:

The updated ABAP code - both methods now use the destinations:

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

    TYPES:
      BEGIN OF access_token,
        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 access_token.

  PRIVATE SECTION.
    CLASS-DATA:
      out TYPE REF TO if_oo_adt_classrun_out.
    CLASS-METHODS:
      get_access_token RETURNING VALUE(result) TYPE access_token,
      call_backend IMPORTING access_token TYPE access_token.
ENDCLASS.

CLASS zcl_oauth_destinations IMPLEMENTATION.
  METHOD if_oo_adt_classrun~main.
    zcl_oauth_destinations=>out = out.

    " Step 1: get access token
    DATA(access_token) = get_access_token( ).

    " Step 2: use it to call the protected resource
    call_backend( access_token ).

  ENDMETHOD.

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

    IF sy-subrc <> 0.
      out->write( |http_client create error { sy-subrc }| ).
      RETURN.
    ENDIF.

    http_client->request->set_header_field(
      name  = '~request_method'
      value = 'POST' ).

    http_client->request->set_header_field(
      name  = 'Content-Type'
      value = 'application/json' ).

    http_client->request->set_header_field(
      name  = 'Accept'
      value = 'application/json' ).

    DATA(request_body) = `{
      "audience": "https://samples-oauth-cc-backend.vercel.app", "grant_type": "client_credentials" }`.

    http_client->request->set_cdata(
      data   = request_body
      offset = 0
      length = strlen( request_body ) ).

    http_client->send(
     EXCEPTIONS
      http_communication_failure = 1
      http_invalid_state         = 2 ).

    IF sy-subrc <> 0.
      out->write( |http_client send error { sy-subrc }| ).
      http_client->get_last_error( IMPORTING message = DATA(message) ).
      out->write( message ).
      RETURN.
    ENDIF.

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

    IF sy-subrc <> 0.
      out->write( |http_client receive error { sy-subrc }| ).
      http_client->get_last_error( IMPORTING message = message ).
      out->write( message ).
      RETURN.
    ENDIF.

    DATA(http_status) = http_client->response->get_header_field( '~status_code' ).
    DATA(json_response) = http_client->response->get_cdata( ).

    cl_fdt_json=>json_to_data(
      EXPORTING iv_json = json_response
      CHANGING ca_data = result ).
  ENDMETHOD.


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

    IF sy-subrc <> 0.
      out->write( |http_client create error { sy-subrc }| ).
      RETURN.
    ENDIF.

    " Use the token as Bearer in the authorization header
    CONCATENATE 'Bearer' access_token-access_token INTO DATA(bearer_token) SEPARATED BY space.

    http_client->request->set_header_field(
      name  = 'Authorization'
      value = bearer_token ).

    " Configure the rest of your call
    http_client->request->set_header_field(
      name  = '~request_method'
      value = 'GET' ).

    http_client->send(
     EXCEPTIONS
      http_communication_failure = 1
      http_invalid_state         = 2 ).

    IF sy-subrc <> 0.
      out->write( |http_client send error { sy-subrc }| ).
      http_client->get_last_error( IMPORTING message = DATA(message) ).
      out->write( message ).
      RETURN.
    ENDIF.

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

    IF sy-subrc <> 0.
      out->write( |http_client receive error { sy-subrc }| ).
      http_client->get_last_error( IMPORTING message = message ).
      out->write( message ).
      RETURN.
    ENDIF.

    DATA(response) = http_client->response->get_cdata( ).
    out->write( response ).

  ENDMETHOD.
ENDCLASS.

Approach 3: use the OAuth client

In this approach I am using the OAuth client available through the transaction oa2c_config. This has some benefits - we can store all configs, credentials, flow type etc. in a central place. The ABAP http client class has also built-in support to use such configuration. You can read more about it here.

The first step is to create an OAuth profile. I don't see an option to do it via Eclipse/ADT, so in SE80 right click on the package, then in the Create menu choose the right option:

Choose DEFAULT as the type.

Now run the transaction oa2c_config. It opens a Wen Dynpro application. If you have not used it before, you might need to go to SICF transaction and enable required nodes (just follow the error messages).

In the app press the Create to create a new OAuth client using the created profile, we also need to provide the client id, in my case the one from Auth0 sample backend.

Before saving we need to fill the required fields - the client secret and authorization endpoints; then set the grant type as Client Credentials.

Note for Auth0 - it requires to provide the audience in the token body request, but we can't set it - neither in the oa2c_config app nor using enhancements - at least I don't see such option for the client_credentials flow. As a workaround in the Auth0 tenant settings I set the default audience to my fake API URL I created earlier in the APIs tab:

Now it is possible to use the client in ABAP. The first step is to create its instance using cl_oauth2_client, then request the token and pass it to the http client instance.

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

  PRIVATE SECTION.
    CLASS-DATA:
      out TYPE REF TO if_oo_adt_classrun_out.
    CLASS-METHODS:
      call_backend.
ENDCLASS.

CLASS zcl_oauth_client IMPLEMENTATION.
  METHOD if_oo_adt_classrun~main.
    zcl_oauth_client=>out = out.
    call_backend( ).
  ENDMETHOD.

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

    IF sy-subrc <> 0.
      out->write( |http_client create error { sy-subrc }| ).
      RETURN.
    ENDIF.

    " Use OAuth client to execute client credentials flow and enrich http client
    TRY.
        DATA(outh_client) = cl_oauth2_client=>create(
          i_profile = 'Z_AUTH0_CC_PROFILE'
          i_configuration = 'Z_AUTH0_CC_PROFILE' ).

        outh_client->set_token(
          io_http_client = http_client
          i_param_kind = if_oauth2_client=>c_param_kind_header_field ).

        " In case of token is not available or expired, run client_credentials flow
      CATCH cx_oa2c.
        TRY.
            outh_client->execute_cc_flow( ).

            outh_client->set_token(
              io_http_client = http_client
              i_param_kind = if_oauth2_client=>c_param_kind_header_field ).

          CATCH cx_root INTO DATA(exception).
            out->write( exception->get_text( ) ).
        ENDTRY.
    ENDTRY.

    " Configure the rest of your call
    http_client->request->set_header_field(
      name  = '~request_method'
      value = 'GET' ).

    http_client->send(
     EXCEPTIONS
      http_communication_failure = 1
      http_invalid_state         = 2 ).

    IF sy-subrc <> 0.
      out->write( |http_client send error { sy-subrc }| ).
      http_client->get_last_error( IMPORTING message = DATA(message) ).
      out->write( message ).
      RETURN.
    ENDIF.

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

    IF sy-subrc <> 0.
      out->write( |http_client receive error { sy-subrc }| ).
      http_client->get_last_error( IMPORTING message = message ).
      out->write( message ).
      RETURN.
    ENDIF.

    DATA(response) = http_client->response->get_cdata( ).
    out->write( response ).

  ENDMETHOD.
ENDCLASS.

As you see the code is shorter, as now the job of access token retrieval is handled by the cl_oauth2_client - no need to code it additionally.