OAuth client credentials flow from ABAP on-premise
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.
Links
- sample API - https://github.com/wozjac/samples-oauth-cc-backend
- ABAP code - https://github.com/wozjac/samples-oauth-cc-abap
- Auth0 - https://auth0.com