Technical Articles
SAP APIM : Shared Flow for OAUTH to SAP CPI
Introduction
A shared flow is a collection of reusable policies and resources that can be consumed from multiple API proxies. It is like an API proxy but does not have endpoints. While the SAP Cloud Platform API Management does not yet support Shared Flow out-of-box, I will show in this blog on how to achieve it using a workaround.
We will see it with an example of abstracting OAuth Client Credentials implementation policies into a shared flow. The shared flow can be called from any API Proxy to fetch an Access token using OAuth Client Credentials grant from any Authorization server.
I will use SAP Cloud Platform OAuth Service as the Authorization server, however, this Shared flow can be used for any Authorization Server that supports Client Credential grant as specified in RFC 6749. We will use SAP CPI as the resource server, however, this approach can be used to access any resource in SAP Cloud Platform protected with SCP OAuth Service.
Scenario
- The main API Proxy – Exposes SAP CPI Endpoint as API. This API proxy connects to SAP CPI using an Access token acquired with OAuth Client Credentials Grant. I will not show how to do this as it’s already shown here Secure connectivity (OAuth) to SAP Cloud Platform Integration.
- The Shared Flow (API Proxy) – This is called from main API Proxy to fetch Bearer Access token from the required Authorization Server.
Process Flow
- Communication between Main API proxy and Shared Flow will be secured using a Shared Secret that is stored in an encrypted key-value map.
Thus the secret is only accessible by the API Proxy in runtime and hence protecting the shared flow endpoint access from outside. (A random number generated, exchanged & validated using cache will be even more secure) - Main API Proxy always looks for the token in Cache using a <key> that uniquely identifies the Authorization Server it’s interested in.
- If Main API Proxy finds the token in Cache it goes directly to step -11.
- If Token Not found, Main API Proxy invoke the Shared Flow using the Shared secret and Authorization Server Key. Step- 5 to 10 are executed in this case.
- Shared Flow validates the Shared Secret.
- Shared Flow then fetches the Client ID, Client Secret, and Authorization Server Token Endpoint from an encrypted key-value map.
- Shared Flow set’s the Client ID and Secret in Authorization Header, dynamically set the Shared flow target endpoint to Authorization Server Token endpoint and invokes it.
- Shared Flow receives the token response and extracts the access token and expiry duration.
- Shared Flow caches the token for the expiry duration and returns control to Main Flow
- Main Flow once again tries to fetch the token from the cache and it finds a hit this time.
- Main Flow set the bearer token in Authorization Header of the SAP CPI request message.
Implementation
Key-Value Map
We have 2 Key Value maps involved here. The first one for saving shared secrets required between Main and Shared Proxy and the next one to save the Authorization Server(s) connection credentials like ID, Secret, and token URL.
1. Shared Secret
2. OAuth Credentials
Main API Proxy
- Lookup Token from Caches with a unique Key of Authorization Server
- Fetch Shared Secret from Key Value Map
- Acquire Access Token by calling the Shared Flow
- Set the Access Token to request Header
Policy in 2 & 3 is executed only when Look-up i.e. 1 does not result in a token.
You can download the policy template “PT_OAuthConnection” from Git Repository or Implement it from the API Policy below.
API Policy
- lookupAccessToken
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <LookupCache async="false" continueOnError="false" enabled="true" xmlns="http://www.sap.com/apimgmt"> <CacheKey> <Prefix>OAUTH</Prefix> <KeyFragment>scpdev</KeyFragment> </CacheKey> <Scope>Global</Scope> <AssignTo>sapapim.accessToken</AssignTo> </LookupCache>
<KeyFragment> value is the identifier for an Authorization Server. Maintain your value.
The same policy is used between 3 & 4 but with Condition String : lookupcache.lookupAccessToken.cachehit = false - getSharedSecret
Condition String : lookupcache.lookupAccessToken.cachehit = false<KeyValueMapOperations mapIdentifier="SharedSecret" async="true" continueOnError="true" enabled="true" xmlns="http://www.sap.com/apimgmt"> <Get assignTo="private.shared.secret"> <Key> <Parameter>secret</Parameter> </Key> </Get> <Scope>environment</Scope> </KeyValueMapOperations>
- fetchAccessToken
Condition String : lookupcache.lookupAccessToken.cachehit = false<ServiceCallout async="true" continueOnError="false" enabled="true" xmlns="http://www.sap.com/apimgmt"> <Request> <Set> <Headers> <Header name="tenant">scpdev</Header> <Header name="secret">{private.shared.secret}</Header> </Headers> <Verb>POST</Verb> </Set> </Request> <Response>sapapim.oauth.response</Response> <Timeout>15000</Timeout> <LocalTargetConnection> <Path>/p318236trial/get/oauth</Path> </LocalTargetConnection> </ServiceCallout>
The header ‘tenant ‘ should be set to the Authorization Server Identifier.
<Path> should be the API base Path of Shared Flow. - setAuthHeader
<AssignMessage async="false" continueOnError="false" enabled="true" xmlns='http://www.sap.com/apimgmt'> <Remove> <Headers/> </Remove> <Set> <Headers> <Header name="Authorization">Bearer {sapapim.accessToken}</Header> </Headers> </Set> <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables> <AssignTo createNew="false" type="request">request</AssignTo> </AssignMessage>
Shared Flow API Proxy
This Shared Flow (API Proxy) in the workaround way will have a proxy endpoint however we will protect it with a shared secret that is technically accessible only from the runtime by API Proxy. I have designed the Shared Flow to have a target end-point for Authorization Server. [But it can also be done with a service call-out step and setting the Target Endpoint to None in route rule. So we can block this flow Proxy and Target Endpoint making it an absolute Shared Flow.]
- Get Shared Secret from Key Value Map
- Compare the Shared Secret with the request header “secret” to allow execution
- Retrieve the request header “tenant” and construct the Keys to read the OAuth Creds
- Read OAuth Connectivity ID, Secret and Token URL from Key Value Map
- Base64 encode the ClientID:ClientSecret
- Set the Encoded value to Authorization Header and the API Proxy Target URL to the Token Endpoint URL fetched from Key Value Map
- Read the response and retrieve the Access Token and Expiry Duration
- Cache the Access Token with the Authorization Server Identifier
You can download the Shared Flow API Proxy “Get_OAuthToken” from Git Repository here or implement it from below API Policy.
API Policy
- getSharedSecret
<KeyValueMapOperations mapIdentifier="SharedSecret" async="true" continueOnError="true" enabled="true" xmlns="http://www.sap.com/apimgmt"> <Get assignTo="private.shared.secret"> <Key> <Parameter>secret</Parameter> </Key> </Get> <Scope>environment</Scope> </KeyValueMapOperations>
- checkSharedSecret
<Javascript async="false" continueOnError="false" enabled="true" timeLimit="200" xmlns='http://www.sap.com/apimgmt'> <ResourceURL>jsc://checkSharedSecret.js</ResourceURL> </Javascript>
var envsecret = context.getVariable("private.shared.secret"); var reqsecret = context.getVariable("request.header.secret"); if (envsecret != reqsecret) throw 'Incorrect Shared Secret';
- setVariables
<Javascript async="false" continueOnError="false" enabled="true" timeLimit="200" xmlns='http://www.sap.com/apimgmt'> <ResourceURL>jsc://setVariables.js</ResourceURL> </Javascript>
var tenant = context.getVariable("request.header.tenant"); context.setVariable("authserver.ClientID", tenant.concat("_ClientID")); context.setVariable("authserver.ClientSecret", tenant.concat("_ClientSecret")); context.setVariable("authserver.tURL", tenant.concat("_tURL")); context.setVariable("req.tenant",tenant);
- getOAuthCreds
<KeyValueMapOperations mapIdentifier="OAuthCreds" async="true" continueOnError="false" enabled="true" xmlns="http://www.sap.com/apimgmt"> <Get assignTo="private.authserver.clientID"> <Key> <Parameter ref="authserver.ClientID"></Parameter> </Key> </Get> <Get assignTo="private.authserver.clientSecret"> <Key> <Parameter ref="authserver.ClientSecret"></Parameter> </Key> </Get> <Get assignTo="private.authserver.tURL"> <Key> <Parameter ref="authserver.tURL"></Parameter> </Key> </Get> <Scope>environment</Scope> </KeyValueMapOperations>
- Authorization
<BasicAuthentication async='true' continueOnError='false' enabled='true' xmlns='http://www.sap.com/apimgmt'> <Operation>Encode</Operation> <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables> <User ref='private.authserver.clientID'></User> <Password ref='private.authserver.clientSecret'></Password> <AssignTo createNew="true">sapapim.auth</AssignTo> </BasicAuthentication>
- setTokenURLnCred
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <AssignMessage async="false" continueOnError="false" enabled="true" xmlns="http://www.sap.com/apimgmt"> <Remove> <Headers> </Headers> </Remove> <Set> <Headers> <Header name="Authorization">{sapapim.auth}</Header> <Header name="Content-Type">application/x-www-form-urlencoded</Header> </Headers> <FormParams> <FormParam name="grant_type">client_credentials</FormParam> </FormParams> </Set> <AssignVariable> <Name>target.url</Name> <Ref>private.authserver.tURL</Ref> </AssignVariable> <IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables> <AssignTo createNew="false" type="request">request</AssignTo> </AssignMessage>
- readaccesstoken
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <ExtractVariables async="false" continueOnError="false" enabled="true" xmlns="http://www.sap.com/apimgmt"> <JSONPayload> <Variable name="sapapim.accessToken" type="string"> <JSONPath>$.access_token</JSONPath> </Variable> <Variable name="sapapim.expiresIn" type="integer"> <JSONPath>$.expires_in</JSONPath> </Variable> </JSONPayload> <Source>message.content</Source> </ExtractVariables>
- cacheaccesstoken
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <PopulateCache async="false" continueOnError="false" enabled="true" xmlns="http://www.sap.com/apimgmt"> <CacheKey> <Prefix>OAUTH</Prefix> <KeyFragment ref="req.tenant"></KeyFragment> </CacheKey> <Scope>Global</Scope> <ExpirySettings> <TimeoutInSec ref="sapapim.expiresIn"></TimeoutInSec> </ExpirySettings> <Source>sapapim.accessToken</Source> </PopulateCache>