Note:
- This tutorial requires access to Oracle Cloud. To sign up for a free account, see Get started with Oracle Cloud Infrastructure Free Tier.
- It uses example values for Oracle Cloud Infrastructure credentials, tenancy, and compartments. When completing your lab, substitute these values with ones specific to your cloud environment.
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:
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:
- Oracle Integration/Oracle Identity Cloud Service (IDCS) Configuration.
- OCI Functions Custom Authorizer Implementation.
- OCI API Gateway Configuration.
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.
-
ROPC Grant: Acquires the initial access token from Microsoft Entra ID by directly using the resource owner’s credentials. The access token includes various claims that includes
upn
, which represents the unique identifier of the authenticated user. With thisupn
claim and atrusted private key
, a user assertion is generated. -
JWT Assertion Grant: The user assertion is then used directly as an authorization grant to obtain an access token from OCI identity domain. This access token is then recognized by OCI, enabling seamless invocation of Oracle Integration flows.
Audience
- OCI IAM professionals and Oracle Integration administrators.
Objectives
-
Use an OAuth 2.0 token from Microsoft Entra ID to invoke Oracle Integration flows.
We will be using the OAuth 2.0 ROPC grant to generate an access token from Microsoft Entra ID. We will use OCI services, specifically OCI API Gateway and OCI Functions. The OCI API Gateway will act as the frontend for our Oracle Integration endpoint. It supports using an authorizer function to add an extra layer of authentication for APIs. This means we can create logic to validate the OAuth 2.0 token from Microsoft Entra ID and then exchange it for a token from OCI IAM, using the JWT assertion grant type, allowing us to invoke the Oracle Integration flows.
Prerequisites
-
OCI Account: Admin access to your OCI tenancy to manage applications.
-
Microsoft Entra ID Account: Admin access to your Microsoft Entra ID tenancy to register an application.
-
Familiarity with OCI services like OCI API gateway, OCI Functions, OCI IAM and Python.
-
Knowledge of OAuth 2.0 Authorization Framework.
-
An existing Oracle Integration.
-
Python 3.x installed.
-
Required OCI IAM policies:
-
Policy for creating OCI Function.
Allow group <group name> to use cloud-shell in compartment <function compartment> Allow group <group name> to manage repos in compartment <function compartment> Allow group <group name> to manage functions-family in compartment <function compartment> Allow group <group name> to use virtual-network-family in compartment <network compartment> Allow dynamic-group <dynamic group name> to use secret-family in compartment <vault compartment>
Note: Dynamic group is created in Task 2 and OCI IAM permissions are broader than ideal, as highlighted in this tutorial. They should adhere to the principles of least privilege, enforcing more restrictive policies.
-
Policy for creating OCI API Gateway deployment.
Allow group <group name> to manage api-gateway-family in compartment <api gateway compartment> ALLOW any-user to use functions-family in compartment <function compartment> where ALL {request.principal.type= 'ApiGateway', request.resource.compartment.id = '<ocid of api gateway compartment id>'}
3. Policy for storing secrets in [OCI Vault](https://docs.oracle.com/en-us/iaas/Content/Identity/Concepts/commonpolicies.htm#sec-admins-manage-vaults-keys).
Allow group <group name> to use secret-family in compartment <vault compartment> Allow group <group name> to manage secret-family in compartment <vault compartment> Allow group <group name> to manage keys in compartment <vault compartment> Allow group <group name> to manage vaults in compartment <vault compartment>
Note: The placeholders
<group name>
,<dynamic group name>
,<api gateway compartment>
,<function compartment>
,<vault compartment>
, and<ocid of api gateway compartment id>
need to be replaced with the actual group name, OCI dynamic group name, the compartments where OCI API Gateway, OCI Functions, and OCI Vault are deployed, and the Oracle Cloud Identifier (OCID) value of the OCI API Gateway compartment, respectively. -
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.
-
Register an application. For more information, see Register an application with the Microsoft identity platform.
-
Note down the
Application (client) ID
value from the Overview section. -
Go to Manage, Certificates & Secrets and add a client secret. Note down the secret value as it will be used in a later task.
Task 2: Prerequisite Steps for JWT User Assertion in OCI Identity Domain
-
Complete the Prerequisite tasks from here: Prerequisites for JWT User Assertion.
-
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 thealias
for later tasks. -
Create a dynamic group to allow resource type
function
from a specific compartment to be able to read secrets from the OCI Vault service.
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.
Once the secrets are created, copy the OCID value from the Secret Information section and store it for later tasks.
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
.
-
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.
-
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. -
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.
-
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. -
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.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 thegetSecret()
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 theintrospectToken()
function to validate the token with Entra ID. If the token is valid, sets the authentication context, calls thegetBackEndAuthToken()
function to retrieve the backend token from OCI IAM and sets the expiration time. If the token is not valid, sets thewwwAuthenticate
header to indicate an authentication error. -
introspectToken(access_token, introspection_endpoint, client_id, client_secret):
Validates the token with the providedintrospection_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 thegetAuthContext()
function. -
handler(ctx, data: io.BytesIO = None):
Main function that handles the function execution. Initializes the OAuth context using theinitContext()
function and calls thegetAuthContext()
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.
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.
-
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.Note: include
fdk>=0.1.74
,requests
,oci
,pyjwt
,serialization
inrequirements.txt
file before deploying the functions application. -
Generate an access token from Microsoft Entra ID using the OAuth 2.0 ROPC flow using Postman client.
-
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. -
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.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.
-
Navigate to Developer Services, API Management and Gateways. Enter the following information and click Create Gateway.
-
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.
-
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.
-
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.
-
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.
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 thatback_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. -
After successfully reviewing the configuration, click Save changes to save the deployment and wait until the deployment state is changed to Active.
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.
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.
-
Copy the access token as we will be using the same while invoking the API from the API gateway.
-
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.
-
Click Send to invoke the API request, it will run Oracle Integration and should give a successful output.
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.
Related Links
-
Creating Policies to Control Access to Network and Function-Related Resources
-
Create Policies to Control Access to Network and API Gateway-Related Resources
-
Register an application with the Microsoft identity platform
-
Accessing Other Oracle Cloud Infrastructure Resources from Running Functions
-
Microsoft identity platform and OAuth 2.0 Resource Owner Password Credentials
Acknowledgments
- Author - Gautam Mishra (Principal Cloud Architect)
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.
Securely Access Oracle Integration using Access Tokens from Microsoft Entra ID
G12396-01
August 2024