Skip to content

CID Software Solutions LTD

Home » Fusion Apps: Call OIC Synchronously as the Real User by using a Native IDCS OAuth Connection

Fusion Apps: Call OIC Synchronously as the Real User by using a Native IDCS OAuth Connection

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.

This post doesn’t extend one of our existing numbered chains — it sits alongside two of them, solving a problem neither one covers:

(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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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):

  1. Client credentials grant only, no trust/certificate → invalid_client.
  2. Application marked Trusted + certificate registered → invalid_grant.
  3. 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
OCI IAM Integrated Application configuration showing Trusted flag and SAML2 assertion grant type

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.

ConnectionURLAuth scheme
Oic<Env>WsResthttps://<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 typeSlot 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
Application Composer REST Web Service Connection using the Call using IDCS OAUTH authentication scheme

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 parsedValue

4. 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 response

5. 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 via dynamicQueryParams, 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 get invalid_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 sub claim 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 override sub per 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.JsonSlurper isn’t available in Application Composer Groovy — hence the hand-rolled parser above.
  • throw new Exception(...) fails to compile in the Application Composer Groovy sandbox, despite Exception being documented as supported on Oracle’s general Classes and Methods page for Groovy Scripts. Use oracle.jbo.JboException instead — 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 :443 and urn:.
  • 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 callOIC REST action directly with Content-Type: application/vnd.oracle.adf.action+json against a known integration (an ECHO integration is a good first test), and confirm the response comes back as real JSON, not a Groovy toString() 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:

Leave a Reply

Your email address will not be published. Required fields are marked *