Tudo começou com uma pergunta que ninguém na sala conseguia responder com segurança.

Como expor uma integração do Oracle Integration Cloud para a Internet sem comprometer a segurança da autenticação?

A primeira resposta parecia óbvia.

“É só validar o token e chamar o OIC.”

Só que, conforme começamos a desenhar a arquitetura, percebemos que a resposta não era tão simples quanto parecia.

Se simplesmente encaminhássemos o token recebido do consumidor para o Oracle Integration Cloud, estaríamos delegando ao cliente uma credencial que não deveria sair do nosso domínio de confiança. Por outro lado, criar usuários técnicos para cada consumidor também não escalava e aumentava significativamente a superfície de ataque e a complexidade operacional.

Durante alguns dias discutimos diferentes possibilidades. Autenticação mTLS, API Gateway, OAuth2, OCI IAM, Oracle Identity Domains, Functions, validação de JWT… cada alternativa resolvia uma parte do problema, mas ainda faltava algo.

A identidade do consumidor serve para provar quem está chamando a API.

A identidade utilizada para acessar o Oracle Integration Cloud serve para provar quem está autorizado a consumir a integração internamente.

São responsabilidades diferentes e, por consequência, precisam de credenciais diferentes.

A partir dessa separação, a arquitetura começou a fazer sentido. A API passou a validar o certificado mTLS, o token apresentado pelo consumidor na borda da aplicação era validado no IdCS, somente depois dessa validação, uma OCI Function assumia a responsabilidade de obter uma credencial técnica e realizar a chamada ao Oracle Integration Cloud.

Este artigo nasceu exatamente dessa experiência. Todos os nomes, domínios, identificadores e demais informações do ambiente foram removidos por questões de confidencialidade, mas o problema, as decisões arquiteturais e as soluções apresentadas refletem um projeto real.

Mais do que mostrar uma implementação, a ideia é explicar o raciocínio por trás dela: por que utilizamos dois tokens distintos, onde cada validação acontece e como conseguimos expor uma integração corporativa para a Internet sem transformar credenciais privilegiadas em segredo compartilhado entre consumidores.

O Problema: um token válido não é qualquer token

Um JWT pode estar assinado corretamente, dentro da validade e ainda assim não ser apropriado para o recurso de destino. Em OAuth2, o token representa uma relação específica entre emissor, cliente, audiência e escopos. Validar apenas a assinatura responde a uma pergunta pequena demais.

Na entrada, precisávamos comprovar duas coisas:

  • a conexão vinha de um consumidor autorizado, por mTLS;
  • o Token A havia sido emitido pelo provedor esperado, para a audiência e os escopos corretos.

No acesso ao OIC, a pergunta era outra. A chamada precisava usar um Token B técnico, emitido por client_credentials para a aplicação que representa o broker.

As duas saídas óbvias falhavam. Encaminhar o Token A direto ao OIC entregaria ao consumidor uma credencial que deveria morrer na borda e acoplaria o contrato externo à identidade interna. Criar um usuário técnico por consumidor multiplicava segredos, escalava mal e aumentava a superfície de ataque a cada novo cliente. No fundo são perguntas diferentes: autenticar quem entra não é o mesmo que autorizar o que ele alcança lá dentro — e tratar as duas com a mesma credencial era a raiz do problema. Sobrou separar os papéis: um token para provar quem chama, outro para acessar a integração.

O Mapa Mental: duas identidades, dois tokens

Consumidor envia mTLS e Token A ao API Gateway; o gateway valida o JWT, a Function obtém Token B usando Vault e cache e chama o OIC.
O API Gateway encerra a confiança externa. A Function inicia uma nova relação de confiança com o OIC.

O desenho ficou dividido em responsabilidades explícitas:

  • Consumidor: recebe um certificado da nossa CA na OCI e as credenciais de um Confidential App (client_id e secret); com elas obtém o Token A e chama a API por mTLS.
  • API Gateway: valida o certificado pelo CN no mTLS, o JWT via JWKS e as regras da rota.
  • CA na OCI: emite o certificado entregue a cada consumidor; o mTLS confere o CN apresentado contra ela.
  • IdCS: hospeda o Confidential App (client_id, secret e scope), publica o JWKS e emite os tokens OAuth2.
  • Function broker: obtém ou reutiliza o Token B e encaminha a chamada.
  • OCI Vault: guarda as credenciais técnicas, fora do código e da configuração aberta.
  • OCI Cache: mantém o Token B enquanto ele ainda é utilizável.
  • OIC: recebe apenas a identidade técnica prevista para suas integrações.

A regra mais importante é simples: o Token A termina no gateway e na camada de entrada da Function. Ele nunca vira a autorização enviada ao OIC.

O Caminho da Chamada

Sequência em sete passos mostrando a emissão do Token A, a validação no gateway, a obtenção do Token B e a chamada ao OIC.
A troca não converte um JWT em outro. Ela encerra uma identidade e inicia uma chamada independente com credenciais técnicas.

1. O consumidor obtém o Token A

Com as credenciais do seu Confidential App no IdCS (client_id e client_secret), o consumidor usa o fluxo OAuth2 client_credentials (RFC 6749, §4.4), adequado à comunicação máquina a máquina. Não existe usuário final nessa etapa: a identidade representada no token é a própria aplicação consumidora.

A obtenção ocorre por uma rota dedicada do API Gateway. O consumidor apresenta seu certificado no TLS, envia as credenciais do Confidential App por Basic Authentication e solicita apenas o escopo previsto para aquela integração:

flowchart LR
    C@{ img: "/diagram-icons/architecture/oracle/services/consumer.svg", label: "Consumidor", pos: "b", h: 72, constraint: "on" }
    G@{ img: "/diagram-icons/architecture/oracle/services/api-gateway.svg", label: "API Gateway", pos: "b", h: 72, constraint: "on" }
    I@{ img: "/diagram-icons/architecture/oracle/services/identity.png", label: "IdCS", pos: "b", h: 72, constraint: "on" }
    C -->|1 · certificado + Basic + scope| G
    G --> M{2 · mTLS válido?}
    M -->|sim| I
    M -->|não| U[HTTP 401]
    I -->|3 · autenticar app e aplicar scope| T[4 · emitir Token A]
    T --> E[5 · Token A entregue ao consumidor]

O certificado protege a conexão; as credenciais autenticam o Confidential App; o IdCS emite o Token A com o escopo permitido.

curl --cert consumidor.crt --key consumidor.key \
  --user "$CLIENT_ID:$CLIENT_SECRET" \
  --data-urlencode "grant_type=client_credentials" \
  --data-urlencode "scope=$SCOPE_PERMITIDO" \
  https://api.exemplo.com/oauth2/v1/token

O gateway encaminha essa solicitação ao IdCS, que autentica o cliente e devolve o Token A. O resultado tem emissor, audiência, validade e escopos próprios. Ele não contém a senha do OIC nem permite chamar a integração diretamente: sua função é provar, na próxima etapa, qual aplicação está entrando pela borda.

Separar a emissão do token da chamada de negócio também melhora o diagnóstico. Uma falha nesse ponto indica problema de certificado, credencial ou escopo; o payload da integração ainda nem chegou ao broker.

2. O API Gateway valida mTLS e JWT

O mTLS comprova a posse da chave privada associada ao certificado emitido pela nossa CA na OCI, cujo CN é conferido na borda. Em seguida, o API Gateway valida o JWT nativamente usando REMOTE_JWKS. Assinatura, emissor, audiência, expiração e demais regras ficam na borda, antes de invocar código.

Na prática, são duas provas independentes. O certificado identifica quem abriu a conexão; o JWT identifica a aplicação e o que ela pode solicitar. Uma credencial não compensa a ausência da outra. A política do deployment expressa essa combinação antes da rota chegar à Function:

flowchart LR
    R@{ img: "/diagram-icons/architecture/oracle/services/consumer.svg", label: "Certificado + Token A", pos: "b", h: 68, constraint: "on" }
    G@{ img: "/diagram-icons/architecture/oracle/services/api-gateway.svg", label: "API Gateway", pos: "b", h: 68, constraint: "on" }
    I@{ img: "/diagram-icons/architecture/oracle/services/identity.png", label: "IdCS · JWKS", pos: "b", h: 68, constraint: "on" }
    F@{ img: "/diagram-icons/architecture/oracle/services/functions.svg", label: "Function broker", pos: "b", h: 68, constraint: "on" }
    R --> G
    G --> M{mTLS válido?}
    M -- não --> U[HTTP 401]
    M -- sim --> J{JWT válido?}
    I -->|JWKS público| J
    J -->|assinatura + iss + aud + exp| D{Todas as regras passaram?}
    J -- não --> U
    D -- sim --> F
    D -- não --> U

As duas validações terminam na borda. Uma falha de certificado ou JWT encerra a chamada antes do runtime serverless.

{
  "mutualTls": { "isVerifiedCertificateRequired": true },
  "authentication": {
    "type": "JWT_AUTHENTICATION",
    "validationPolicy": { "type": "REMOTE_JWKS" },
    "isAnonymousAccessAllowed": false
  }
}

No primeiro uso de uma chave, o gateway consulta o JWKS publicado pelo IdCS; nas chamadas seguintes, reutiliza a chave pública pelo período configurado. Depois verifica assinatura, iss, aud e exp. Certificado recusado ou JWT inválido encerra a requisição com 401, sem executar o broker e sem consumir recursos do OIC.

Isso remove da Function uma tarefa sensível que o gateway já executa e mantém chamadas inválidas fora do runtime serverless. Também estabelece uma fronteira fácil de operar: falhas de identidade aparecem nos logs do gateway; a Function recebe somente requisições que passaram pelas duas validações.

3. A rota informa o destino

Cada rota do deployment injeta um cabeçalho interno, o x-oic-target, com o destino OIC correspondente. Esse valor não vem do corpo do consumidor: o gateway é a fonte da decisão. A mesma Function atende todas as rotas — o que muda entre elas é apenas o destino que o gateway injeta.

O API Gateway injeta o x-oic-target por rota; a mesma Function lê o cabeçalho, exige HTTPS contra SSRF e faz o POST ao flow OIC correspondente.
O cliente escolhe a rota; o gateway decide o destino; a Function só confia no que é HTTPS.

Do lado da Function, o x-oic-target é tratado como dado a validar, nunca como URL confiável. Como o destino chega por cabeçalho, exigir HTTPS antes de usá-lo evita transformar o broker em uma porta para SSRF:

from urllib.parse import urlparse

def resolver_destino(headers: dict) -> str:
    # O x-oic-target é injetado pela ROTA do API Gateway, nunca pelo cliente.
    # Mesmo assim validamos: sem isso, a Function viraria um proxy aberto (SSRF).
    destino = headers.get("x-oic-target", "")
    url = urlparse(destino)
    if url.scheme != "https" or not url.netloc:
        raise ValueError("x-oic-target ausente ou nao e HTTPS")
    return destino

# a chamada ao OIC usa o destino resolvido + a credencial tecnica (Token B)
destino = resolver_destino(request.headers)
resposta = http_client.post(
    destino,
    headers={"Authorization": f"Bearer {token_b}"},
    data=request.body,
)

Essa escolha mudou o custo de evolução. Adicionar ou mover uma integração altera a especificação do API Gateway, não uma tabela interna e nem o código da Function.

4. A Function procura o Token B

Depois de validar o destino, a Function precisa de uma credencial técnica para o OIC. Antes de chamar novamente o IdCS, o broker consulta o OCI Cache. Esse cache é externo ao container da Function, portanto uma instância pode reutilizar o token emitido por outra, mesmo depois de uma nova inicialização do runtime.

A chave representa a identidade técnica, não a rota. O mesmo client_id e o mesmo scope produzem um Token B que pode atender todos os flows autorizados para aquele broker:

flowchart LR
    F@{ img: "/diagram-icons/architecture/oracle/services/functions.svg", label: "Function broker", pos: "b", h: 72, constraint: "on" }
    C@{ img: "/diagram-icons/architecture/oracle/services/cache.svg", label: "OCI Cache", pos: "b", h: 72, constraint: "on" }
    I@{ img: "/diagram-icons/architecture/oracle/services/identity.png", label: "IdCS · renovar", pos: "b", h: 72, constraint: "on" }
    F --> K[Montar chave<br/>oic-broker + client_id + scope]
    K --> C
    C --> T{Token B existe?}
    T -- não --> M[MISS]
    T -- sim --> V{Validade restante<br/>maior que a margem?}
    V -- sim --> H[HIT<br/>reutilizar Token B]
    V -- não --> M
    M --> I

O cache compartilha o Token B entre execuções, mas só entrega a credencial quando ainda existe uma janela segura de validade.

cache_key = f"oic-broker|{client_id}|{scope}"
cached = token_cache.get(cache_key)

if cached and cached.is_usable(time.time(), refresh_skew_seconds=300):
    return cached, "hit"

Um token presente não é automaticamente reutilizável. O broker compara expires_at com o horário atual e exige uma margem antes da expiração. Se ainda houver tempo seguro, o status é hit; se não houver, o fluxo segue para renovação.

Essa decisão reduz chamadas ao endpoint OAuth2 e estabiliza a latência sem misturar responsabilidades. O x-oic-target escolhe o flow; a chave de cache identifica a credencial autorizada a chegar ao OIC.

5. O broker emite um novo token quando necessário

Em caso de ausência ou expiração próxima, a Function resolve no OCI Vault as credenciais do Confidential App técnico. O acesso ocorre com Resource Principal: a identidade temporária da própria Function é incluída em um Dynamic Group, e uma política concede somente a leitura dos segredos necessários.

Com client_id, client_secret e scope resolvidos, o provider faz uma nova solicitação client_credentials ao IdCS:

flowchart LR
    F@{ img: "/diagram-icons/architecture/oracle/services/functions.svg", label: "Function broker", pos: "b", h: 72, constraint: "on" }
    V@{ img: "/diagram-icons/architecture/oracle/services/vault.svg", label: "OCI Vault", pos: "b", h: 72, constraint: "on" }
    I@{ img: "/diagram-icons/architecture/oracle/services/identity.png", label: "IdCS", pos: "b", h: 72, constraint: "on" }
    C@{ img: "/diagram-icons/architecture/oracle/services/cache.svg", label: "OCI Cache", pos: "b", h: 72, constraint: "on" }
    F -->|1 · Resource Principal| V
    V -->|2 · credenciais técnicas| P[Provider OAuth2]
    P -->|3 · client_credentials| I
    I -->|4 · Token B + expires_in| T[Calcular expiração menos margem]
    T -->|5 · TTL controlado| C

O segredo sai do Vault apenas durante a execução; o que permanece no cache é o Token B com TTL controlado.

provider = OAuthClientCredentialsProvider(
    token_url=token_url,
    client_id=client_id,
    client_secret=client_secret,
    scope=scope,
    cache=token_cache,
    refresh_skew_seconds=300,
)
token, cache_status = provider.get_token(cache_key)

O segredo existe apenas pelo tempo necessário dentro da execução. Ele não fica gravado no código, em variável aberta de configuração ou em log. Se Vault, IAM ou IdCS recusarem a operação, o broker devolve erro técnico e registra somente o tipo da falha e os identificadores de correlação.

Quando o IdCS devolve o Token B, o provider calcula expires_at e o salva com TTL menor que sua validade real. A margem evita retirar do cache uma credencial que pode expirar durante a viagem entre Function e OIC. O status miss também segue na resposta e no log, permitindo explicar por que uma chamada específica demorou mais.

Aqui está a troca de identidade propriamente dita: o Token A não é convertido. A Function usa sua própria identidade técnica para obter outra credencial, com audiência e privilégios compatíveis com o OIC.

6. A chamada chega ao OIC

A Function preserva o corpo da requisição, mas monta novos cabeçalhos de saída. O Authorization recebido do consumidor não é repassado: ele é substituído pelo Token B técnico. O broker também gera um correlation_id próprio e o envia ao OIC para relacionar gateway, Function e integração sem usar o token como evidência.

flowchart LR
    G@{ img: "/diagram-icons/architecture/oracle/services/api-gateway.svg", label: "API Gateway", pos: "b", h: 72, constraint: "on" }
    F@{ img: "/diagram-icons/architecture/oracle/services/functions.svg", label: "Function broker", pos: "b", h: 72, constraint: "on" }
    O@{ img: "/diagram-icons/architecture/oracle/services/oic.svg", label: "OIC", pos: "b", h: 72, constraint: "on" }
    L@{ img: "/diagram-icons/architecture/oracle/services/logging.svg", label: "OCI Logging", pos: "b", h: 72, constraint: "on" }
    G -->|1 · corpo + Token A + destino| F
    F --> X[2 · descartar Token A<br/>gerar correlation_id]
    X -->|3 · POST único + Token B| O
    O -->|4 · status + corpo| R[Resposta do broker]
    R -->|5 · status + correlation_id| G
    F -->|6 · metadados seguros| L

O payload segue para o flow uma única vez; a credencial muda na fronteira e os logs preservam evidência sem registrar tokens.

outbound_headers = {
    "Authorization": f"{token.token_type} {token.access_token}",
    "Content-Type": incoming_headers.get("content-type", "application/json"),
    "Accept": incoming_headers.get("accept", "application/json"),
    "x-correlation-id": correlation_id,
}
backend_response = invoke_http_backend(
    method="POST", url=target_url, body=request_body, headers=outbound_headers
)

O OIC valida o Token B e executa o flow definido pelo x-oic-target. A resposta do backend volta pelo mesmo caminho, preservando status e corpo. O broker acrescenta x-correlation-id e x-token-cache-status, informação útil para diferenciar uma chamada com token reutilizado de outra que precisou renová-lo.

Nos logs ficam status do backend, latência total, tempo de resolução do token, situação do cache e identificadores de correlação. Token A, Token B, segredo e payload permanecem fora do registro. Assim, a chamada pode ser investigada de ponta a ponta sem transformar observabilidade em vazamento de credencial.

A Function Broker, por Dentro

Na implementação, o contrato técnico da Function é:

  1. receber a chamada do API Gateway, cuja permissão de invocação é controlada pela política IAM;
  2. validar o destino interno recebido;
  3. obter uma credencial técnica válida;
  4. encaminhar método, corpo e cabeçalhos permitidos;
  5. devolver status e corpo do backend nas chamadas concluídas ou uma resposta de erro com correlation_id.

Vault com Resource Principal

A Function entra em um Dynamic Group e recebe por política apenas a permissão necessária para ler os segredos do broker. O SDK usa Resource Principal, portanto não existe uma chave permanente da OCI empacotada junto ao código.

Dynamic Group, política e Vault formam a cadeia que permite à identidade da Function resolver as credenciais técnicas sem armazenar uma chave permanente da OCI.

Cache com margem de renovação

Cache de token não é apenas otimização. Ele reduz latência, diminui dependência do endpoint de emissão e evita uma nova autenticação a cada chamada. Porém, armazenar o token até o último segundo cria uma corrida com o relógio.

O broker calcula o TTL útil com margem de renovação e limite máximo. Se a leitura do Redis falhar, o resultado é tratado como ausência de token e o provider solicita outro ao IdCS. Se a gravação falhar, a Function registra o aviso e conclui a chamada atual com o token recém-emitido.

Logs Estruturados

A Function registra uma linha JSON por evento. Os exemplos abaixo usam valores ilustrativos e mostram apenas os campos emitidos pelo broker, sem JWT, segredo, cabeçalho Authorization ou payload.

Chamada concluída com Token B encontrado no cache

{
  "ts": "2026-07-10T14:32:18.421Z",
  "level": "INFO",
  "function": "oic-broker",
  "function_version": "v0.3.0",
  "event": "broker.proxy",
  "fn_request_id": "fn-request-exemplo",
  "correlation_id": "corr-gerado-pelo-broker",
  "client_correlation_id": "corr-enviado-pelo-consumidor",
  "apigw_request_id": "request-do-api-gateway",
  "backend_status": 200,
  "latency_ms": 184,
  "breakdown_ms": {
    "token_resolve": 3,
    "oic_call": 176
  },
  "cache": {
    "oic_token": {
      "layer": "redis",
      "status": "hit",
      "ttl_remaining_s": 2410
    }
  },
  "target_path": "/integration/flow"
}

O evento broker.proxy confirma que o OIC respondeu com HTTP 200. O cache.status igual a hit mostra que a Function reutilizou o Token B no Redis; token_resolve e oic_call separam o tempo gasto com a credencial do tempo gasto no backend.

Chamada concluída após emitir outro Token B

{
  "ts": "2026-07-10T14:38:06.903Z",
  "level": "INFO",
  "function": "oic-broker",
  "function_version": "v0.3.0",
  "event": "broker.proxy",
  "fn_request_id": "fn-request-exemplo-2",
  "correlation_id": "corr-gerado-pelo-broker-2",
  "client_correlation_id": null,
  "apigw_request_id": "request-do-api-gateway-2",
  "backend_status": 202,
  "latency_ms": 463,
  "breakdown_ms": {
    "token_resolve": 281,
    "oic_call": 174
  },
  "cache": {
    "oic_token": {
      "layer": "redis",
      "status": "miss",
      "ttl_remaining_s": 3598
    }
  },
  "target_path": "/integration/flow"
}

Aqui, cache.status igual a miss mostra que o broker precisou obter outro Token B antes de chamar o OIC. Como o consumidor não enviou um identificador próprio, client_correlation_id aparece como null; o correlation_id gerado pela Function continua presente.

Falha ao chamar o backend

{
  "ts": "2026-07-10T14:42:51.127Z",
  "level": "ERROR",
  "function": "oic-broker",
  "function_version": "v0.3.0",
  "event": "broker.error",
  "fn_request_id": "fn-request-exemplo-3",
  "correlation_id": "corr-gerado-pelo-broker-3",
  "client_correlation_id": "corr-enviado-pelo-consumidor-3",
  "apigw_request_id": "request-do-api-gateway-3",
  "target_path": "/integration/flow",
  "error_type": "TimeoutError",
  "error": "backend request timed out",
  "latency_ms": 15021,
  "breakdown_ms": {
    "token_resolve": 2
  }
}

O evento broker.error identifica a etapa que falhou sem registrar o corpo da requisição ou qualquer credencial. O mesmo correlation_id também volta no corpo e no cabeçalho da resposta de erro, permitindo localizar esse evento no OCI Logging.

Os quatro identificadores ficam separados porque representam origens diferentes:

  • fn_request_id: invocação da OCI Function;
  • correlation_id: UUID gerado pelo broker e propagado ao OIC e à resposta;
  • client_correlation_id: valor opcional recebido do consumidor;
  • apigw_request_id: identificador recebido do API Gateway.

Checklist da Implementação

  • certificado mTLS e Confidential App entregues ao consumidor por canal seguro;
  • Token A emitido pelo IdCS com client_credentials e escopo definido;
  • certificado e Token A validados pelo API Gateway antes da Function;
  • assinatura, emissor, audiência e expiração conferidos com REMOTE_JWKS;
  • x-oic-target definido na rota do API Gateway e validado como HTTPS pela Function;
  • credenciais técnicas lidas do OCI Vault com Resource Principal;
  • Token B armazenado no OCI Cache por client_id e scope, com margem antes da expiração;
  • Authorization do consumidor substituído pelo Token B na chamada ao OIC;
  • correlation_id, IDs de origem, latência e status do cache registrados em JSON;
  • JWT, segredo, cabeçalho Authorization e payload mantidos fora dos logs.

Aprendizados

O ponto central não foi implementar OAuth2 usando uma Function. Foi separar a autenticação do consumidor da autorização usada para acessar o OIC: são duas relações de confiança diferentes.

Com essa fronteira clara, o gateway valida a entrada, a rota define o destino, o Vault protege a credencial, o cache controla a vida do token e a Function faz a troca entre as duas identidades. Tudo funciona, mas não existe mágica.

Agradecimento

Neste trabalho, contei com o Clayton Costa, que teve um papel fundamental na construção da validação JWKS que permitiu integrar nativamente a validação do JWT com a OCI. Mais do que configurar permissões, ele ajudou a transformar uma arquitetura de identidade em uma solução segura, validada e pronta para operação. Obrigado pela parceria, pelo comprometimento e pelo rigor técnico em cada etapa.

Referências Públicas