Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
miroll
Product and Topic Expert
Product and Topic Expert

Scenario Overview


Single Sign-On is one of the most convenient features for users. This convenience however usually comes with a lot of integration effort and challenges on the implementation side. This is especially the case if we have different Identity Providers (IdP) and Trust setups involved.

This blog entry aims to guide you through one of these scenarios: Enabling principal propagation between an Azure AD as our IdP and an On-Premise SAP System through an API on SAP API Management.

For this we'll make use of the great work done by mraepple which he presented in his blog entry on Principal propagation in a multi-cloud solution between Microsoft Azure and SAP BTP. In our case we'll have a more specialised scenario in terms of the xsuaa connection on SAP BTP side. However we have an additional component with the SAP API Management in between which will play an important role in this case.

The challenge we faced with the integration was that clients would call our endpoints exposed by API Proxies in the SAP API Management with an OIDC token issued by the Azure AD. This OIDC token however can't be processed by the xsuaa in this form and therefore cannot be used for accessing the On-Premise connection. For the purpose of this blog we assume that the principal propagation setup from the BTP CF environment to the On-Premise system is already configured and tested. The main issue is to integrate this into our API flow in the API Proxy and exchange the Azure AD token for a token trusted by our xsuaa.

Solution Overview


The following picture gives an overview of the intended setup of the solution and the request flows in between:


Solution Overview and sequence


We use the approach explained in the blog mentioned above to exchange the incoming OIDC token for a SAML token. That token can then be used to acquire a valid JWT token from our xsuaa instance. Due to the setup of the principal propagation we can use that token to forward the initial request to the backend system through the SAP Cloud Connector.

For this connection we use the On-Premise Connectivity Plan of the API Portal service as described in the help page of SAP API Management. After setting it up and creating a service key to get the credentials we're ready to implement the API Proxy and its flow in the API Portal.

Prerequisites


To enable the flow which was previously outlined we need access to both the Azure AD instance and our SAP API Management API Portal instance. Of course these access permissions might be distributed across different people. As per Martin's blog I linked to above this is (by my knowledge) currently only possible with Azure AD and the token exchange feature of it.

To create the needed on-premise-connectivity instance of the API Management and its service key you'll also need the according permission and entitlement in your subaccount.

The most important step in the preparation is to setup the trust between the Azure AD instance and our xsuaa instance as described in Martin's blog!

The complete list:

  • Azure AD instance and access to the service key creation

  • Trust setup between the Azure AD instance and our xsuaa instance

  • SAP API Management API Portal instance and Developer permissions on it

  • on-premise-connectivity instance service key or permission and entitlement to create it

  • (optional) an on premise system to test the setup with 😉


SAP API Management Artefacts


Since we don't want to add the same steps for every API Proxy that wants to access On-Premise resources with Principal Propagation, we'll implement it as a shared API Proxy similar to the one mentioned in this blog. This way it can be called from the other API Proxies in order to retrieve the valid token.

Preparation


For the needed calls to Azure AD we'll first create a relatively simple API Provider with the configuration to connect to the Azure AD instance:


Azure AD API Provider


Second we'll create an encrypted Key Value Map for the credentials we need. it contains details of three categories:

  • The shared secret to use the Shared Flow (see above)

  • Credentials from the on-premise-connectivity service instance key

  • Credentials from Azure AD



Key Value Map with needed credentials


Sorry for the poor ordering of the key value map entries 🙂

API Proxy


Now for the API Proxy itself. The API Proxy we're going to create is a so called Loopback API which does not actually forward the request to a target endpoint - all logic will be done in Service Callouts. For this we create a blank API Proxy and just one resource: /token The actual work then is done in the policies of the API Proxy.

Let's look at the overview first - there's two flows which we're using: the PreFlow of the ProxyEndpoint to check whether the caller has our shared secret and the token flow of the ProxyEndpoint where we do the actual token conversion. The former can be taken from the blog I already linked above and is not part of this blog.

In total we use 8 policies - 7 on the Incoming request and 1 on the outgoing response:


Policies in token flow


The first JS policy extracts the incoming JWT token issued by Azure AD and stores it in a variable:
// extract auth header to retrieve JWT Token
var reqAuth = context.getVariable("request.header.Authorization");

if (!reqAuth) throw 'Missing Auth Header';

var authParts = reqAuth.split(" ");

if (!authParts[0].toLowerCase() === "bearer" || authParts.length !== 2) throw 'Only OAuth Bearer Tokens Accepted'

if (!authParts[1] || !/^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/.test(authParts[1])) throw 'Only valid OAuth Bearer Tokens Accepted'

context.setVariable("private.jwttoken", authParts[1]);

Next we extract all needed values from our previously created encrypted key value map. For convenience all the local variables in the flow are called the same as the keys in the key value map ("private.<key value map key>").

Using the credentials retrieved from the key value map and the incoming JWT token we now query the token endpoint of the Azure AD instance to exchange our JWT token for a SAML assertion. This equals the "Request SAML assertion from AAD with ObO flow" Postman request of the Postman collection in Martin's blog. The code of the ServiceCallout policy makes use of the credentials and details of the Azure AD instance we just retrieved from the encrypted key value map. We're using the API Provider which we previously created in the preparation as the HTTPTargetConnection of our request.
<ServiceCallout async="true" continueOnError="false" enabled="true" xmlns="http://www.sap.com/apimgmt">
<Request>
<Set>
<Headers>
<Header name="Authorization">Bearer {private.jwttoken}</Header>
<Header name="Content-Type">application/x-www-form-urlencoded</Header>
<Header name="Accept">*/*</Header>
<Header name="Accept-Encoding">gzip, deflate, br</Header>
</Headers>
<FormParams>
<FormParam name="grant_type">urn:ietf:params:oauth:grant-type:jwt-bearer</FormParam>
<FormParam name="client_id">{private.aad_client_id}</FormParam>
<FormParam name="client_secret">{private.aad_client_secret}</FormParam>
<FormParam name="resource">{private.aad_resource_id}</FormParam>
<FormParam name="requested_token_use">on_behalf_of</FormParam>
<FormParam name="requested_token_type">urn:ietf:params:oauth:token-type:saml2</FormParam>
<FormParam name="assertion">{private.jwttoken}</FormParam>
</FormParams>
<Verb>POST</Verb>
</Set>
</Request>
<!-- the variable into which the response from the external service should be stored -->
<Response>private.azuread.response</Response>
<!-- The time in milliseconds that the Service Callout policy will wait for a response from the target before exiting. Default value is 120000 ms -->
<Timeout>30000</Timeout>
<HTTPTargetConnection>
<APIProvider>INT0201_Azure_AD</APIProvider>
<Path>/{private.aad_tenant_id}/oauth2/token</Path>
</HTTPTargetConnection>
</ServiceCallout>

To retrieve the SAML assertion from the response we use the ExtractVariables policy.
<ExtractVariables async="false" continueOnError="false" enabled="true" xmlns="http://www.sap.com/apimgmt">
<JSONPayload>
<Variable name="private.azuread.access_token" type="string">
<JSONPath>$.access_token</JSONPath>
</Variable>
</JSONPayload>
<Source>private.azuread.response.content</Source>
</ExtractVariables>

Additionally to retrieving the SAML assertion we also need to create credentials for our ServiceCallout to the xsuaa instance which will be the last of our service calls. For this we use the third set of variables retrieved from the key value maps: the credentials of our on-premise-connectivity service instance. For this we use the BasicAuthentication policy with the client id and secret.
<BasicAuthentication async='true' continueOnError='false' enabled='true' xmlns='http://www.sap.com/apimgmt'>
<Operation>Encode</Operation>
<IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
<User ref='private.uaa_client_id'></User>
<Password ref='private.uaa_client_secret'></Password>
<AssignTo createNew="true">sapapim.auth</AssignTo>
</BasicAuthentication>

Now we're ready for the last call which will give us the token to be used when connecting to an on premise system.
<ServiceCallout async="true" continueOnError="false" enabled="true" xmlns="http://www.sap.com/apimgmt">
<Request>
<Set>
<Headers>
<Header name="Authorization">{sapapim.auth}</Header>
<Header name="Content-Type">application/x-www-form-urlencoded</Header>
<Header name="Accept">*/*</Header>
<Header name="Accept-Encoding">gzip, deflate, br</Header>
</Headers>
<FormParams>
<FormParam name="grant_type">urn:ietf:params:oauth:grant-type:saml2-bearer</FormParam>
<FormParam name="assertion">{private.azuread.access_token}</FormParam>
</FormParams>
<Verb>POST</Verb>
</Set>
</Request>
<Response>sapapim.oauthresponse.token</Response>
<Timeout>30000</Timeout>
<HTTPTargetConnection>
<URL>https://{private.uaa_token_domain}</URL>
<SSLInfo>
<Enabled>true</Enabled>
<ClientAuthEnabled>false</ClientAuthEnabled>
<KeyStore/>
<KeyAlias/>
<TrustStore/>
</SSLInfo>
</HTTPTargetConnection>
</ServiceCallout>

The SAML assertion of the previous step is passed to the xsuaa instance which (because of the previously set up trust relationship) returns a valid JWT token for this xsuaa instance.

Lastly we retrieve the token from the response and assign the corresponding fields to our response so the caller can use it. The latter is done in an AssignMessage policy on the response flow of the token flow.
<ExtractVariables async="false" continueOnError="false" enabled="true" xmlns="http://www.sap.com/apimgmt">
<JSONPayload>
<Variable name="private.btp.access_token" type="string">
<JSONPath>$.access_token</JSONPath>
</Variable>
</JSONPayload>
<Source>sapapim.oauthresponse.token.content</Source>
</ExtractVariables>

<AssignMessage async="false" continueOnError="false" enabled="true" xmlns='http://www.sap.com/apimgmt'>
<Set>
<Payload contentType="application/json" variablePrefix="@" variableSuffix="#">{"auth_token":"@private.btp.access_token#"}</Payload>
</Set>
<IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
<AssignTo createNew="false" type="response">response</AssignTo>
</AssignMessage>

Usage


So how are you going to use the newly created Shared Principal Propagation API Proxy? You can just integrate it as a ServiceCallout into any other API Proxy on your instance e.g. like this:
<ServiceCallout async="true" continueOnError="false" enabled="true" xmlns="http://www.sap.com/apimgmt">
<Request>
<Set>
<Headers>
<Header name="Authorization">Bearer {request.header.Authorization}</Header>
<Header name="secret">{private.shared.secret}</Header>
</Headers>
<Verb>POST</Verb>
</Set>
</Request>
<Response>sapapim.accessToken</Response>
<Timeout>15000</Timeout>
<LocalTargetConnection>
<Path>/v1/shared/principalpropagation/token</Path>
</LocalTargetConnection>
</ServiceCallout>

Of course you can also use the Copy functionality of the AssignMessage policy to create the request to the Service Callout. /v1/shared/principalpropagation in this case is the base path of the Shared Principal Propagation API Proxy I created and of course you need to add the resource /token as well.

Summary


Using the great blog by Martin as our blueprint we applied the principle to our specialised use case of doing this in SAP API Management. Since SAP API Management specifically provides a service plan for the on premise access with the on-premise-connectivity service plan, we don't need to dynamically set our target xsuaa instance but can always use the one connected to that instance.

Outlook


Of course there are some things that could be improved about this setup like error handling on false or illegitimate requests. If you spot anything feel free to point it out in the comments!

Thanks


At this point I'd also like to thank tobias.pahlings for his help with all of the security knowledge needed for this integration.
15 Comments