Difficulty: Advanced
TL;DR
Problem: Most Fusion-to-OIC calling patterns we see in the field — including two we’ve documented ourselves — either authenticate with a bare username/password (a BIP HTTP connection, which also means the OIC log shows a generic service user, never the real person) or bolt a manual OAuth 2.0 Client Credentials hop onto Groovy: fetch a token from the IDCS/IAM token endpoint, attach it as a bearer header, then make the actual OIC call. That’s extra code, an extra credential key, and still no real user identity in OIC’s own log.
Stack: OCI IAM/IDCS Integrated Application (Confidential, Trusted, SAML2 assertion grant, registered certificate) + a single Application Composer REST Web Service Connection per environment using the native Call using IDCS OAUTH authentication scheme + one reusable Groovy Global Function.
Read this if you’re building VB AppUI or Redwood extensions — or any Groovy trigger — that needs to call OIC synchronously, and you’re tired of token plumbing or of OIC’s execution log showing a service account instead of the person who actually clicked the button.
Tested on / Prerequisites
Tested on Fusion 26B · OCI region eu-frankfurt-1 · OIC Gen 3, both v1 (standalone) and v2 (project-managed) integrations · Fusion and OIC in the same OCI identity domain · Application Composer enabled · a role that can create Custom Objects, Global Functions, and Web Service Connections · an OCI IAM/IDCS Integrated Application (Confidential).
Status note, in the interest of not overselling this: this pattern is proven end-to-end in a single environment as of this writing. Test/prod replication, and whether the native connection caches or refreshes its own token internally, are open items — see Gotchas below.
Related patterns on this blog
This post doesn’t extend one of our existing numbered chains — it sits alongside two of them, solving a problem neither one covers:
- If you don’t need a synchronous response, our Custom Object Integration Events pattern already eliminates URL and credential management for async Fusion → OIC calls. This post is the synchronous counterpart.
- For the opposite direction — OIC calling back into Fusion/OTBI — see Fusion OTBI: Securely obtain a session ID from OIC. Different mechanism, because OIC, not Fusion, is the caller there.
- The JSON parser in this post replaces the lenient one we published in Overcome newly introduced limitation of using SQL inside OM Extensions by using BIP and OIC — see the callout under Implementation Details.
(Suggest treating this as the head of a new chain — “Sync OIC from Fusion via native IDCS OAuth” — going forward.)
Business Challenge
Most of our Fusion engagements eventually need to call an OIC integration from inside Fusion itself — a VB AppUI action, a Redwood page extension, a Groovy trigger — to run business logic or hand off to an external system. We’ve covered pieces of this before: an internal BIP HTTP connection through an ESS job for the asynchronous case, and the fa-internal SAML pattern for calling Fusion’s own internal endpoints. But calling OIC itself synchronously is a different problem, and across the environments we’ve reviewed it consistently comes down to one of two approaches:
The two patterns we keep seeing
- A BIP-based HTTP connection to OIC. It supports username/password only. It works, but the OIC integration’s own execution history always shows a generic service user — never the person who actually triggered it from Fusion.
- An Application Composer REST Web Service Connection authenticated with OAuth 2.0 Client Credentials. This is what we see in the majority of customer environments. It requires a manual token-fetch hop in Groovy — call the IDCS/IAM token endpoint, then attach the bearer header to the actual OIC call — plus a second connection or a CSF credential key to hold the client secret. And a client-credentials token still carries only the client’s identity, never a person’s.
What’s different here
In this environment, Fusion and OIC share the same OCI identity domain — meaning a Fusion user, if granted access, can reach OIC directly. So instead of any token hop, we built a single Application Composer REST Web Service Connection per environment using the connection’s own native “Call using IDCS OAUTH” authentication scheme, backed on the OCI side by an Integrated Application configured with a SAML2 assertion grant and a registered Oracle certificate. The connection authenticates itself — there is no OAuth code in Groovy at all. And because of the SAML2 assertion, OIC’s own execution history shows the real Fusion user who triggered the call, not a generic identity.
One important nuance, confirmed by testing rather than assumed: this propagation covers exactly one hop. OIC’s own outbound call back into Fusion/ERP (or anywhere else) still authenticates as whatever identity that specific connection is configured with — propagation doesn’t continue automatically further down the chain. See Gotchas for what we tried here and why we stopped.
Built once, reusable everywhere
We built the engine as a Global Function precisely so it isn’t scoped to one AppUI: any Groovy in the instance — a VB AppUI action, a Redwood extension, a field trigger, another team’s custom object — gets synchronous, user-attributed OIC access for free, with no token code to write or maintain.
Solution Overview
- OCI IAM/IDCS: one Confidential Integrated Application per environment, marked Trusted, with a registered public certificate, and all four grant types enabled together — Client credentials, JWT assertion, Refresh token, and SAML2 assertion.
- Application Composer: one REST Web Service Connection per environment (
Oic<Env>WsRest), authenticated with Call using IDCS OAUTH against the application above. One connection covers every OIC integration in that environment, v1 or v2. - A parseJson Global Function — needed because a REST-callable object function can only declare String parameters, so the JSON payload has to cross that boundary as a string and be parsed back into a real Groovy object afterward.
- A callOIC Global Function — the engine. It detects the current environment, splits the caller’s relative path, selects that environment’s connection, and issues the GET or POST.
- A thin row-level callOIC delegate and a REST-callable object-level callOIC entry point on a small “REST Utility” custom object — this is what VB AppUI and Redwood extensions actually call over REST.
- Any other Groovy in the instance skips the REST hop entirely and calls
adf.util.callOIC(...)directly with a real object payload — no JSON string, no parsing.
Implementation Details
1. OCI IAM/IDCS setup
Create one Confidential Integrated Application per environment (dev, test, prod), scoped to that environment’s OIC instance:
- Client Type: Confidential — “Configure this application as a client now.”
- Allowed grant types: Client credentials, JWT assertion, Refresh token, and SAML2 assertion. All four together, confirmed by testing — client credentials alone reproduces the failure described below.
- Resources: the target OIC instance, with the integration REST API scope.
- Note the Client ID, Client Secret, and Scope — these go on the Fusion connection in step 2, never into Groovy source.
In addition, the application must be marked Trusted, and its Oracle-issued public certificate registered from Fusion’s Security Console → API Authentication. Both this trust step and the SAML2 assertion grant type were required — neither alone was sufficient, and we haven’t found this exact combination spelled out on Oracle’s public documentation pages. Treat it as an empirically-derived recipe, not a fully explained one, and re-verify it the first time it’s set up in test or prod.
The sequence we saw while getting this working (useful if you hit the same wall):
- Client credentials grant only, no trust/certificate →
invalid_client. - Application marked Trusted + certificate registered →
invalid_grant. - SAML2 assertion added to the allowed grant types (alongside the other three) → succeeded.
Once working, OIC’s own process execution history shows the integration running as the actual Fusion user who triggered the call — the expected signature of a SAML2 Bearer Assertion / User Assertion flow, consistent with Oracle’s documented OAuth identity propagation pattern for these adapters, even though here the propagation runs the other direction (Fusion into OIC) via the connection’s native scheme rather than an adapter security policy.
Scope format that worked (no space between :443 and urn: — confirmed against a working example, not a typo):
https://<OIC-instance-audience-id>.integration.<region>.ocp.oraclecloud.com:443urn:opc:resource:consumer::all
2. The single REST Web Service Connection per environment
One connection per environment now carries the whole call — no separate token connection. This depends on step 1 already being in place; without the Trusted application, registered certificate, and SAML2 assertion grant, the same connection configuration returns invalid_client / invalid_grant instead of working.
| Connection | URL | Auth scheme |
|---|---|---|
Oic<Env>WsRest | https://<api-gateway-or-oic-host>/ic/api/integration/##c1##/.../##c10## | Call using IDCS OAUTH — Client ID, Client Secret, and Scope set directly on the connection |
The URL uses 10 positional placeholders (##c1## … ##c10##) after the fixed /ic/api/integration/ segment — every placeholder is a single path segment, none may contain a literal /. The Global Function below fills them differently for v1 vs v2:
| Call type | Slot assignment |
|---|---|
| v1 (non-project) | c1=v1, c2=flows, c3=rest, c4={INTEGRATION}, c5={VERSION}, c6–c10=resourcePath segments |
| v2 (project) | c1=v2, c2=flows, c3=rest, c4=project, c5={PROJECT}, c6={INTEGRATION}, c7={VERSION}, c8–c10=resourcePath segments |

3. Global Function: parseJson
Why we changed this: an earlier post (Overcome newly introduced limitation of using SQL inside OM Extensions) used a lenient scan-and-extract JSON parser. The version below is a strict, grammar-validating recursive-descent parser (RFC 8259) — it enforces required commas and colons, rejects trailing commas and invalid tokens, supports a bare scalar as the JSON root, and validates Unicode surrogate pairs. This is now our default; treat the lenient version as superseded.
A REST-callable object function can only declare String (and other primitive) parameters — an Object-typed parameter isn’t supported on the external contract, confirmed by testing. Since our payload is genuinely a JSON object, it has to cross that boundary as a raw string and be parsed into a real Groovy Map/List afterward. groovy.json.JsonSlurper is not available in Application Composer Groovy, so this is hand-rolled.
One typing note worth flagging for anyone consuming its output: integers return as Long (or BigInteger if they overflow 64 bits); decimals and exponents return as BigDecimal. Don’t assume a single uniform numeric type.
// Strict, grammar-validating recursive-descent JSON parser (RFC 8259).
// Global Function: parseJson(jsonText: String) -> Object
if (jsonText == null) {
throw new oracle.jbo.JboException("parseJson: JSON input cannot be null")
}
def source = jsonText
int sourceLength = source.length()
def cursor = [0]
def fail = { String message ->
throw new oracle.jbo.JboException("parseJson: invalid JSON at position " + cursor[0] + ": " + message)
}
def isWhitespace = { c -> c == ' ' || c == '\t' || c == '\r' || c == '\n' }
def skipWhitespace = {
while (cursor[0] < sourceLength && isWhitespace(source.charAt(cursor[0]))) { cursor[0]++ }
}
def hexValue = { c ->
if (c >= '0' && c <= '9') return ((int)c) - ((int)'0')
if (c >= 'a' && c <= 'f') return 10 + ((int)c) - ((int)'a')
if (c >= 'A' && c <= 'F') return 10 + ((int)c) - ((int)'A')
return -1
}
def readUnicodeEscape = {
if (cursor[0] + 4 > sourceLength) { fail("Incomplete Unicode escape") }
int value = 0
for (int digitIndex = 0; digitIndex < 4; digitIndex++) {
int digit = hexValue(source.charAt(cursor[0] + digitIndex))
if (digit < 0) fail("Invalid Unicode escape")
value = value * 16 + digit
}
cursor[0] += 4
return value
}
def parseString = {
if (cursor[0] >= sourceLength || source.charAt(cursor[0]) != '"') { fail("Expected a string") }
cursor[0]++
def output = new StringBuilder()
while (cursor[0] < sourceLength) {
def current = source.charAt(cursor[0])
cursor[0]++
if (current == '"') return output.toString()
if (((int)current) < 32) { fail("Unescaped control character in string") }
if (current != '\\') {
output.append(current)
} else {
if (cursor[0] >= sourceLength) fail("Incomplete escape sequence")
def escaped = source.charAt(cursor[0])
cursor[0]++
switch (escaped) {
case '"': output.append('"'); break
case '\\': output.append('\\'); break
case '/': output.append('/'); break
case 'b': output.append('\b'); break
case 'f': output.append('\f'); break
case 'n': output.append('\n'); break
case 'r': output.append('\r'); break
case 't': output.append('\t'); break
case 'u':
int firstUnit = readUnicodeEscape()
if (firstUnit >= 0xD800 && firstUnit <= 0xDBFF) {
if (cursor[0] + 6 > sourceLength || source.charAt(cursor[0]) != '\\' || source.charAt(cursor[0] + 1) != 'u') {
fail("High Unicode surrogate must be followed by a low surrogate")
}
cursor[0] += 2
int secondUnit = readUnicodeEscape()
if (secondUnit < 0xDC00 || secondUnit > 0xDFFF) { fail("Invalid low Unicode surrogate") }
output.append((char)firstUnit)
output.append((char)secondUnit)
} else if (firstUnit >= 0xDC00 && firstUnit <= 0xDFFF) {
fail("Unexpected low Unicode surrogate")
} else {
output.append((char)firstUnit)
}
break
default:
fail("Invalid escape sequence: \\" + escaped)
}
}
}
fail("Unterminated string")
}
def parseNumber = {
int start = cursor[0]
if (cursor[0] < sourceLength && source.charAt(cursor[0]) == '-') { cursor[0]++ }
if (cursor[0] >= sourceLength) fail("Incomplete number")
def firstDigit = source.charAt(cursor[0])
if (firstDigit == '0') {
cursor[0]++
if (cursor[0] < sourceLength) {
def following = source.charAt(cursor[0])
if (following >= '0' && following <= '9') { fail("Leading zero is not allowed") }
}
} else if (firstDigit >= '1' && firstDigit <= '9') {
while (cursor[0] < sourceLength) {
def digit = source.charAt(cursor[0])
if (digit < '0' || digit > '9') break
cursor[0]++
}
} else {
fail("Invalid number")
}
boolean hasFractionOrExponent = false
if (cursor[0] < sourceLength && source.charAt(cursor[0]) == '.') {
hasFractionOrExponent = true
cursor[0]++
int fractionStart = cursor[0]
while (cursor[0] < sourceLength) {
def digit = source.charAt(cursor[0])
if (digit < '0' || digit > '9') break
cursor[0]++
}
if (cursor[0] == fractionStart) { fail("Fraction requires at least one digit") }
}
if (cursor[0] < sourceLength && (source.charAt(cursor[0]) == 'e' || source.charAt(cursor[0]) == 'E')) {
hasFractionOrExponent = true
cursor[0]++
if (cursor[0] < sourceLength && (source.charAt(cursor[0]) == '+' || source.charAt(cursor[0]) == '-')) { cursor[0]++ }
int exponentStart = cursor[0]
while (cursor[0] < sourceLength) {
def digit = source.charAt(cursor[0])
if (digit < '0' || digit > '9') break
cursor[0]++
}
if (cursor[0] == exponentStart) { fail("Exponent requires at least one digit") }
}
def numberText = source.substring(start, cursor[0])
if (hasFractionOrExponent) return new BigDecimal(numberText)
def integerValue = new BigInteger(numberText)
def minimumLong = new BigInteger("-9223372036854775808")
def maximumLong = new BigInteger("9223372036854775807")
if (integerValue.compareTo(minimumLong) >= 0 && integerValue.compareTo(maximumLong) <= 0) {
return Long.parseLong(numberText)
}
return integerValue
}
def consumeLiteral = { String expected, value ->
int end = cursor[0] + expected.length()
if (end > sourceLength || source.substring(cursor[0], end) != expected) { fail("Expected " + expected) }
cursor[0] = end
return value
}
def parseValue
def parseArray
def parseObject
parseArray = { int depth ->
if (depth > 100) fail("Maximum nesting depth exceeded")
def result = []
cursor[0]++
skipWhitespace()
if (cursor[0] < sourceLength && source.charAt(cursor[0]) == ']') { cursor[0]++; return result }
while (true) {
result.add(parseValue(depth))
skipWhitespace()
if (cursor[0] >= sourceLength) fail("Unterminated array")
def separator = source.charAt(cursor[0])
cursor[0]++
if (separator == ']') return result
if (separator != ',') fail("Expected ',' or ']'")
skipWhitespace()
if (cursor[0] < sourceLength && source.charAt(cursor[0]) == ']') { fail("Trailing comma in array") }
}
}
parseObject = { int depth ->
if (depth > 100) fail("Maximum nesting depth exceeded")
def result = [:]
cursor[0]++
skipWhitespace()
if (cursor[0] < sourceLength && source.charAt(cursor[0]) == '}') { cursor[0]++; return result }
while (true) {
if (cursor[0] >= sourceLength || source.charAt(cursor[0]) != '"') { fail("Object key must be a quoted string") }
def key = parseString()
skipWhitespace()
if (cursor[0] >= sourceLength || source.charAt(cursor[0]) != ':') { fail("Expected ':' after object key") }
cursor[0]++
skipWhitespace()
result.put(key, parseValue(depth))
skipWhitespace()
if (cursor[0] >= sourceLength) fail("Unterminated object")
def separator = source.charAt(cursor[0])
cursor[0]++
if (separator == '}') return result
if (separator != ',') fail("Expected ',' or '}'")
skipWhitespace()
if (cursor[0] < sourceLength && source.charAt(cursor[0]) == '}') { fail("Trailing comma in object") }
}
}
parseValue = { int depth ->
if (depth > 100) fail("Maximum nesting depth exceeded")
skipWhitespace()
if (cursor[0] >= sourceLength) fail("Expected a JSON value")
def current = source.charAt(cursor[0])
if (current == '{') return parseObject(depth + 1)
if (current == '[') return parseArray(depth + 1)
if (current == '"') return parseString()
if (current == 't') return consumeLiteral('true', true)
if (current == 'f') return consumeLiteral('false', false)
if (current == 'n') return consumeLiteral('null', null)
if (current == '-' || (current >= '0' && current <= '9')) return parseNumber()
fail("Unexpected character '" + current + "'")
}
skipWhitespace()
if (cursor[0] >= sourceLength) fail("JSON input is empty")
def parsedValue = parseValue(0)
skipWhitespace()
if (cursor[0] != sourceLength) { fail("Unexpected content after JSON value") }
return parsedValue4. Global Function: callOIC — the engine
Parameters: relativePath (String), method (String), payload (Object). Return type: Object — the raw parsed response is returned directly, not string-converted, so Fusion’s REST framework serializes it as real JSON instead of Groovy’s Map.toString() representation.
Because this is a Global Function, any other Groovy script in the instance can call adf.util.callOIC(relativePath, method, payload) directly with a real payload — no REST hop, no duplicated logic, and no OAuth code, because the connection it selects authenticates itself natively.
def pod = oracle.topologyManager.client.deployedInfo.DeployedInfoProvider.getEndPoint('ORA_CRM_UIAPP')
def env = 'UNKNOWN'
if (pod?.contains('dev1')) {
env = 'DEV1'
} else if (pod?.contains('test')) {
env = 'TEST'
} else {
env = 'PROD' // [Unverified] confirm real pod values in test/prod before trusting this fallback
}
def rawPath = relativePath ?: ""
def pathPart = rawPath
def queryString = null
def qIdx = rawPath.indexOf('?')
if (qIdx >= 0) {
pathPart = rawPath.substring(0, qIdx)
queryString = rawPath.substring(qIdx + 1)
}
def segments = pathPart.tokenize('/')
def isProject = (segments.size() > 0 && segments[0] == 'project')
def slots = ["", "", "", "", "", "", "", "", "", ""]
int i = 0
if (isProject) {
slots[i++] = 'v2'; slots[i++] = 'flows'; slots[i++] = 'rest'; slots[i++] = 'project'
for (int s = 1; s < segments.size() && i < slots.size(); s++) { slots[i++] = segments[s] }
} else {
slots[i++] = 'v1'; slots[i++] = 'flows'; slots[i++] = 'rest'
for (int s = 0; s < segments.size() && i < slots.size(); s++) { slots[i++] = segments[s] }
}
def queryParams = [:]
if (queryString) {
queryString.tokenize('&').each { pair ->
def kv = pair.tokenize('=')
if (kv.size() == 2) { queryParams[kv[0]] = kv[1] }
}
}
// Pick the connection once, based on environment. OAuth is handled natively BY the
// connection itself (Call using IDCS OAUTH, against a Trusted Integrated Application
// with a registered certificate and scope) -- no manual token fetch, no bearer header.
def callOIC
switch (env) {
case 'DEV1': callOIC = adf.webServices.OicDev1WsRest; break
case 'TEST': callOIC = adf.webServices.OicTestWsRest; break
case 'PROD': callOIC = adf.webServices.OicProdWsRest; break
default: throw new oracle.jbo.JboException("Unable to determine Fusion environment from pod: " + pod)
}
callOIC.dynamicQueryParams = queryParams
def response
if (method?.toUpperCase() == 'POST') {
response = callOIC.POST(slots[0], slots[1], slots[2], slots[3], slots[4], slots[5], slots[6], slots[7], slots[8], slots[9], payload)
} else {
response = callOIC.GET(slots[0], slots[1], slots[2], slots[3], slots[4], slots[5], slots[6], slots[7], slots[8], slots[9])
}
return response5. Row-level and object-level entry points
Row-level (thin delegate):
return adf.util.callOIC(relativePath, method, payload)Object-level (24D feature — REST-callable without a row ID; String parameters only, for the reason given in step 3):
def vo = newView('<Prefix>RestUtil_c')
def row = vo.first()
if (row == null) {
row = vo.createRow()
vo.insertRow(row)
}
return row.callOIC(relativePath, method, payload == null ? null : adf.util.parseJson(payload))6. Calling convention
- v1 (non-project):
"{INTEGRATION}/{VERSION}/{resourcePath}", e.g."ECHO/1.0/process" - v2 (project):
"project/{PROJECT}/{INTEGRATION}/{VERSION}/{resourcePath}", e.g."project/MY_PROJECT/ECHO/1.0/process" - Query parameters append with
?, e.g."...process?param=123&other=abc"— these are split out before path tokenizing and sent viadynamicQueryParams, never smuggled through a path placeholder.
From a VB AppUI or Redwood extension:
POST .../resources/<version>/<objectCollection>/action/callOIC
Content-Type: application/vnd.oracle.adf.action+json
{
"name": "callOIC",
"parameters": {
"relativePath": "ECHO/1.0/process",
"method": "GET",
"payload": "{}"
}
}IDCS / IAM setup gotchas
- The four grant types have to be enabled together. Client credentials alone gives
invalid_client. Add Trusted + certificate and you getinvalid_grant. Add SAML2 assertion on top of the other three and it works. We haven’t found this exact combination documented on Oracle’s public pages — re-verify the first time you reproduce it in test/prod, since it’s not yet clear which of the three changes was strictly necessary versus incidental.
Identity propagation limits
- Identity propagation stops at one hop. The real Fusion user shows up as the caller in OIC’s own execution history, but OIC’s own outbound call back into Fusion/ERP authenticates as whatever identity that connection is configured with — it does not inherit the propagated user automatically.
- We tested carrying the identity one hop further using the ERP Cloud Adapter’s “OAuth using JWT User Assertion” invoke policy — Oracle’s own documented mechanism for exactly this — and hit two real limits: the JWT payload’s
subclaim is uploaded as a static file, tied to one identity at setup time rather than overridable at runtime, and the “Subject” mapper property that’s supposed to let you overridesubper call wasn’t available in our testing, contradicting the general adapter documentation. We accepted this as a scope boundary rather than pursuing it further.
Groovy sandbox quirks
- REST-callable object functions can’t take an Object parameter — only String and other primitives. That’s the whole reason the payload crosses as a JSON string and gets parsed on the other side rather than passed as a native Map.
groovy.json.JsonSlurperisn’t available in Application Composer Groovy — hence the hand-rolled parser above.throw new Exception(...)fails to compile in the Application Composer Groovy sandbox, despiteExceptionbeing documented as supported on Oracle’s general Classes and Methods page for Groovy Scripts. Useoracle.jbo.JboExceptioninstead — that page evidently doesn’t fully describe Application Composer’s own, more specific compile-time whitelist.
Connection, scope, and open items
- Scope format is easy to get wrong — no space between
:443andurn:. - The REST Web Service Connection is explicitly excluded from Configuration Set Migration. It has to be re-created natively in every environment, including after any refresh — that, and the IDCS trust setup, are the accepted one-time-per-environment manual steps for this pattern.
- [Unverified] whether the native connection caches or reuses tokens internally, or fetches a fresh one per call — there’s no visible token-fetch code left to control this either way. Worth confirming under load via IDCS/IAM sign-in activity logs before assuming any earlier rate-limit concern is resolved.
- [Unverified] the pod-string environment detection has only been confirmed against dev1’s real value. Log and confirm test/prod pod values before trusting the fallback branch, which currently defaults any unmatched value to PROD.
How to verify
- OIC’s own tracking/monitoring console: open a tracked instance’s execution history and confirm the running-as identity is the real Fusion user, not a generic service account.
- IDCS/IAM sign-in activity logs: confirm token issuance events against this Integrated Application, and use them to check whether repeated calls in a short window produce one sign-in event or many — this informs the token-caching open item above.
- Postman: call the object-level
callOICREST action directly withContent-Type: application/vnd.oracle.adf.action+jsonagainst a known integration (an ECHO integration is a good first test), and confirm the response comes back as real JSON, not a GroovytoString()representation. - Browser network tab / VB AppUI debug console when calling from an actual AppUI action, to confirm the round trip end-to-end.
Conclusion
One connection per environment replaces per-integration URL and token wiring. OIC’s own execution log shows the real Fusion user who triggered the call, not a generic service account — which matters the moment anyone needs to audit who did what. And because the engine lives in a Global Function, every Groovy script in the instance — not just one AppUI — gets synchronous, user-attributed OIC access for free, with no OAuth code to write or maintain.
Want this in your environment?
Choosing today between a generic service user showing up in your OIC logs and a manual OAuth token hop bolted onto Groovy? We’ve proven this native-IDCS-OAuth pattern end-to-end and are the team to set it up correctly in your Fusion and OCI tenancy — the IDCS trust configuration, the connection, and the reusable Groovy engine. Reach out at info@cidsolutions.co.il or WhatsApp — let’s get it solved.
Related posts:
- Fusion Apps: Submit Asynchronous OIC Integrations by Using Custom Object Integration Events – Revised
- Fusion OTBI: Securely obtain a session ID from OIC by using OCI Vault and an Oracle Function
- Overcome Groovy Execution Limits by Submitting OIC Integration Through ESS Job
- Overcome newly introduced limitation of using SQL inside OM Extensions by using BIP and OIC