Personal Insights
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. Disclaimer:
|
Putting it all together
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 with the OIDC Authorization code grant flow as a nodejs 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:
Methodology explainer:
|
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. More details on how to fetch this user JWT in appendix below,
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-xxxxxxxxxxxx",
"iss": "https://login.microsoftonline.com/<tenant_id>/v2.0",
"iat": 1663452399,
"nbf": 1663452399,
"exp": 1663456299,
"aio": "<aio>",
"email": "foo.bar@microsoft.com",
"idp": "https://sts.windows.net/42f7676c-f455-423c-82f6-xxxxxxxxxxxx/",
"rh": "<rh>",
"sub": "<sub>",
"tid": "<tenant_id>",
"uti": "<uti>",
"ver": "2.0"
}.[Signature]
Quovadis-AzureAD-JWT-ppm – the On-behalf-of destination definition:
{
"owner": {
"SubaccountId": "<SubaccountId>",
"InstanceId": null
},
"destinationConfiguration": {
"Name": "Quovadis-AzureAD-JWT-ppm",
"Type": "HTTP",
"URL": "https://graph.microsoft.com/v1.0/me:443",
"Authentication": "OAuth2JWTBearer",
"ProxyType": "Internet",
"tokenServiceURLType": "Dedicated",
"tokenService.KeyStorePassword": "<KeyStorePassword>",
"clientId": "5a945db3-f165-4b7b-abbd-xxxxxxxxxxxx",
"Description": "https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow",
"tokenService.body.client_assertion": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1dCI6IkRRTU9kcjlJVFp4aGQ2TllJN2w4eTZMSWRUdz0ifQ.eyJpc3MiO(truncated)H7XLf1EgHuWHw9Y5cGekyS8A_ZPIVM",
"scope": "openid email offline_access https://graph.microsoft.com/.default",
"x_user_token.jwks_uri": "https://login.microsoftonline.com/<azureAD_teant_id>/discovery/v2.0/keys",
"tokenService.body.assertion": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjJaUXBKM1VwYmpBWVhZR2FYRUpsOGxWMFRPSSJ9.eyJhdWQiOiI(truncated)th7ougSpgvkKaAxA6lOB60PVQEj7LAdqaUqJ8MN36rBXYUJnaN3sgJbrlbQDjNE2FP9sw",
"tokenServiceURL": "https://login.microsoftonline.com/958a12e6-de37-4185-8ea2-c3f59ed0f97f/oauth2/v2.0/token",
"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 https://graph.microsoft.com/Calendars.Read https://graph.microsoft.com/Calendars.Read.Shared https://graph.microsoft.com/Calendars.ReadWrite https://graph.microsoft.com/Calendars.ReadWrite.Shared https://graph.microsoft.com/Mail.Read https://graph.microsoft.com/Mail.Read.Shared https://graph.microsoft.com/Mail.ReadBasic https://graph.microsoft.com/Mail.ReadWrite https://graph.microsoft.com/Mail.ReadWrite.Shared https://graph.microsoft.com/Mail.Send https://graph.microsoft.com/Mail.Send.Shared https://graph.microsoft.com/Tasks.Read https://graph.microsoft.com/Tasks.Read.Shared https://graph.microsoft.com/Tasks.ReadWrite https://graph.microsoft.com/Tasks.ReadWrite.Shared https://graph.microsoft.com/User.Read https://graph.microsoft.com/.default",
"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"
}
]
}
Here goes the exchanged access bearer token. It has a user context so can be used with both the technical endpoints of a resource and the ones that require passing of the user context (like /me).
{
"typ": "JWT",
"nonce": "<nonce>",
"alg": "RS256",
"x5t": "<x5t>",
"kid": "<kid>"
}.{
"aud": "https://graph.microsoft.com",
"iss": "https://sts.windows.net/<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-xxxxxxxxxxxx",
"appidacr": "2",
"email": "foo.bar@microsoft.com",
"family_name": "Bar",
"given_name": "Foo",
"idp": "https://sts.windows.net/42f7676c-f455-423c-82f6-xxxxxxxxxxxx/",
"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": "foo.bar@microsoft.com",
"uti": "<uti>",
"ver": "1.0",
"wids": [
"62e90394-69f5-4237-9190-012177145e10",
"b79fbf4d-3ef9-4689-8143-76b194e85509"
],
"xms_st": {
"sub": "<sub>"
},
"xms_tcdt": <xms_tcdt>
}.[Signature]
Eventually, we can call the MS Graph’s resource endpoint with the access token:
curl https://graph.microsoft.com/v1.0/me -H "Authorization: Bearer <access_token>"
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users/$entity",
"businessPhones": [],
"displayName": "LUC-ISVENG",
"givenName": "Foo",
"jobTitle": null,
"mail": null,
"mobilePhone": null,
"officeLocation": null,
"preferredLanguage": null,
"surname": "Bar",
"userPrincipalName": "foo.bar_sap.com#EXT#@ppmxxxxxxx.onmicrosoft.com",
"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/workload is hosted on one of SAP BTP runtimes whereas both the receiving OIDC application and the backend resource are, respectively, registered with and hosted on 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": "https://graph.microsoft.com/v1.0/$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 https://graph.microsoft.com/.default",
"clientSecret": "c91z***********************",
"tokenServiceURL": "https://login.microsoftonline.com/3e2cfdd6-0dc4-4387-a7cf-xxxxxxxxxxxx/oauth2/v2.0/token",
"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
andtokenService.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 as they both eliminate the use of secrets when calling the token endpoint.)
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": "https://graph.microsoft.com/v1.0/$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 https://graph.microsoft.com/.default",
"tokenServiceURL": "https://login.microsoftonline.com/3e2cfdd6-0dc4-4387-a7cf-xxxxxxxxxx/oauth2/v2.0/token",
"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": "https://graph.microsoft.com",
"iss": "https://sts.windows.net/<azureAD_tenant_id>/",
"iat": 1663621969,
"nbf": 1663621969,
"exp": 1663625869,
"aio": "<aio>",
"app_displayname": "QuoVadis-Web",
"appid": "8e4ed817-c1b0-4cab-89a0-3bbf1234567",
"appidacr": "2",
"idp": "https://sts.windows.net/<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": [
"<wids>"
],
"xms_tcdt": <xms_tcdt>
}.[Signature]
Last but not least, here goes a call against the $metadata endpoint of our resource – the MS Graph service.
curl https://graph.microsoft.com/v1.0/$metadata -H "Authorization: Bearer <access_token>"
{
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata",
"value": [
{
"name": "invitations",
"kind": "EntitySet",
"url": "invitations"
},
{
"name": "users",
"kind": "EntitySet",
"url": "users"
},
{
"name": "applicationTemplates",
"kind": "EntitySet",
"url": "applicationTemplates"
},
]
}
Caveats
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. More details in the appendix here.
Conclusion
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 getting that done with destinations and the destination service?
Well, the sweet spot of destinations is that they make really easy to define business and/or functional logic (=routes) and that the approuter (either SAP managed or a standalone one) can prropagate them accordingly.
That approach greatly simplifies the design, integration and maintenance of modular applications with multiple functional endpoints.
Last but not least. I hope you have enjoyed reading this post. As usual, please make use of the comments section below in case of questions and to provide feedback.
Appendix
Propagate a named user context
The most straightforward option to propagate a user context and fetch the user’s id_token
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.
For educational purposes: the below code snippet shows how this could be done.
Alternatively, quoting after Microsoft documentation:
To compute the assertion, you can use one of the many JWT libraries in the language of your choice – MSAL supports this using
.WithCertificate()
. The information is carried by the token in its Header, Claims, and Signature.
const jwt = require('jsonwebtoken');
const { randomUUID } = require('crypto');
const key = '-----BEGIN PRIVATE KEY-----\nMIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDFz/eQv30tj5oC\nLjT1Im7OtVAVo6mB/wQbEpbOh3LSI8h/f00fwLMJ/uQ3nYHiwqsElTvKA0h0B5tm\n79w(truncaated)LSxBTGdiZznqgLKnImxU1WDSA2xlKJy7J\nAwx8lLYgANSJ7qkKPgPR/t5ZHrx/plY=\n-----END PRIVATE KEY-----\n';
// https://www.samltool.com/fingerprint.php
// the fingerprint can be fetched from the OIDC application manifest
const fingerprint = '0d030e76bf484d9c6177a35823b97ccba1234567';
// https://stackoverflow.com/a/56236665
//
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' } );
}
Please note: the client assertion below has the Azure Identity token issuance service endpoint as its audience and the OIDC application client_id as the issuer.
jwt.ms
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: https://www.samltool.com/fingerprint.php
- 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.