Note:

Securely Access Oracle Integration using Access Tokens from Microsoft Entra ID

Introduction

As Oracle Integration customers adopt multicloud strategies, they often need to connect business applications and processes across different cloud providers. For example, a company might have an application running on Microsoft Azure that needs to access data from Oracle Cloud Infrastructure applications. Normally, you would get a token from Oracle Cloud Infrastructure Identity and Access Management (OCI IAM) to retrieve this data. However, using multiple cloud providers means dealing with multiple tokens, which can be complex and pose security risks.

Imagine how convenient it would be if you could use one OAuth token to integrate with applications across different cloud providers. This tutorial is about using a third-party OAuth provider to invoke Oracle Integration flow.

Architecture

Let us visualize the solution flow:

Image 1

The process begins with the user or business application obtaining an OAuth token from Microsoft Entra ID. Once acquired, this token is used to invoke the endpoint exposed through OCI API Gateway. The OCI API Gateway, configured to use a custom authorizer OCI Functions (formerly known as Oracle Functions), first calls this authorizer function to validate the token. Upon successful validation, it then invokes the actual backend endpoint, which is the Oracle Integration flow.

Now, let us dive into the details of implementing this process. For simplicity, we will divide it into three steps:

Why use Resource Owner Password Credentials (ROPC) and JSON Web Token (JWT) assertion grant types respectively to obtain access token from Microsoft Entra ID and OCI IAM?

Using ROPC and JWT assertion grants together provides a streamlined and secure approach to handle authentication and token exchange in a multicloud environment.

Audience

Objectives

Prerequisites

Task 1: Register an Application with Microsoft Entra ID

In order to use the IAM capabilities of Microsoft Entra ID, including accessing protected resources (graph APIs), you must register an application.

  1. Register an application. For more information, see Register an application with the Microsoft identity platform.

  2. Note down the Application (client) ID value from the Overview section.

    Image 2

  3. Go to Manage, Certificates & Secrets and add a client secret. Note down the secret value as it will be used in a later task.

    Image 3

Task 2: Prerequisite Steps for JWT User Assertion in OCI Identity Domain

  1. Complete the Prerequisite tasks from here: Prerequisites for JWT User Assertion.

  2. Once the Oracle Integration application is validated for the required scopes, self-signed key pairs are generated and a confidential application is configured. Note down the scope value, private_key.pem, Client ID and Client Secret.

    Note: While importing the private key as a trusted partner in the confidential application use the same alias as being used during creation of the self-signed key pairs and note down the alias for later tasks.

  3. Create a dynamic group to allow resource type function from a specific compartment to be able to read secrets from the OCI Vault service.

    Image 6

Task 3: Create the Secrets in OCI Vault

Use OCI Vault manual secret generation option to store the secrets collected from Task 1 and Task 2. For more information, see Creating a Secret in a Vault.

Image 4

Once the secrets are created, copy the OCID value from the Secret Information section and store it for later tasks.

Image 5

Task 4: Create and Configure the func.py File

We will be using OCI Functions as custom authorizer to validate the Microsoft Entra ID access token and generate the OCI IAM access token as a back_end_token.

  1. To start, create an application. In OCI Functions, an application is a logical grouping of functions. The properties you specify for an application determine resource allocation and configuration for all functions in that application. For more information, see Creating Applications.

  2. Once the application is created, add the configuration to the application. We will fetch the following items from our function code, making it more portable and configurable without modifying the code. Enter Key and Value and click +.

    Add the client IDs from Microsoft Entra ID, OCI identity domains, the OCID of the secrets collected in Task 3, the alias, scope collected from Task 2 and a graph endpoint https://graph.microsoft.com/v1.0/me against which the microsoft Entra ID token will be validated.

    Image 7

  3. To create the function, go to Getting Started and click Launch OCI Cloud Shell to open the interactive Linux style cloud shell in your browser. Once OCI Cloud Shell is loaded, you can create, develop, and deploy the custom authorizer Oracle function right from the OCI Cloud Shell.

  4. To create the function using the Fn Project Command Line Interface (CLI), enter the following command for a Python function fn init --runtime python MyCustomAuthorizer and click Enter.

    Image 8

  5. The boilerplate of the function is created, now it can be edited accordingly to include the custom authorizer logic. Change directory to the function folder and edit the func.py file. Copy and paste the following code snippet.

    Image 9

    import io
    import json
    import logging
    import jwt
    import datetime
    from datetime import timedelta
    import time
    import base64
    
    from fdk import response
    import requests
    from requests.auth import HTTPBasicAuth
    from cryptography.hazmat.primitives import serialization
    from cryptography.hazmat.backends import default_backend
    import ociVault
    
    oauth_apps = {}
    
    def initContext(context):
        # This method takes elements from the Application Context and from OCI Vault to create the OAuth App Clients object.
        if (len(oauth_apps) < 2):
            try:
                logging.getLogger().info("initContext: Initializing context")
    
                oauth_apps['idcs'] = {'introspection_endpoint': context['idcs_token_endpoint'],
                                    'client_id': context['idcs_app_client_id'],
                                    'scope':context['idcs_oauth_scope'],
                                    'alias':context['alias'],
                                    'client_secret': ociVault.getSecret(context['idcs_client_secret_ocid'])}
                oauth_apps['AD'] = {'token_endpoint': context['ad_endpoint'],
                                    'client_id': context['ad_app_client_id'],
                                    'client_secret': ociVault.getSecret(context['ad_client_secret_ocid'])}
    
            except Exception as ex:
                logging.getLogger().error("initContext: Failed to get config or secrets" + str(ex))
                raise
    
    
    def getAuthContext(token, client_apps):
        # This method populates the Auth Context that will be returned to the gateway.
        auth_context = {}
        access_token = token[len('Bearer '):]
        jwtToken = json.loads(json.dumps(jwt.decode(access_token, options={"verify_signature": False})))
        # Calling MSFT to validate the token
        try:
        logging.getLogger().info("getAuthContext: Calling Token Introspection function") 
        respIntrospectToken = introspectToken(access_token, client_apps['AD']['token_endpoint'], client_apps['AD']['client_id'], client_apps['AD']['client_secret'])
        except Exception as ex:
                logging.getLogger().error("getAuthContext: Failed to introspect token" + str(ex))
                raise
    
        # If AD confirmed the token valid and active, we can proceed to populate the auth context
        if (respIntrospectToken.status_code == 200):
            auth_context['active'] = True
            auth_context['principal'] = jwtToken['upn']
            auth_context['scope'] = 'https://graph.microsoft.com/.default'
            # Retrieving the back-end Token
            backend_token = getBackEndAuthToken(client_apps['idcs']['introspection_endpoint'], client_apps['idcs']['client_id'], client_apps['idcs']['client_secret'],client_apps['idcs']['scope'],client_apps['idcs']['alias'],auth_context['principal'])
    
            # The maximum TTL for this auth is the lesser of the API Client Auth (Entra ID) and the Gateway Client Auth (OCI IAM)
            if (datetime.datetime.fromtimestamp(jwtToken['exp']) < (datetime.datetime.utcnow() + timedelta(seconds=backend_token['expires_in']))):
                auth_context['expiresAt'] = (datetime.datetime.fromtimestamp(jwtToken['exp'])).replace(tzinfo=datetime.timezone.utc).astimezone().replace(microsecond=0).isoformat()
            else:
                auth_context['expiresAt'] = (datetime.datetime.utcnow() + timedelta(seconds=backend_token['expires_in'])).replace(tzinfo=datetime.timezone.utc).astimezone().replace(microsecond=0).isoformat()
            # Storing the back_end_token in the context of the auth decision so we can map it to Authorization header using the request/response transformation policy
            auth_context['context'] = {'back_end_token': ('Bearer ' + str(backend_token['access_token']))}
    
        else:
            # API Client token is not active, so we will go ahead and respond with the wwwAuthenticate header
            auth_context['active'] = False
            auth_context['wwwAuthenticate'] = 'Bearer realm=\"identity.oraclecloud.com\"'
    
        return(auth_context)
    
    def introspectToken(access_token, introspection_endpoint, client_id, client_secret):
        # This method simply invokes the introspection api as configured in the configuration screen.  
        # The real validation happens in the getAuthContext function.  
        #payload = {'token': access_token}
        headers = {'Accept': 'application/json',
                'Authorization':'Bearer '+access_token}
        try:
            logging.getLogger().info("introspectToken: Introspecting Token") 
            resp = requests.get(introspection_endpoint,
                                headers=headers)
            print(resp)
    
        except Exception as ex:
            logging.getLogger().error("introspectToken: Failed to introspect token" + str(ex))
            raise
    
        return resp
    
    def getBackEndAuthToken(token_endpoint, client_id, client_secret, scope, alias, principal):
        # This method gets the token from the back-end system (ORDS in this case)
        try:
            logging.getLogger().info("getBackEndAuthToken: Getting Backend Token") 
            print("Sub is " + principal)
    
            with open("private_key.pem", "rb") as key_file:
                private_key = serialization.load_pem_private_key(
                    key_file.read(),
                    password=None,
                    backend=default_backend()
                )
    
            headers = {
                "alg": "RS256",
                "typ": "JWT",
                "kid": "abc"
            }
    
            claims = {
                "sub": principal,
                "aud": "https://identity.oraclecloud.com/",
                "iss": client_id,
                "iat": int(time.time()),
                "exp": int(time.time()) + 3600,  # 1 hour expiration
                "jti": "8c7df446-bfae-40be-be09-0ab55c655436"  # random number
            }
    
            logging.getLogger().info("Claims : ")
            logging.getLogger().info(claims) 
    
            jwt_assertion = jwt.encode(
                payload=claims,
                key=private_key,
                algorithm="RS256",
                headers=headers
            )
            logging.getLogger().info("Assertion is :") 
            logging.getLogger().info(jwt_assertion) 
    
            encoded = client_id + ":" + client_secret
            baseencoded = base64.urlsafe_b64encode(encoded.encode('UTF-8')).decode('ascii')
            payload = {'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
                    'scope':scope, 'assertion':jwt_assertion}
            headers = {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 'Authorization': 'Basic %s' % baseencoded, 'Accept': '*/*'}
            backend_token = json.loads(requests.post(token_endpoint,
                                                    data=payload,
                                                    headers=headers).text)
            logging.getLogger().info("Backend token in generated :") 
            logging.getLogger().info(backend_token) 
    
        except Exception as ex:
            logging.getLogger().error("getBackEndAuthToken: Failed to get ORDS token" + str(ex))
            raise
    
        return backend_token
    
    
    
    def handler(ctx, data: io.BytesIO = None):
        initContext(dict(ctx.Config()))
        logging.getLogger().info(oauth_apps)
    
        auth_context = {}
        try:
            logging.getLogger().info("handler: Started Function Execution") 
            gateway_auth = json.loads(data.getvalue())
            auth_context = getAuthContext(gateway_auth['token'], oauth_apps)
            if (auth_context['active']):
                logging.getLogger().info('Authorizer returning 200...')
                return response.Response(
                    ctx,
                    response_data=json.dumps(auth_context),
                    status_code = 200,
                    headers={"Content-Type": "application/json"}
                    )
            else:
                logging.getLogger().info('Authorizer returning 401...')
                return response.Response(
                    ctx,
                    response_data=json.dumps(str(auth_context)),
                    status_code = 401,
                    headers={"Content-Type": "application/json"}
                    )
    
        except (Exception, ValueError) as ex:
            logging.getLogger().info('error parsing json payload: ' + str(ex))
    
            return response.Response(
                ctx,
                response_data=json.dumps(str(auth_context)),
                status_code = 500,
                headers={"Content-Type": "application/json"}
                )
    
    
    • Imports

      • io, json, logging, datetime, time, base64: Standard Python libraries for handling I/O, JSON data, logging, date and time operations, and base64 encoding.
      • jwt: Library for encoding and decoding JSON Web Tokens (JWT).
      • requests: Library for making HTTP requests.
      • HTTPBasicAuth: Class for handling HTTP basic authentication.
      • serialization, default_backend: From the cryptography library, used for handling cryptographic operations.
      • ociVault: A custom module for interacting with OCI Vault.
    • Global Variable

      • oauth_apps: Dictionary to store application configuration.
    • Functions

      • initContext(context): Purpose of this function is to initialize the application configuration using the context data and secrets from the OCI Vault. It receives context dictionary object which gets invoked as the first thing in the main handler method and retrieves the secrets from OCI Vault using the getSecret() function explained in Task 5.

      • getAuthContext(token, client_apps): Populates and returns the authentication context for the OCI API Gateway. Extracts and decodes the access token. Calls the introspectToken() function to validate the token with Entra ID. If the token is valid, sets the authentication context, calls the getBackEndAuthToken() function to retrieve the backend token from OCI IAM and sets the expiration time. If the token is not valid, sets the wwwAuthenticate header to indicate an authentication error.

      • introspectToken(access_token, introspection_endpoint, client_id, client_secret): Validates the token with the provided introspection_endpoint. Makes a GET request to the introspection endpoint with the token. Returns the response from the introspection or validation endpoint. As Microsoft Entra ID does not have OAuth introspection API endpoint, we are invoking the configured endpoint using the token received as input.

      • getBackEndAuthToken(token_endpoint, client_id, client_secret, scope, alias, principal): Loads the private key from a PEM file. Creates JWT claims and encodes them into a JWT assertion. Prepares the payload and headers for the token request. Makes a POST request to the token endpoint to obtain the backend token and returns the backend token to the getAuthContext() function.

      • handler(ctx, data: io.BytesIO = None): Main function that handles the function execution. Initializes the OAuth context using the initContext() function and calls the getAuthContext() function to get the authentication context. Returns a 200 response if the token is valid, otherwise returns a 401 response. Logs and returns a 500 response in case of errors.

Task 5: Create and Configure the ociVault.py File

Create a ociVault.py file in the same folder and paste the following code snippet. This utility function reads the secrets from the OCI Vault service.

# Utility Function to get secrets from OCI Vault
import logging
import oci
import base64

def getSecret(ocid):
    signer = oci.auth.signers.get_resource_principals_signer()
    try:
        client = oci.secrets.SecretsClient({}, signer=signer)
        secret_content = client.get_secret_bundle(ocid).data.secret_bundle_content.content.encode('utf-8')
        decrypted_secret_content = base64.b64decode(secret_content).decode('utf-8')
    except Exception as ex:
        logging.getLogger().error("getSecret: Failed to get Secret" + ex)
        print("Error [getSecret]: failed to retrieve", ex, flush=True)
        raise
    return decrypted_secret_content

Note: Keep the private_key.pem file from Task 2 in the same folder.

Image 10

Task 5: Test the Function

In order to test the function, we need to deploy the function and then invoke it by passing the Microsoft Entra ID token as input.

  1. Navigate to the function folder and run the following command fn -v deploy --app MyCustomAuthorizer to deploy it. The Fn Project CLI command will be building the function and deploying the same on the OCI Functions application.

    Image 11

    Note: include fdk>=0.1.74, requests, oci, pyjwt, serialization in requirements.txt file before deploying the functions application.

  2. Generate an access token from Microsoft Entra ID using the OAuth 2.0 ROPC flow using Postman client.

    Image 12

  3. Note down the access token to generate a payload.json, that will be passed as an input to test the OCI Functions. Keep the JSON file in the same function directory.

    Image 13

  4. Once you have the payload saved, you can run the following command to mimic execution of function as it will be invoked through the OCI API Gateway, cat payload.json | fn invoke <AppName> <function name> as shown in the following image.

    Image 14

    If the Microsoft Entra ID token is valid, you will see a response as shown in the following image where you will see OCI IAM token value in back_end_token value of context structure.

Task 6: Configure OCI API Gateway

OCI API Gateway is a fully managed, scalable, cloud-native API management platform that offers a suite of services from rapid API deployment to lifecycle management and backend service integration. We will leverage the API gateway to mediate authorization for Oracle Integration using an external identity provider like Microsoft Entra ID.

Begin by creating a new API gateway and then creating a new deployment on the API gateway.

  1. Navigate to Developer Services, API Management and Gateways. Enter the following information and click Create Gateway.

    Image 15

    Image 16

  2. In the Gateway Details page, click Create Deployment and enter the following required information for your API deployment.

    • Name: Enter a name.
    • Path prefix: Define the path.
    • Compartment: Select the appropriate compartment for your API deployment.

    Image 17

  3. Add the authentication policy details. This is where you configure the OCI Functions, which is to be invoked as a custom authorizer. Select the function created in Task 4.

    Image 18

  4. In the Routes page, configure the API routing to the backend service. In this tutorial, we will be defining routing to the Oracle Integration endpoint.

    Image 19

  5. Click Show Route Request Policies, this is where the user would perform the swap of authentication tokens from OCI Functions response to the authentication header of the request.

    Image 20

    This step involves setting the authentication token for the backend service based on the backend identity provider. In our scenario, we are setting the bearer token for OCI IAM as received from the custom authorizer OCI Functions. Here, we configure the authorization header to be overridden with the value ${request.auth[back_end_token]}. Note that back_end_token is a part of the context in the Oracle function response structure. Ensure that this expression evaluates successfully after the custom authorizer OCI Functions completes.

  6. After successfully reviewing the configuration, click Save changes to save the deployment and wait until the deployment state is changed to Active.

    Image 21

    After activating the API deployment, copy the Endpoint (base URL) from the Deployment information section. This URL serves as the endpoint for your deployment, where your business process or application will invoke the Oracle Integration endpoints using the Microsoft Entra ID bearer token. We will be using the base URL in the next task.

    Image 22

Task 7: Test the API

First, obtain an access token from Microsoft Entra ID using the Postman client. We will use ROPC flow to ensure that the access token includes the necessary identity information.

  1. Copy the access token as we will be using the same while invoking the API from the API gateway.

    Image 12

  2. Create a new REST request combining base endpoint URL copied in Task 6 from the API gateway and Oracle Integration endpoint as shown in the following image. Use the bearer token in the request header.

    Image 23

  3. Click Send to invoke the API request, it will run Oracle Integration and should give a successful output.

    Image 24

Next Steps

We successfully invoked the API on Oracle API Gateway using an OAuth token from Microsoft Entra ID, receiving a response from the Oracle Integration REST trigger flow. This integration is crucial for customers connecting digital services across different cloud vendors.

Acknowledgments

More Learning Resources

Explore other labs on docs.oracle.com/learn or access more free learning content on the Oracle Learning YouTube channel. Additionally, visit education.oracle.com/learning-explorer to become an Oracle Learning Explorer.

For product documentation, visit Oracle Help Center.