Skip to Content
Technical Articles
Author's profile photo Martin Pankraz

BTP private linky swear with Azure – propagate your SAP principels via Private Link Service

This post is part 5 of a series sharing service implementation experience and possible applications. Find the introduction in part 1 of the series. Part 2 discusses the integration with Integration Suite. Part3 sheds light on the different deployment modes given by your SAP architecture. Part4 focusses on how to debug and test your private-link-enabled app. Part 6 describes how to restrict access to your SAP backend endpoints to keep your auditor happy. Part 7 discusses how to setup end-to-end SSL with the new host name feature.

Find the associated SAP on Azure YouTube session here and the GitHub repos here.

Dear community,

Continuing with the implementation journey of BTP Private Link Service (PLS) we will have a closer look at the famous SAP Principal Propagation. The term describes the mapping of your BTP authenticated users to your SAP backend users to ensure audit trail and your complex server-side authorization mechanisms are applied. Your users on BTP or respectively CloudFoundry are identified by their email-address whereas SAP backend users usually have other ids.

My colleague Martin Raepple runs a comprehensive series on the topic. Check it out for more details on the general Principal Propagation setup.

Fig.1 Pinkie with principals

The mapping process usually relies on the OAuth2SAMLBearerAssertion flow. The Destination service enables you to configure the properties that the BTP Connectivity service needs to request the tokens to authenticate your request. Remember that the BTP private link service requires a service binding to allow traffic to flow. That means we are stuck here, because the connectivity service cannot traverse our private link to contact our SAP backend – more specifically the SAP OAuth server hosted on the backend.

Okay, byeee! Just kidding 😉

The services are isolated for security reasons by default. We will see if SAP introduces a secure way to open the connectivity service towards private link service going forward. For now, we need to break up our assertion flow into two calls and have our app orchestrate the requests, so that we can reach through the private link.

Fig.2 SAP Principal Propagation flow overview

Communication flows from right to left and the arrows point towards the service being called. The process starts with the application requesting authorization for the app you are calling on BTP (on Cloud Foundry the XSUAA guards the “door”). This is done through an app router. The app router is configured to forward its JWT token to the target application. In our case that is Java servlet being used throughout the blog series.

Fig.3 Flow diagram illustrating the sequence of calls for the required token exchange

With that JWT token we can contact XSUAA through the Destination service again to exchange our JWT for a SAML assertion. Using the SAML assertion we can finally call the SAP OAuth server to request a Bearer token and conclude the mapping from to i12345.

I provided a Postman collection with all the configurations for an easier start. You simply need to fill the variables according to your environment.

Fig.4 Variables on Postman Collection to test token exchange

Let’s look at the moving parts

The approuter provides the JWT token from the XSUAA to the Java servlet as a typical Authorization http header:

String UserTokenFromAppRouter = request.getHeader("Authorization").split("Bearer ")[1];

That is our user token referring to Next, we need the credentials to call the Destination service and the XSUAA. It is a common practice to read them from the VCAP_SERVICES environment variable on BTP. See below the generic method getCredentialsFor that abstracts the two calls.

private JSONObject getCredentialsFor(String xsuaaOrDestination){
        JSONObject jsonObj = new JSONObject(System.getenv("VCAP_SERVICES"));
        JSONArray jsonArr = jsonObj.getJSONArray(xsuaaOrDestination);
        return jsonArr.getJSONObject(0).getJSONObject("credentials");

private String getSAMLForXSUAA(String userJWTToken){

        JSONObject destinationCredentials = getCredentialsFor("destination");
        // get value of "clientid" and "clientsecret" from the environment variables
        String clientid = destinationCredentials.getString("clientid");
        String clientsecret = destinationCredentials.getString("clientsecret");
        // get the URL to xsuaa from the environment variables
        JSONObject xsuaaCredentials = getCredentialsFor("xsuaa");
        String xsuaaJWTToken = "";
        URI xsuaaUri = null;
        try {
            xsuaaUri = new URI(xsuaaCredentials.getString("url"));
            // use the XSUAA client library to ease the implementation of the user token exchange flow
            XsuaaTokenFlows tokenFlows = new XsuaaTokenFlows(new DefaultOAuth2TokenService(), new 
            XsuaaDefaultEndpoints(xsuaaUri.toString()), new ClientCredentials(clientid, clientsecret));
            xsuaaJWTToken = tokenFlows.clientCredentialsTokenFlow().execute().getAccessToken();

Given that we have the two tokens that we need to trigger the SAMLAssertion flow.


HttpsURLConnection con = (HttpsURLConnection)url.openConnection();
con.setRequestProperty("Authorization", "Bearer " + xsuaaJWTToken);
con.setRequestProperty("X-user-token", userJWTToken);
status = con.getResponseCode();

So far so good. Let’s check the config of that destination.

Note: that I configured TrustAll with false for end-to-end SSL and a custom trust store to prove the target architecture. Find more details on the SSL setup on part 7 of the series.

If you don’t want proper SSL to start with, you can get away with TrustAll:true and the default JDK truststore.

Fig.5 Destination config for SAMLAssertion

You need to manually add the property tokenServiceURL. Be aware there is currently a UI glitch that allows only “mouse” driven navigation in that box. The url needs to point to the oauth2 token endpoint on your SAP backend including the SAP client number (in my case In addition to that it needs to use the IP or respectively the host name, that is resolvable by the PLS. Till private DNS becomes active for PLS I am using to mimic the same behavior.

Find your Audience config via the SAP backend transaction SAML2. It opens the webdynpro sap/bc/webdynpro/sap/saml2?sap-client=XXX.

Fig.6 Find provider name on SAML2 transaction

So, now we can call the same endpoint for the SAML2-Bearer token exchange. I use a different destination for that purpose because you need to maintain BasicAuth for the OAuth2 Client. Find your OAuth2 Client info via the SAP backend transaction SOAUTH2. It opens the webdynpro /sap/bc/webdynpro/sap/oauth2_config?sap-client=XXX.

Fig.7 Find OAuth2 client and trust info

You can get the password via transaction SU01 for instance:

Fig.8 Find OAuth2 user credential on transaction SU01

Fig.9 Destination config for saml2-bearer exchange

private String getBearerFromSAP(String assertion){
        DefaultHttpClientFactory customFactory = new DefaultHttpClientFactory();
        final HttpDestination oauthDestination = 
        HttpClient httpClient = customFactory.createHttpClient(oauthDestination);

        HttpPost myRequest = new HttpPost("/sap/bc/sec/oauth2/token");
        HashMap<String, String> formValues = new HashMap<String, String>();
        formValues.put("grant_type", "urn:ietf:params:oauth:grant-type:saml2-bearer");
        BasicCredentials props = oauthDestination.getBasicCredentials().get();
        formValues.put("client_id", props.getUsername());
        formValues.put("scope", "ZEPM_REF_APPS_PROD_MAN_SRV_0001");
        formValues.put("assertion", assertion);

My OData service requires the scope ZEPM_REF_APPS_PROD_MAN_SRV_0001 (check fig. 7 for the assignment). You can check on transaction /IWFND/MAINT_SERVICE if your service is OAuth enabled. See Martin Raepple’s post series or SAP docs for more info.

The response gives us the final bearer token issues by the SAP OAuth server to proceed with our OData calls.

String finalBearerToken = getBearerFromSAP(exchangedTokenForSAMLBearer);
        //"SAP Bearer token :::" + finalBearerToken);
        DefaultHttpClientFactory customFactory = new DefaultHttpClientFactory();
        final HttpDestination destination = 

        //"Destination for OAUTH loaded:"+destination.getAuthenticationType().toString());
        HttpClient httpClient = customFactory.createHttpClient(destination);
        HttpUriRequest myRequest = new HttpGet(url);
        myRequest.setHeader("Content-Type", request.getContentType());
        //Inject token from SAMLAssertion flow
        myRequest.setHeader("Authorization", "Bearer " + finalBearerToken);

        final HttpResponse productResponse = httpClient.execute(myRequest);

For the final call to my OData service, I use another destination called “s4NoAuth” to indicate that the authorization header is set by the Java servlet dynamically and not by the destination. However, the URL and trust config are identical to “s4BasicAuth”. I believe this is a cleaner approach. Of course, you could spare this extra destination and override the authorization header provided by destination “s4BasicAuth”.

Fig.10 Destination config overview

If everything was configured correctly and our user is authorized to see the data, we get the desired output again honoring Principal Propagation 😊

Fig.11 Screenshot from sec_diag_tool trace for OAuth user CHATBOT

Fig.12 Screenshot of OData result from Java Servlet after Principal Propagation

I can warmly recommend transaction sec_diag_tool to troubleshoot any Principal Propagation errors. It saved me quite some time.

Further Reading and SAP Docs references

Final Words

Uhh, that was quite the ride. We saw why SAP Principal Propagation is important and how to overcome the security boundary between the BTP connectivity and private link service. We achieved that by breaking about the call into two requests. One using SAMLAssertion flow and another one using saml2-bearer grant type to exchange the SAML token for a bearer token. The post concludes with the saml trace proving the user mapping.

In case SAP decides to connect the BTP Connectivity service with the private link going forward we would also be able to consolidate the calls and apply the typical OAuth2SAMLBearerAssertion flow.

Any further inputs from you @Developers?

In part 6 I will talk about how to restrict access to your SAP exposed endpoints via PLS with focus on OData and RFCs.

@Kudos to Martin Raepple, Tsvetan, Manol and Piotr for nudging me in the right direction for the SAMLAssertion config 😊

Find the related GitHub repos here.

As always feel free to ask lots of follow-up questions.


Best Regards


Assigned Tags

      Be the first to leave a comment
      You must be Logged on to comment or reply to a post.