Service to service calls from SAP BTP to Microsoft Azure with BTP destinations

This brief is to describe how to leverage SAP BTP destination service to implement service to service calls from a SAP BTP hosted service (backend or frontend) against Microsoft Azure hosted resource (like MS Graph for instance), with or without a business user context.


  • Please note all the code snippets below are provided “as is”.
  • All the x509 certificates, bearer access and/or refresh tokens and the likes have been redacted.
  • Images/data in this blog post is from SAP internal sandbox, sample data, or demo systems. Any resemblance to real data is purely coincidental.
  • Access to online resources referenced in this blog may be subject to a contractual relationship with SAP and a S-user login may be required.

In many ways this brief is a sequel to my other blog, namely AzureAD as an OpenID Connect (OIDC) and OAuth provider | SAP Blogs, where I showcased a user delegated authentication flow  implemented in nodejs as a function with SAP BTP, Kyma runtime.

Let’s see how to make service to service calls from SAP BTP into Microsoft Azure with either named or technical user context implemented with destinations.

Terminology and security concepts refresher:

  • On Microsoft AzureAD Identity Platform a web application is an OIDC service provider. Likewise, Microsoft AzureAD Identity Platform enterprise application is a SAML SSO service provider.  Azure platform workloads are defined as Application services.
  • On SAP BTP platform a BTP sub-account is a service provider (either OIDC or SAML). Trusted with either the default SAP ID or any other Identity Provider (IDP) of your choice. SAP BTP workloads typically require a runtime environment: either provided by SAP: Cloud Foundry, Kyma/Kubernetes or any Other runtime of your choice. With of a hundred business services that BTP platform has to offer, there are many of them available across multiple runtime environments.

Methodology explainer:

  • To pass a named user context we will be leveraging the OBO authentication flow with Azure Quovadis-Web application.
  • For a technical user context we will be leveraging the client credentials authentication flow that is performed on behalf of a technical user represented by the receiving application client_id. With this flow the client secret is a technical user password. However, passwords may pose security risk thus we shall be using client assertions in lieu.

  • OBO flow is much alike the user delegated authentication flow [I have demonstrated in my previous blog], whereas client credentials flow is the application authentication flow. The following article may help understand the either flow.

  • In either case (OBO or client credentials) we shall be calling the Azure identity platform token issuance endpoint. The token issuance endpoint is protected with either a shared key or a client assertion of Quovadis-Web application
  • As aforementioned we shall be using OIDC client assertions in lieu of passwords. A client assertion is a JWT token signed with an x509 certificate known (uploaded) to the receiving Quovadis-Web application.

1. Service to service calls with a named user context

Microsoft identity platform OAuth 2.0 On-Behalf-Of flow , known as OBO,  allows exchanging a client application’s id_token that has a named user context against a bearer access_token of a resource.

OBO extends the urn:ietf:params:oauth:grant-type:jwt-bearer grant type and thus is very much alike the OAuth2JWTBearer authentication flow offered by the destination service.

The destination service OAuth2JWTBearer authentication flow requires passing the user’s JWT token in the X-user-token header of the Find Destination call..

However, the OBO flow accepts the user JWT token (a user’s assertion) as a property value instead, what eliminates the requirement of passing it in the X-user-token header of the Find Destination call. Should you prefer the X-user-token header method this works the either way.

Here goes an id_token that has the user context (email claim) of a client application. The id_token audience (aud claim below) – is the client id of the OIDC application.

{ "typ": "JWT", "alg": "RS256", "kid": "<kid>"
}.{ "aud": "5a945db3-f165-4b7b-abbd-e7d7d36d1b23", "iss": "<tenant_id>/v2.0", "iat": 1663452399, "nbf": 1663452399, "exp": 1663456299, "aio": "<aio>", "email": "", "idp": "", "rh": "<rh>", "sub": "<sub>", "tid": "<tenant_id>", "uti": "<uti>", "ver": "2.0"

Quovadis-AzureAD-JWT-ppm – the On-behalf-of destination definition:

{ "owner": { "SubaccountId": "4af53149-557f-43a6-a215-3a20636889ee", "InstanceId": null }, "destinationConfiguration": { "Name": "Quovadis-AzureAD-JWT-ppm", "Type": "HTTP", "URL": "", "Authentication": "OAuth2JWTBearer", "ProxyType": "Internet", "tokenServiceURLType": "Dedicated", "tokenService.KeyStorePassword": "<KeyStorePassword>", "clientId": "5a945db3-f165-4b7b-abbd-e7d7d36d1b23", "Description": "", "tokenService.body.client_assertion": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1dCI6IkRRTU9kcjlJVFp4aGQ2TllJN2w4eTZMSWRUdz0ifQ.eyJpc3MiO(truncated)H7XLf1EgHuWHw9Y5cGekyS8A_ZPIVM", "scope": "openid email offline_access", "x_user_token.jwks_uri": "", "tokenService.body.assertion": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjJaUXBKM1VwYmpBWVhZR2FYRUpsOGxWMFRPSSJ9.eyJhdWQiOiI(truncated)th7ougSpgvkKaAxA6lOB60PVQEj7LAdqaUqJ8MN36rBXYUJnaN3sgJbrlbQDjNE2FP9sw", "tokenServiceURL": "", "tokenService.KeyStoreLocation": "<KeyStoreLocation>", "tokenService.body.requested_token_use": "on_behalf_of", "tokenService.body.client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" }, "authTokens": [ { "type": "Bearer", "value": "eyJ0eXAiOiJKV1QiLCJub25jZSI6IkJubGc1eWJMM29RS0xtcnAwLWVnLTN3dVZHQktlVUNIUGRvSE9FTDVpaWsiLCJhbGci(truncated)yfAU2hXe3xNU2mo7GGjIHpOZugwacgoRQ040RuTf8wBr-aHpANUXPvx6-xp05dYSkvf6XDzjOAd9cJg0A4R6PLFGVSOnQ", "http_header": { "key": "Authorization", "value": "Bearer eyJ0eXAiOiJKV1QiLCJub25jZSI6IkJubGc1eWJMM29RS0xtcnAwLWVnLTN3dVZHQktlVUNIUGRvSE9FTDVpaWsiLCJhbGciO(truncated)yfAU2hXe3xNU2mo7GGjIHpOZugwacgoRQ040RuTf8wBr-aHpANUXPvx6-xp05dYSkvf6XDzjOAd9cJg0A4R6PLFGVSOnQ" }, "expires_in": "1613", "scope": "email openid profile", "refresh_token": "0.AVkA5hKKlTfehUGOosP1ntD5f7NdlFpl8XtLq73n19NtGyNZAJo.AgABAAEAAAD--DLA3VO7QrddgJg7WevrAgDs_wQA9P9qaIHJUpAw6SAIk02ESRo4DTgdzWFRBEbqAeS1BjSyhN5UYYXUtbXs7rU-PmqJQDKv8Ovq_T4Qvl_gmPNia6R2-wPwthKp4TWx6SnHoaBcSrqGjgO0CGIsYZs_Jb2QA-joU93r4mxa83bT8WRrweS53FQGrS2ACCa_FazILmSRSz6MtTbMEycELsG_ou1U7VldFssSNXKWpaY1k9CWWGEunLVmODHujbjpT9FD1WH7f__sjj2WVtle19WvxoxJngsuKU0Oj-mj-a2NJw_mMweQ14nW7S9E93OE6kQWRDuk3mPqLLq29PUp-0-ogU1xfIV26myC_EjZ16DWbqyytQZdPG77iZjtl0_v-nEz3iAff0JFE34NyBhMJlTCwQRkFH0qx12mGXJDs7iPITuxA6X_CLkyug22FY_7RPRQgrBjwS_4PkXppu85HZJoCCp_cnOHzICS6D621gTCyFz0vUQy8AyE9vdcTybkkI2MnXnavHKsEeVZ-OuAfCyfPkgHOd2LbHm73KCd0mFDpnQMfK1rOMnoaVa5dQudOtjWl2KZAvK6d_MyGxEN-R8O-Ac2-XT3i2PQ4oK0iJb0S0qNmBP6LuOwwtDiCFUgyZ1-LjFCZzyMQ0W92DqO0OPIZKdjywyWao-Tu7vynS6Rbs7NOmryx03_kheAcJqahL9aZg7VTqQG_YEF0kaw7GxHDORmNo3-5fw007xbXBQnjxIyUFd93xiIWbaMwneiojXg8CFEOvHbuMfMqy9AdTLyStvpx_3WKVSGQQT7-nOoov3BzoyO7pNmrGUI7Dw6iRtiTgc5STvPlw32wNo1gFhveEJ4qX8enW7xVEYBsFL7tDraKmM21eA" } ]

Exchanged access bearer token

{ "typ": "JWT", "nonce": "<nonce>", "alg": "RS256", "x5t": "<x5t>", "kid": "<kid>"
}.{ "aud": "", "iss": "<tenant_id>/", "iat": 1663620945, "nbf": 1663620945, "exp": 1663624653, "acct": 0, "acr": "0", "aio": "<aio>", "altsecid": "5::10037FFE94410318", "amr": [ "rsa", "mfa" ], "app_displayname": "Quovadis-SAP", "appid": "5a945db3-f165-4b7b-abbd-e7d7d36d1b23", "appidacr": "2", "email": "", "family_name": "Bar", "given_name": "Foo", "idp": "", "idtyp": "user", "ipaddr": "<ipaddr>", "name": "LUC-ISVENG", "oid": "<oid>", "platf": "5", "puid": "<puid>", "rh": "<rh>", "scp": "Calendars.Read Calendars.Read.Shared Calendars.ReadWrite Calendars.ReadWrite.Shared email Mail.Read Mail.Read.Shared Mail.ReadBasic Mail.ReadWrite Mail.ReadWrite.Shared Mail.Send Mail.Send.Shared openid profile Tasks.Read Tasks.Read.Shared Tasks.ReadWrite Tasks.ReadWrite.Shared User.Read", "sub": "<sub>", "tenant_region_scope": "NA", "tid": "<tenant_id>", "unique_name": "", "uti": "<uti>", "ver": "1.0", "wids": [ "62e90394-69f5-4237-9190-012177145e10", "b79fbf4d-3ef9-4689-8143-76b194e85509" ], "xms_st": { "sub": "<sub>" }, "xms_tcdt": 1595613654

Call the resource endpoint with the access token:

curl -H "Authorization: Bearer <access_token>" { "@odata.context": "$metadata#users/$entity", "businessPhones": [], "displayName": "LUC-ISVENG", "givenName": "Foo", "jobTitle": null, "mail": null, "mobilePhone": null, "officeLocation": null, "preferredLanguage": null, "surname": "Bar", "userPrincipalName": "", "id": "<id>"

2. Service to service calls with technical user context

Microsoft offers ample instructions covering this use case. However, in our case, the calling application (the caller) is registered or hosted on SAP BTP and the receiving application (the callee) is registered with MS Azure.

2.1. Shared secret option.

Let’s start with the shared secret option described here

We are going to request a bearer access token for the receiving Quovadis-Web application client_id (=a technical user name).

This can be handled using an OAuth2ClientCredentials destination out-of-the-box as follows:

{ "owner": { "SubaccountId": "<SubaccountId>", "InstanceId": null }, "destinationConfiguration": { "Name": "Quovadis-AzureAD-clientSecret", "Type": "HTTP", "URL": "$metadata:443", "Authentication": "OAuth2ClientCredentials", "ProxyType": "Internet", "tokenServiceURLType": "Dedicated", "tokenService.KeyStorePassword": "<KeyStorePassword for mTLS communication>", "clientId": "8e4ed817-c1b0-4cab-89a0-3bbfa1234567", "Description": "OAuth2ClientCredentials", "scope": "openid email offline_access", "clientSecret": "c91z***********************", "tokenServiceURL": "", "tokenService.KeyStoreLocation": "<KeyStoreLocation for mTLS communication>" }, "authTokens": [ { "type": "Bearer", "value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dC(truncated)YZ_SDLqvwl0cojOsmek_cJAfw_EqdDysfbr8XGXcUKKQpt-uQ", "http_header": { "key": "Authorization", "value": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dC(truncated)YZ_SDLqvwl0cojOsmek_cJAfw_EqdDysfbr8XGXcUKKQpt-uQ" }, "expires_in": "3599" } ]

Good to know:

  • The tokenService.KeyStorePassword and tokenService.KeyStoreLocation properties are for the mutual TLS communication against the OAuth server. However, if mTLS is not required or not enabled, you may remove these properties from the definition above.

2.2. Client assertion (certificate) option.

The certificate or client_assertion method to request a bearer access token described here is more complex to implement.

But it has one undeniable advantage: it mitigates the security risk posed by using passwords. And, furthermore, certificates can be rotated, thus providing additional security.

(Just an observation, the client_assertion option on Azure is quite similar to the x509 option offered for instance by the xsuaa service on BTP.)

At the time of this writing, the destination service does not offer a built-in support to handle client assertions. However, it has a built-in mechanism to let you pass additional parameters when calling the remote OAuth server token endpoint: either with additional headers, url params (queries) or as data (body).

I used the latter option as depicted below:

{ "owner": { "SubaccountId": "<SubaccountId>", "InstanceId": null }, "destinationConfiguration": { "Name": "Quovadis-AzureAD-JWT", "Type": "HTTP", "URL": "$metadata:443", "Authentication": "OAuth2ClientCredentials", "ProxyType": "Internet", "tokenServiceURLType": "Dedicated", "clientId": "8e4ed817-c1b0-4cab-89a0-3bbfaxxxxxxx", "Description": "OAuth2ClientCredentials", "tokenService.body.client_assertion": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1dCI6IkRRTU9kcjlJVFp4aGQ2TllJN2w4eTZMSWRUdz0ifQ.eyJpc3MiO(truncated)WjmL07s_ucHfblfVApYfp1m3eeq-_G35MfUCT_HFL-bVMFV6oq9RWuO4NwtCPT_VA23Qgaf6P8HXBwTcQ9aidKl7hJjIgK93t-9zOyASf0ML44KfQMP67j5jVdIY", "scope": "openid email offline_access", "tokenServiceURL": "", "tokenService.body.client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" }, "authTokens": [ { "type": "Bearer", "value": "eyJ0eXAiOiJKV1QiLCJub25jZSI6Ik9IOUdRVk(truncated)xM6d1lwgMpg9VlpjfY2BtO1Nxp9rYfmgsl77QFcYnBNfj40zWQEGW8QGCk_EzzNXRTUIA"", "http_header": { "key": "Authorization", "value": "Bearer eyJ0eXAiOiJKV1QiLCJub25jZSI6Ik9IOUdRVk(truncated)xM6d1lwgMpg9VlpjfY2BtO1Nxp9rYfmgsl77QFcYnBNfj40zWQEGW8QGCk_EzzNXRTUIA" }, "expires_in": "3599" } ]

When looking at the above definition the only additional parameter that requires some more insight is the value of tokenService.body.client_assertion. More details in the appendix here.

You may also notice the clientSecret is no more…

Last but not least. Here goes a decoded access_token. Please note the audience claim identifies the resource.

{ "typ": "JWT", "nonce": "<nonce>", "alg": "RS256", "x5t": "<x5t>", "kid": "<kid>"
}.{ "aud": "", "iss": "<azureAD_tenant_id>/", "iat": 1663621969, "nbf": 1663621969, "exp": 1663625869, "aio": "<aio>", "app_displayname": "QuoVadis-Web", "appid": "8e4ed817-c1b0-4cab-89a0-3bbfa4071274", "appidacr": "2", "idp": "<azureAD_tenant_id>/", "idtyp": "app", "oid": "<oid>", "rh": "<rh>", "sub": "<sub>", "tenant_region_scope": "EU", "tid": "<azureAD_tenant_id>", "uti": "<uti>", "ver": "1.0", "wids": [ "0997a1d0-0d1d-4acb-b408-d5ca73121e90" ], "xms_tcdt": 1593441337

Last but not least, here goes a call against the $metadata endpoint of our resource – the MS Graph service.

curl$metadata -H "Authorization: Bearer <access_token>" { "@odata.context": "$metadata", "value": [ { "name": "invitations", "kind": "EntitySet", "url": "invitations" }, { "name": "users", "kind": "EntitySet", "url": "users" }, { "name": "applicationTemplates", "kind": "EntitySet", "url": "applicationTemplates" }, ]


Until the client assertion option is natively supported by a destination service the client assertion must be (re)generated outside of a destination definition and then the destination definition must be amended with its value. Only then a call to Find Destination can follow.

Albeit not that practical for approuter destinations, it still might work well in cases the client assertion does not need to be refreshed every time the destination is called.

There is no mandate to use the destination service at all times. Eventually, both user delegated and application flows can be easily implemented with just a few lines of code, without it.

So why bothering that much about having destinations with the destination service?

Well, the sweet spot of destinations is that they can be used to define business and/or functional routes and the approuter (either SAP managed or a standalone one) will parse and process them accordingly.

That approach greatly simplifies the design and maintenance of modular applications with multiple functional endpoints.

Propagate a named user context

The most straightforward option to propagate a user context is to use the OAuth 2.0 code grant flow described here: Authorize access to Azure Active Directory web applications using the OAuth 2.0 code grant flow | Microsoft Docs and here: Microsoft identity platform code samples | Microsoft Docs

Or you may goto my sibling blog, namely AzureAD as an OpenID Connect (OIDC) and OAuth provider | SAP Blogs to see how it can be done in nodejs as a kyma function.

Generate a client assertion

Assuming you have uploaded a valid x509 certificate (which, for the record, can be self-signed) to your Azure hosted application the first thing you need to do is to generate a client assertion.

QuoVadis-Web | Certificates & secrets


A client assertion is a JWT token signed with the private key of your x509 certificate uploaded to the Quovadis-Web application on Azure side. (You may have uploaded several x509 certificates and thus can generate several client assertions…)

This article, Microsoft identity platform application authentication certificate credentials | Microsoft Docs, describes the mandatory format of this JWT token.

In order to generate a JWT you will need a flattened private key and the fingerprint (thumbprint) of the public x509 certificate.

The below code snippet shows how this can be done.

const jwt = require('jsonwebtoken'); const { randomUUID } = require('crypto'); // Added in: node v14.17.0 const key = '-----BEGIN PRIVATE KEY-----\nMIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDFz/eQv30tj5oC\nLjT1Im7OtVAVo6mB/wQbEpbOh3LSI8h/f00fwLMJ/uQ3nYHiwqsElTvKA0h0B5tm\n79w(truncaated)LSxBTGdiZznqgLKnImxU1WDSA2xlKJy7J\nAwx8lLYgANSJ7qkKPgPR/t5ZHrx/plY=\n-----END PRIVATE KEY-----\n'; //
const fingerprint = '0d030e76bf484d9c6177a35823b97ccba1234567'; //
function hex2bin(hexSource) { var bin = ''; for (var i=0;i<hexSource.length;i=i+2) { bin += String.fromCharCode(hexdec(hexSource.substr(i,2))); } return bin;
function hexdec(hexString) { hexString = (hexString + '').replace(/[^a-f0-9]/gi, '') return parseInt(hexString, 16)
const x5t = hex2bin(fingerprint); function generateAccessToken_azure(client_id, token_endpoint) { console.log('generateAccessToken_azure: ', randomUUID()); return jwt.sign( {iss: client_id, sub: client_id, aud: token_endpoint, jti: randomUUID() }, key, { algorithm: 'RS256', header: {x5t : btoa(x5t) }, expiresIn: '1d' } );


client assertion (=JWT) decoded

Certificate fingerprints (thumbprints)

A certificate fingerprint, or a SHA-1 thumbprint of a X.509 certificate. is a hexadecimal number (and not a string). It needs to be converted to a binary format before being Base64-encoded as a x5t JWT header claim.

Failure to do so will result in an invalid JWT header and errors for instance: AADSTS700027: Client assertion failed signature validation | Microsoft Community

Good to know:

  • The uploaded certificates thumbprints can be copied directly from the manifest of the Quovadis-Web application.
  • Alternatively, the following tool is of help when dealing with a public x509 certificate especially when it comes to thumbprints:
  • SAP BTP,Kyma runtime function could be used to implement the JWT token generation logic and expose it as an endpoint via a protected API rule.