[[{“value”:”
SAP API Management for Beginners – Part 4: OAuth 2.0, KVM, Traffic Policies, Threat Protection & CORS
Part 4 of a 5-part beginner series on SAP API Management within SAP Integration Suite.
A quick recap
In Part 1, we toured every APIM feature. In Part 2, we built Northwind_API_V1 — 75 auto-discovered resources, conditional flows, and the defaultRaiseFaultPolicy. In Part 3, we added the VerifyAPIKey policy, created a Product and Application, tested the full subscription flow, explored Debug, versioning, and policy templates.
After Part 3, our proxy’s PreFlow has one policy:
<preFlow>
<name>PreFlow</name>
<request>
<isRequest>true</isRequest>
<steps>
<step>
<policy_name>VerifyAPIKey</policy_name>
<sequence>1</sequence>
</step>
</steps>
</request>
</preFlow>
And the <policies> section lists two entries:
<policies>
<policy type="RaiseFault">defaultRaiseFaultPolicy</policy>
<policy type="VerifyAPIKey">VerifyAPIKey</policy>
</policies>
By the end of this post, both sections will be much bigger. We’ll add six policies — one at a time, each with placement rationale, XML config, and a Postman test.
1. Policy execution order — the golden rule
Before adding anything, let’s establish the execution model. This is the single biggest source of beginner confusion.
The golden rule: reject early, transform late.
Here’s where each type of policy belongs, and why:
ProxyEndpoint PreFlow (Incoming Request) — consumer-facing:
1. VerifyAPIKey ← authenticate first (already done)
2. ValidateOAuthToken ← second auth layer (this post)
3. JSONThreatProtection ← validate payload before processing
4. SpikeArrest ← throttle bursts
5. QuotaLimit ← cap total calls
ProxyEndpoint PostFlow (Outgoing Response) — consumer-facing:
6. AddCORSHeaders ← add Access-Control headers to response
TargetEndpoint PreFlow (Incoming Request) — backend-facing:
7. KVM-GetCredentials ← read backend credentials from secure store
8. InjectBasicAuth ← encode and inject Authorization header
💡This is the APIM equivalent of the “credential direction” insight from the Event Mesh series. Consumer-facing policies (who are you? how much can you call? is your payload safe?) go on ProxyEndpoint. Backend-facing policies (what credentials does the backend need?) go on TargetEndpoint. Mixing them up is the #1 reason policies “don’t work.”
After this post, the Proxy Endpoint PreFlow will have five steps:
<preFlow>
<name>PreFlow</name>
<request>
<isRequest>true</isRequest>
<steps>
<step>
<policy_name>VerifyAPIKey</policy_name>
<sequence>1</sequence>
</step>
<step>
<policy_name>ValidateOAuthToken</policy_name>
<sequence>2</sequence>
</step>
<step>
<policy_name>JSONThreatProtection</policy_name>
<sequence>3</sequence>
</step>
<step>
<policy_name>SpikeArrest</policy_name>
<sequence>4</sequence>
</step>
<step>
<policy_name>QuotaLimit</policy_name>
<sequence>5</sequence>
</step>
</steps>
</request>
</preFlow>
And the Target Endpoint PreFlow (currently empty) will gain two steps:
<preFlow>
<name>PreFlow</name>
<request>
<isRequest>true</isRequest>
<steps>
<step>
<policy_name>KVM-GetCredentials</policy_name>
<sequence>1</sequence>
</step>
<step>
<policy_name>InjectBasicAuth</policy_name>
<sequence>2</sequence>
</step>
</steps>
</request>
</preFlow>
Let’s build them one at a time.
2. OAuth 2.0 — APIM as its own token server
API keys are simple but limited — they’re long-lived strings that don’t expire unless you manually revoke them. OAuth 2.0 is the industry standard for token-based authentication, where consumers get short-lived access tokens.
Here’s the question most tutorials get wrong: where does the token come from? Most guides tell you to set up SAP IAS, Azure Entra ID, or Okta as an external OAuth provider. That works — but it adds complexity, cost, and another system to manage.
APIM can generate OAuth tokens itself. No external provider needed. We’ll build a dedicated OauthService proxy that acts as the token server, and then validate those tokens on our Northwind_API_V1 proxy.
2.1 The architecture — two proxies working together
Step 1 — Consumer gets a token from APIM:
POST https://<apim-host>/oauth/GenerateToken
Body: grant_type=client_credentials
&client_id=<Application Key>
&client_secret=<Application Secret>
→ APIM generates and returns an access_token
Step 2 — Consumer calls the API with that token:
GET https://<apim-host>/V1/ProxyNorthwindAPI/Customers
Header: Authorization: Bearer <access_token>
→ APIM validates the token → Route to Northwind
Two proxies:
OauthServiceat/oauth— generates tokens (usingGenerateAccessToken)Northwind_API_V1at/V1/ProxyNorthwindAPI— validates tokens (usingVerifyAccessToken)
2.2 Build the OauthService proxy
This proxy has no backend — it IS the service. APIM’s built-in OAuth engine handles everything.
Step 1: In the API Portal, go to Develop → Create → API.
Step 2: This time, select URL (not API Provider):
| Select | URL |
| URL | http://none.com/ |
| Name | OauthService |
| Title | OauthService |
| API Base Path | /oauth |
| Service Type | REST |
💡Why
http://none.com/? This proxy never calls a backend. The OAuth policy generates the token and returns it directly to the consumer. The target URL is a placeholder — APIM requires one, but it’s never used.
Here’s what the Target Endpoint looks like in the exported XML — notice provider_id=NONE:
<TargetEndPoint xmlns="http://www.sap.com/apimgmt">
<name>default</name>
<url>http://none.com/</url>
<provider_id>NONE</provider_id>
...
</TargetEndPoint>
Step 3: Click Create.
Step 4: Go to the Resources tab. Add one resource:
| Resource Path | /GenerateToken |
| Methods | All enabled (GET, POST, PUT, DELETE, etc.) |
The resource XML:
<APIResource xmlns="http://www.sap.com/apimgmt">
<name>GenerateToken</name>
<canShowGet>true</canShowGet>
<canShowPost>true</canShowPost>
<canShowPut>true</canShowPut>
<canShowDelete>true</canShowDelete>
<canShowHead>true</canShowHead>
<canShowOption>true</canShowOption>
<canShowPatch>true</canShowPatch>
<resource_path>/GenerateToken</resource_path>
</APIResource>
⚠️All methods are enabled because the OAuth token endpoint needs to accept POST (the standard) but also supports other methods for flexibility.
2.3 Add the GenerateAccessToken policy
This is the key difference from validation. Instead of VerifyAccessToken, we use GenerateAccessToken.
Step 1: Open the OauthService proxy → Policies → Edit.
Step 2: In the conditional flows on the left, you’ll see the GenerateToken flow. Select it.
Step 3: Under Security Policies, click + next to OAuth v2.0.
| Policy Name | Oauthv2 |
| Stream | Incoming Request |
Step 4: Update the XML:
<OAuthV2 async="false" continueOnError="false"
enabled="true" xmlns="http://www.sap.com/apimgmt">
<Operation>GenerateAccessToken</Operation>
<GenerateResponse/>
<SupportedGrantTypes>
<GrantType>client_credentials</GrantType>
</SupportedGrantTypes>
</OAuthV2>
Operation |
GenerateAccessToken |
APIM generates a token (not validates) |
GenerateResponse |
(empty element) | Tells APIM to return the token directly in the response |
GrantType |
client_credentials |
Consumer authenticates with Application Key + Secret |
💡Notice the difference: On
OauthService, the operation isGenerateAccessToken. OnNorthwind_API_V1, it will beVerifyAccessToken. Generate vs Verify — one proxy creates tokens, the other checks them.
Step 5: The policy is placed on the conditional flow for /GenerateToken, not on the PreFlow. Here’s what the proxy endpoint XML looks like:
<conditionalFlows>
<conditionalFlow>
<name>GenerateToken</name>
<request>
<isRequest>true</isRequest>
<steps>
<step>
<policy_name>Oauthv2</policy_name>
<sequence>1</sequence>
</step>
</steps>
</request>
<conditions>
(proxy.pathsuffix MatchesPath "/GenerateToken" ...)
AND (request.verb = "POST" OR request.verb = "GET" ...)
</conditions>
</conditionalFlow>
</conditionalFlows>
Step 6: Update, Save, Deploy.
The main proxy XML for OauthService:
<APIProxy xmlns="http://www.sap.com/apimgmt">
<name>OauthService</name>
<title>OauthService</title>
<isVersioned>false</isVersioned>
<service_code>REST</service_code>
<APIState>Active</APIState>
<policies>
<policy type="RaiseFault">defaultRaiseFaultPolicy</policy>
<policy type="OAuthV2">Oauthv2</policy>
</policies>
</APIProxy>
2.4 Add the Product to OauthService
The consumer’s Application Key and Secret are used as OAuth client_id and client_secret. For this to work, the Application must be subscribed to a Product that includes the OauthService proxy.
Step 1: Go to Engage → select Northwind_Demo_Product → Edit.
Step 2: Add the OauthService proxy alongside Northwind_API_V1.
Step 3: Save and Publish.
Now the same Application (Demo_Test_App) is subscribed to both proxies — it can generate tokens through OauthService and use them on Northwind_API_V1.
2.5 Test token generation in Postman
Step 1: Create a POST request:
POST https://<your-apim-host>/oauth/GenerateToken
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=<your-Application-Key>
&client_secret=<your-Application-Secret>
⚠️The Content-Type must be
application/x-www-form-urlencoded, NOTapplication/json. This is the same gotcha from the Event Mesh series with the XSUAA token endpoint. OAuth token endpoints always expect form-encoded bodies.
Expected response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "BearerToken",
"expires_in": "3599",
"scope": ""
}
🖼️ [Screenshot: Postman showing the token generation response with access_token, token_type, and expires_in]
💡The
client_idandclient_secretare your Application Key and Application Secret from Part 3. APIM validates them against the registered Application, confirms it’s subscribed to a Product that includes theOauthServiceproxy, and generates a token. No external IdP involved.
2.6 Add token validation to Northwind_API_V1
Now we need the Northwind proxy to accept and validate these tokens.
Step 1: Open Northwind_API_V1 → Policies → Edit.
Step 2: Select PreFlow under ProxyEndpoint (Incoming Request).
Step 3: Under Security Policies, click + next to OAuth v2.0.
| Policy Name | ValidateOAuthToken |
| Stream | Incoming Request |
Step 4: Update the XML — notice the operation is VerifyAccessToken:
<OAuthV2 async="false" continueOnError="false"
enabled="true"
xmlns="http://www.sap.com/apimgmt">
<Operation>VerifyAccessToken</Operation>
<SupportedGrantTypes/>
<Tokens/>
</OAuthV2>
Step 5: Update, Save, Deploy.
2.7 Test the full OAuth flow in Postman
Step 1: Generate a token (from section 2.5). Copy the access_token.
Step 2: Call the Northwind proxy with the token:
GET https://<apim-host>/V1/ProxyNorthwindAPI/Customers?$top=3&$format=json
Headers:
Authorization: Bearer <your-access-token>
Expected: 200 OK with Northwind Customers data.
Step 3: Try with a fabricated token:
Authorization: Bearer fake-token-12345
Expected: 401 Unauthorized — InvalidAccessToken
Step 4: Wait for the token to expire (default 3599 seconds = ~1 hour), then retry:
Expected: 401 Unauthorized — access_token_expired
💡OAuth vs API Key — when to use which:
Approach Use when
API Key only Simple integrations, internal consumers, quick setup OAuth only Token-based auth with automatic expiry, more secure Both together API key identifies the Application (analytics/quota), OAuth token authenticates the identity (authorization). Best for production
2.8 What the export structure looks like
You now have two proxies:
OauthService/
├── APIProxy/
│ ├── OauthService.xml ← service_code: REST, no versioning
│ ├── APIProxyEndPoint/default.xml ← base_path: /oauth
│ ├── APITargetEndPoint/default.xml ← url: http://none.com/, provider_id: NONE
│ ├── Policy/Oauthv2.xml ← GenerateAccessToken + client_credentials
│ └── APIResource/GenerateToken.xml ← /GenerateToken, all methods
Northwind_API_V1/
├── APIProxy/
│ ├── Northwind_API_V1.xml
│ ├── Policy/ValidateOAuthToken.xml ← VerifyAccessToken
│ └── ... (75 resources, 9+ policies)
💡This pattern scales beautifully. One
OauthServiceproxy serves tokens for ALL your API proxies — Northwind, S/4HANA, CPI endpoints. You create it once, add it to every Product, and every consumer gets OAuth for free.
2.9 Alternative: External OAuth provider
If your enterprise requires integration with an existing IdP (SAP IAS, Azure Entra ID, Okta), you can skip the OauthService proxy and configure APIM to trust external tokens instead. In that case:
- Configure the OAuth Provider in Configure → API Portal Settings (JWKS URI, token endpoint, etc.)
- The
ValidateOAuthTokenpolicy onNorthwind_API_V1validates tokens from the external provider - The consumer gets tokens from the external IdP, not from APIM
Both approaches use the same VerifyAccessToken policy on the API proxy — the only difference is where the token comes from. For this series (and for most beginner setups), the APIM-native OauthService approach is simpler and self-contained.
2.10 What changed in the Northwind export
A new file appears in the Policy/ folder: ValidateOAuthToken.xml. The main proxy XML now lists three policies:
<policies>
<policy type="RaiseFault">defaultRaiseFaultPolicy</policy>
<policy type="VerifyAPIKey">VerifyAPIKey</policy>
<policy type="OAuthV2">ValidateOAuthToken</policy>
</policies>
3. Key Value Maps — secure credential storage
Our Northwind_API_V1 proxy connects to a public service with no authentication. But in production (S/4HANA, third-party APIs), the backend requires credentials. Key Value Maps (KVM) are APIM’s secure credential store — the equivalent of Security Material in CPI.
3.1 Create a KVM
Step 1: Go to Configure → Key Value Maps.
Step 2: Click Create.
| Name | Backend_Credentials |
| Encrypted | ✅Yes |
Step 3: Add entries:
username |
APIM_COMM_USER |
password |
<your-communication-user-password> |
Step 4: Click Save.
🔒Encrypted KVMs mask values after save. You can’t read them back — only overwrite. This is the right way to handle credentials, not hardcoding them in the API Provider or in policy XML.
🖼️ [Screenshot: KVM creation with Backend_Credentials name and two encrypted entries]
3.2 Read KVM values in a policy
Where: TargetEndpoint → PreFlow → Incoming Request
⚠️Why TargetEndpoint? Look at our exported Target Endpoint — it has
provider_id=Northwind_APIandrelativePath=/Northwind/Northwind.svc/. These credentials are for authenticating to that backend, not for the consumer. Consumer auth (API key, OAuth) lives on ProxyEndpoint. Backend auth lives on TargetEndpoint. Same credential direction principle from Event Mesh — who the credentials belong to determines where they go.
Step 1: In the Policy Editor, select PreFlow under TargetEndpoint (Incoming Request).
Step 2: Under Mediation Policies, click + next to Key Value Map Operations.
| Policy Name | KVM-GetCredentials |
| Stream | Incoming Request |
Step 3: Update the XML:
<KeyValueMapOperations mapIdentifier="Backend_Credentials"
async="true" continueOnError="false"
enabled="true"
xmlns="http://www.sap.com/apimgmt">
<Get assignTo="private.backend.username" index="1">
<Key><Parameter>username</Parameter></Key>
</Get>
<Get assignTo="private.backend.password" index="1">
<Key><Parameter>password</Parameter></Key>
</Get>
<Scope>environment</Scope>
</KeyValueMapOperations>
mapIdentifier="Backend_Credentials" |
References the KVM we created |
assignTo="private.backend.username" |
Stores the value in a flow variable |
Scope |
environment — accessible across all proxies |
💡The
private.prefix is critical. Variables namedprivate.xxxare automatically excluded from debug traces and analytics logs. Without it, your credentials appear in plain text during debugging. Always useprivate.for sensitive values.
3.3 Inject Basic Auth header
Add a Basic Authentication policy right after the KVM policy on TargetEndpoint PreFlow:
| Policy Name | InjectBasicAuth |
| Stream | Incoming Request |
<BasicAuthentication async="true" continueOnError="false"
enabled="true"
xmlns="http://www.sap.com/apimgmt">
<Operation>Encode</Operation>
<IgnoreUnresolvedVariables>false</IgnoreUnresolvedVariables>
<User ref="private.backend.username"/>
<Password ref="private.backend.password"/>
<AssignTo createNew="true">request.header.Authorization</AssignTo>
</BasicAuthentication>
Operation: Encode |
Base64-encodes username:password into Authorization: Basic xxx |
User ref / Password ref |
Reads from the flow variables set by the KVM policy |
AssignTo |
Writes the encoded value into the outbound Authorization header |
Step 4: Update, Save, Deploy.
💡What this achieves: The consumer sends only an API key (and optionally an OAuth token). APIM internally reads the backend credentials from the KVM and injects the Authorization header before calling the backend. The consumer never sees or handles backend credentials. This decoupling of consumer identity from backend identity is fundamental.
⚠️For Northwind specifically: Since Northwind is a public service, these backend credentials aren’t actually needed — Northwind accepts unauthenticated requests. But the pattern is identical for S/4HANA, where the Communication User credentials in the KVM authenticate against the Communication Arrangement. We’re building the pattern here so it’s ready when you swap the backend.
🖼️ [Screenshot: TargetEndpoint PreFlow with KVM-GetCredentials and InjectBasicAuth policies]
4. Spike Arrest — throttle traffic bursts
Where: ProxyEndpoint → PreFlow → Incoming Request (after authentication policies)
| Policy Name | SpikeArrest |
| Stream | Incoming Request |
<SpikeArrest async="true" continueOnError="false"
enabled="true"
xmlns="http://www.sap.com/apimgmt">
<Identifier ref="request.header.APIKey"/>
<Rate>12pm</Rate>
<UseEffectiveCount>true</UseEffectiveCount>
</SpikeArrest>
Identifier |
request.header.APIKey |
Throttle per Application — each consumer gets their own spike limit |
Rate |
12pm |
12 per minute |
UseEffectiveCount |
true |
Count across all APIM runtime nodes |
⚠️Understanding the smoothing:
12pmdoes NOT mean “allow 12 calls then block until the minute resets.” APIM distributes evenly: 12/min = one every 5 seconds. If two requests arrive within 1 second, the second is rejected — even if only 2 calls have happened the whole minute. This catches spikes, not aggregate overuse. For aggregate limits, use Quota (next section).
Testing in Postman:
Send 3 rapid requests to /V1/ProxyNorthwindAPI/Customers?$top=3&$format=json (with your API key). Click Send as fast as you can.
- Request 1:
200 OK - Request 2 or 3:
{
"fault": {
"faultstring": "Spike arrest violation. Allowed rate: 12pm",
"detail": {
"errorcode": "policies.ratelimit.SpikeArrestViolation"
}
}
}
HTTP Status: 429 Too Many Requests
Wait 5 seconds, send again — 200 OK.
💡For production, set this higher.
12pmis intentionally low for easy testing. Real-world values:30ps(30 per second) or1000pmdepending on backend capacity.
🖼️ [Screenshot: Postman showing the 429 SpikeArrestViolation response]
5. Quota — cap total API calls
While Spike Arrest handles bursts, Quota controls the total over a longer period.
Where: ProxyEndpoint → PreFlow → Incoming Request (after SpikeArrest)
| Policy Name | QuotaLimit |
| Stream | Incoming Request |
<Quota async="true" continueOnError="false"
enabled="true" type="calendar"
xmlns="http://www.sap.com/apimgmt">
<Identifier ref="request.header.APIKey"/>
<Allow countRef="apiproduct.developer.quota.limit" count="1000"/>
<Interval ref="apiproduct.developer.quota.interval">1</Interval>
<Distributed>true</Distributed>
<StartTime>2025-01-01 00:00:00</StartTime>
<Synchronous>true</Synchronous>
<TimeUnit ref="apiproduct.developer.quota.timeunit">day</TimeUnit>
</Quota>
⚠️Element order matters in APIM policy XML. The schema enforces a strict sequence:
Allow → Interval → Distributed → StartTime → Synchronous → TimeUnit. If you putTimeUnitbeforeStartTime, you’ll get:Invalid content was found starting with element 'StartTime'. No child element is expected at this point.This is one of those errors where the XML looks correct but the parser rejects it — the elements are all valid, just in the wrong order.
countRef="apiproduct.developer.quota.limit" |
Reads the limit from the Product’s Quota settings — different Products can have different limits |
count="1000" |
Fallback if the Product doesn’t define a quota |
TimeUnit: day |
Counter resets daily at midnight |
Distributed: true |
Count across all APIM runtime nodes |
💡Spike Arrest vs Quota — the quick rule:
- Spike Arrest = speed limit (requests per second/minute) — prevents floods
- Quota = data plan (total calls per day/month) — prevents overuse
You almost always want both.
Testing: Temporarily set count="5", redeploy, and send 6 requests:
- Requests 1–5:
200 OK - Request 6:
429 QuotaViolation
⚠️Remember to reset to
count="1000"after testing.
Dynamic Quota from the Product:
The countRef attribute makes quotas dynamic. To configure it on the Product side:
- Go to Engage → select
Northwind_Demo_Product→ Edit - Set Calls:
5000, Interval:1, Time Unit:Day - Save and Publish
Now a “Free” Product can have 100 calls/day and a “Premium” Product 10,000 — same proxy, same policy, different limits.
6. JSON Threat Protection — block malicious payloads
For APIs that accept POST or PUT (our Northwind_API_V1 supports POST on collections and PUT on single entities), protect against oversized or deeply nested payloads.
Where: ProxyEndpoint → PreFlow → Incoming Request (after VerifyAPIKey, before SpikeArrest)
| Policy Name | JSONThreatProtection |
| Stream | Incoming Request |
<JSONThreatProtection async="true" continueOnError="false"
enabled="true"
xmlns="http://www.sap.com/apimgmt">
<Source>request</Source>
<ArrayElementCount>50</ArrayElementCount>
<ContainerDepth>10</ContainerDepth>
<ObjectEntryCount>50</ObjectEntryCount>
<ObjectEntryNameLength>128</ObjectEntryNameLength>
<StringValueLength>5000</StringValueLength>
</JSONThreatProtection>
⚠️Element order matters here too.
<Source>must come first — before any of the limit elements. If you put it last, you’ll get:Invalid content was found starting with element 'StringValueLength'. One of 'Source' is expected.This is the same strict-ordering pattern we saw with the Quota policy — always check the policy template for the correct element sequence.
ArrayElementCount |
50 | Arrays with 50+ items (memory-bomb payloads) |
ContainerDepth |
10 | Nesting deeper than 10 levels (stack overflow attacks) |
ObjectEntryCount |
50 | Objects with 50+ keys |
ObjectEntryNameLength |
128 | Key names longer than 128 characters |
StringValueLength |
5000 | Strings longer than 5,000 characters (content injection) |
Source |
request |
Apply to the incoming request body only |
Testing in Postman:
Create a POST request to /V1/ProxyNorthwindAPI/Customers with an 11-level deep JSON body:
{
"a": { "b": { "c": { "d": { "e": { "f": { "g": { "h": { "i": { "j": { "k": "too deep" } } } } } } } } } }
}
Expected: 400 Bad Request
{
"fault": {
"faultstring": "JSONThreatProtection[JSONThreatProtection]: Exceeded container depth...",
"detail": {
"errorcode": "steps.jsonthreatprotection.ExecutionFailed"
}
}
}
💡This works together with the conditional flows from Part 2. The conditional flow for
Customers(collection) allows POST. If the POST passes the conditional flow check but the JSON body is malicious, JSONThreatProtection catches it. Two layers: method enforcement + payload validation.
🖼️ [Screenshot: Postman showing the 400 JSONThreatProtection violation]
7. CORS — enable browser-based consumers
If a web application (SAP Build Apps, Fiori, React) calls your /V1/ProxyNorthwindAPI endpoint from a browser, it will fail with a CORS error. Browsers enforce the Same-Origin Policy — they block requests to different domains unless the response includes Access-Control-* headers.
Where: ProxyEndpoint → PostFlow → Outgoing Response
⚠️Why PostFlow Outgoing Response? CORS headers go on the response, not the request. And on the ProxyEndpoint (consumer-facing), because the browser’s CORS check happens between the consumer and APIM — not between APIM and Northwind.
| Policy Name | AddCORSHeaders |
| Stream | Outgoing Response |
<AssignMessage async="false" continueOnError="false"
enabled="true"
xmlns="http://www.sap.com/apimgmt">
<Set>
<Headers>
<Header name="Access-Control-Allow-Origin">*</Header>
<Header name="Access-Control-Allow-Methods">GET, POST, PUT, DELETE, OPTIONS</Header>
<Header name="Access-Control-Allow-Headers">APIKey, Content-Type, Authorization</Header>
<Header name="Access-Control-Max-Age">3600</Header>
</Headers>
</Set>
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
<AssignTo createNew="false" type="response"/>
</AssignMessage>
Allow-Origin: * |
Allow any domain. In production, restrict to specific origins like https://myapp.launchpad.cfapps.us10.hana.ondemand.com |
Allow-Headers: APIKey, ... |
APIKey must be listed here — otherwise the browser strips it from the request |
Max-Age: 3600 |
Cache the preflight response for 1 hour |
⚠️OPTIONS preflight: Browsers send a preflight OPTIONS request before the real call. If
VerifyAPIKeyruns on OPTIONS too, it’ll reject the preflight (which has no API key). Use a conditional flow to skip key verification for OPTIONS requests — add the condition(request.verb != "OPTIONS")around the VerifyAPIKey policy step.
Step 5: Update, Save, Deploy.
🖼️ [Screenshot: Policy Editor showing AddCORSHeaders on ProxyEndpoint PostFlow (Outgoing Response)]
8. The complete policy lineup — what the export looks like now
After this post, your proxy has eight policies. Here’s the full picture:
8.1 Policy summary table
| 1 | VerifyAPIKey |
ProxyEndpoint PreFlow | Incoming Request | Identify the consumer (Application) |
| 2 | ValidateOAuthToken |
ProxyEndpoint PreFlow | Incoming Request | Authenticate the consumer (identity) |
| 3 | JSONThreatProtection |
ProxyEndpoint PreFlow | Incoming Request | Block malicious payloads |
| 4 | SpikeArrest |
ProxyEndpoint PreFlow | Incoming Request | Throttle traffic bursts |
| 5 | QuotaLimit |
ProxyEndpoint PreFlow | Incoming Request | Cap daily API calls |
| 6 | AddCORSHeaders |
ProxyEndpoint PostFlow | Outgoing Response | Enable browser access |
| 7 | KVM-GetCredentials |
TargetEndpoint PreFlow | Incoming Request | Read backend credentials |
| 8 | InjectBasicAuth |
TargetEndpoint PreFlow | Incoming Request | Add Authorization header to backend call |
Plus the auto-generated defaultRaiseFaultPolicy on the DefaultFaultFlow.
8.2 What the export’s Policy folder looks like
APIProxy/
├── Policy/
│ ├── defaultRaiseFaultPolicy.xml ← auto-generated (Part 2)
│ ├── VerifyAPIKey.xml ← added in Part 3
│ ├── ValidateOAuthToken.xml ← added in this post
│ ├── JSONThreatProtection.xml ← added in this post
│ ├── SpikeArrest.xml ← added in this post
│ ├── QuotaLimit.xml ← added in this post
│ ├── AddCORSHeaders.xml ← added in this post
│ ├── KVM-GetCredentials.xml ← added in this post
│ └── InjectBasicAuth.xml ← added in this post
8.3 The main proxy XML’s policies section
<policies>
<policy type="RaiseFault">defaultRaiseFaultPolicy</policy>
<policy type="VerifyAPIKey">VerifyAPIKey</policy>
<policy type="OAuthV2">ValidateOAuthToken</policy>
<policy type="JSONThreatProtection">JSONThreatProtection</policy>
<policy type="SpikeArrest">SpikeArrest</policy>
<policy type="Quota">QuotaLimit</policy>
<policy type="AssignMessage">AddCORSHeaders</policy>
<policy type="KeyValueMapOperations">KVM-GetCredentials</policy>
<policy type="BasicAuthentication">InjectBasicAuth</policy>
</policies>
💡This is your proxy’s bill of materials. When you export the zip and check it into Git, you can see every policy, every placement, and every configuration. When a colleague asks “what governance does this proxy have?” — this list is the answer.
⚠️Order matters within a PreFlow. The
<sequence>numbers determine execution order. If you put SpikeArrest (sequence 4) before VerifyAPIKey (sequence 1), you’d count rate limits for unauthorized requests — wasting your spike budget on junk traffic. Always: authenticate → validate → throttle → route.
9. Update your Policy Template
In Part 3, we created Baseline_Security_Template with just VerifyAPIKey. Let’s expand it to the full baseline:
Step 1: Go to Develop → Policy Templates → open Baseline_Security_Template.
Step 2: Add these policies in order on ProxyEndpoint PreFlow (Incoming Request):
VerifyAPIKey(already there)JSONThreatProtectionSpikeArrest(Rate:30ps— more realistic than12pm)QuotaLimit(withcountReffor dynamic Product limits)
Step 3: Add on ProxyEndpoint PostFlow (Outgoing Response): 5. AddCORSHeaders
Step 4: Save.
Now every new proxy gets the full security + traffic + CORS baseline in one click. Five policies, correct placement, sensible defaults.
💡SAP recommends a baseline template for all proxies: Verify API Key + JSON/XML Threat Protection + Regular Expression Protection + Spike Arrest + Quota. Our template covers four of five — add Regular Expression Protection if your APIs accept user input in URLs or headers.
10. Troubleshooting reference
FailedToResolveAPIKey |
401 | VerifyAPIKey | Consumer didn’t send the APIKey header |
InvalidApiKey |
401 | VerifyAPIKey | Wrong key, expired, or not subscribed to the Product |
InvalidAccessToken |
401 | OAuth v2.0 | Token is invalid, expired, or from an untrusted issuer |
access_token_expired |
401 | OAuth v2.0 | Token has expired — consumer needs to refresh |
SpikeArrestViolation |
429 | SpikeArrest | Too many requests too fast — space them out or raise the rate |
QuotaViolation |
429 | Quota | Daily limit hit — upgrade Product tier or wait for midnight reset |
JSONThreatProtection: Exceeded... |
400 | JSONThreatProtection | Payload too deep/large — consumer must simplify |
UnresolvedVariable: private.backend.username |
500 | KVM-GetCredentials | KVM name mismatch (mapIdentifier doesn’t match the KVM name) or key doesn’t exist |
BasicAuthentication: Unable to Encode |
500 | InjectBasicAuth | KVM values are empty or variable names don’t match between KVM and BasicAuth policies |
Invalid content was found starting with element... |
(save error) | Any policy | XML element order is wrong — APIM enforces strict element sequence. Check the policy template for the correct order |
Client identifier is required |
401 | OauthService | client_id and client_secret must be in the body (x-www-form-urlencoded), not in headers |
Unresolved variable: private.backend.username |
500 | KVM-GetCredentials | KVM not created, or mapIdentifier doesn’t match the KVM name. For Northwind (no auth), remove KVM + BasicAuth policies from TargetEndpoint entirely |
💡Use the Debug tool (Part 3, section 😎 to pinpoint which policy failed. The trace shows each policy step in sequence — you see exactly where the flow stopped and what variables were set. When
KVM-GetCredentialssucceeds butInjectBasicAuthfails, it means the variable names don’t match between the two policies.
Quick Reference
| OAuth 2.0 | ProxyEndpoint PreFlow · <Operation>VerifyAccessToken</Operation> |
| Token Content-Type | application/x-www-form-urlencoded (NOT JSON) |
| KVM | Configure → Key Value Maps → Create (Encrypted) · private. prefix for variables |
| Basic Auth (backend) | TargetEndpoint PreFlow · reads from KVM · writes to request.header.Authorization |
| Spike Arrest | ProxyEndpoint PreFlow · <Rate>12pm</Rate> (testing) or <Rate>30ps</Rate> (production) |
| Quota | ProxyEndpoint PreFlow · countRef="apiproduct.developer.quota.limit" for dynamic limits |
| JSON Threat Protection | ProxyEndpoint PreFlow · limits depth (10), arrays (50), strings (5000) |
| CORS | ProxyEndpoint PostFlow (Outgoing Response) · AssignMessage with Access-Control-* headers |
| Export structure | 9 policy files in Policy/ folder after this post |
What’s next
In Part 5: Advanced Topics — Routing, Path Removal, Developer Hub, Analytics & MCP Gateway, we tackle the patterns that separate beginners from practitioners. We’ll route requests to different backends using policies, rewrite URL paths for clean consumer-facing URLs, walk through the Developer Hub from a consumer’s perspective, set up Analytics dashboards with custom metrics via Statistics Collector, and close the series with the brand-new MCP Gateway for AI agents.
👉Part 5: Advanced Topics — coming next.
“}]]
Read More Technology Blog Posts by Members articles
#abap