ノート:

Microsoft Entra IDからのアクセス・トークンを使用したOracle Integrationへの安全なアクセス

イントロダクション

Oracle Integrationのお客様は、マルチクラウド戦略を採用しているため、多くの場合、異なるクラウド・プロバイダー間でビジネス・アプリケーションとプロセスを接続する必要があります。たとえば、Oracle Cloud Infrastructureアプリケーションからデータにアクセスする必要があるアプリケーションをMicrosoft Azureで実行しているとします。通常、Oracle Cloud Infrastructure Identity and Access Management (OCI IAM)からトークンを取得して、このデータを取得します。ただし、複数のクラウド・プロバイダを使用すると、複数のトークンを処理できるため、複雑になり、セキュリティ・リスクが生じる可能性があります。

1つのOAuthトークンを使用して、異なるクラウド・プロバイダ間でアプリケーションと統合できる場合の利便性について考えてみます。このチュートリアルでは、サード・パーティのOAuthプロバイダを使用してOracle Integrationフローを起動します。

アーキテクチャ

ソリューション・フローを視覚化します。

イメージ 1

プロセスは、ユーザーまたはビジネス・アプリケーションがMicrosoft Entra IDからOAuthトークンを取得することから始まります。取得後、このトークンはOCI API Gatewayを介して公開されるエンドポイントの起動に使用されます。カスタム認可プロバイダOCIファンクション(以前のOracle Functions)を使用するように構成されたOCI APIゲートウェイは、最初にこの認可プロバイダ・ファンクションをコールしてトークンを検証します。検証に成功すると、実際のバックエンド・エンドポイント(Oracle Integrationフロー)が起動されます。

次に、このプロセスの実行の詳細を説明します。わかりやすくするために、次の3つのステップに分割します。

Microsoft Entra IDおよびOCI IAMからアクセス・トークンを取得するために、リソース所有者パスワード資格証明(ROPC)およびJSON Webトークン(JWT)アサーション付与タイプをそれぞれ使用するのはなぜですか。

ROPCおよびJWTアサーション権限を一緒に使用すると、マルチクラウド環境で認証およびトークン交換を処理するための合理化されたセキュアなアプローチが提供されます。

対象読者

目的

前提条件

タスク1: Microsoft Entra IDを使用したアプリケーションの登録

保護されたリソース(グラフAPI)へのアクセスなど、Microsoft Entra IDのIAM機能を使用するには、アプリケーションを登録する必要があります。

  1. アプリケーションの登録詳細は、「Microsoft IDプラットフォームへのアプリケーションの登録」を参照してください。

  2. 「概要」セクションのApplication (client) ID値を書き留めます。

    イメージ 2

  3. 「管理」「証明書およびシークレット」に移動して、クライアント・シークレットを追加します。シークレット値は、後のタスクで使用するためノートにとります。

    イメージ 3

タスク2: OCIアイデンティティ・ドメインでのJWTユーザー・アサーションの前提条件ステップ

  1. 「JWTユーザー・アサーションの前提条件」から前提条件タスクを完了します。

  2. Oracle Integrationアプリケーションが必要なスコープについて検証されると、自己署名キー・ペアが生成され、機密アプリケーションが構成されます。scope値、private_key.pemClient ID、および Client Secretを書き留めます。

    ノート:機密アプリケーションで秘密キーを信頼できるパートナとしてインポートする際、自己署名キー・ペアの作成時に使用されているものと同じaliasを使用し、後のタスクのためにaliasを書き留めます。

  3. 動的グループを作成して、リソース・タイプfunctionを特定のコンパートメントからOCI Vaultサービスからシークレットを読み取れるようにします。

    イメージ 6

タスク3: OCI Vaultでのシークレットの作成

OCI Vaultの手動シークレット生成オプションを使用して、タスク1およびタスク2から収集されたシークレットを格納します。詳細は、Vaultでのシークレットの作成を参照してください。

イメージ 4

シークレットが作成されたら、「シークレット情報」セクションからOCID値をコピーし、後のタスク用に保存します。

イメージ 5

タスク4: func.pyファイルの作成と構成

Microsoft Entra IDアクセス・トークンを検証し、OCI IAMアクセス・トークンをback_end_tokenとして生成するために、カスタム認可プロバイダとしてOCI Functionsを使用します。

  1. 開始するには、アプリケーションを作成します。OCIファンクションでは、アプリケーションはファンクションの論理グループです。アプリケーションに指定するプロパティによって、そのアプリケーション内のすべての機能のリソース割当ておよび構成が決まります。詳細は、アプリケーションの作成を参照してください。

  2. アプリケーションが作成されたら、構成をアプリケーションに追加します。関数コードから次の項目を取得して、コードを変更せずに移植性と構成性を高めます。「キー」および「値」と入力し、「+」をクリックします。

    Microsoft Entra ID、OCIアイデンティティ・ドメイン、タスク3で収集されたシークレットのOCID、タスク2から収集されたエイリアス、スコープ、およびmicrosoft Entra IDトークンが検証されるグラフ・エンドポイントhttps://graph.microsoft.com/v1.0/meからクライアントIDを追加します。

    イメージ 7

  3. 関数を作成するには、スタート・ガイドに移動し、「OCI Cloud Shellの起動」をクリックして、ブラウザで対話型のLinuxスタイルのクラウド・シェルを開きます。OCI Cloud Shellがロードされると、OCI Cloud Shellからカスタム認可プロバイダOracle関数をすぐに作成、開発およびデプロイできます。

  4. Fn Projectコマンドライン・インタフェース(CLI)を使用して関数を作成するには、Python関数fn init --runtime python MyCustomAuthorizerに対して次のコマンドを入力し、[Enter]をクリックします。

    イメージ 8

  5. ファンクションのボイラープレートが作成され、カスタム認可プロバイダ・ロジックを含めるように編集できるようになりました。ディレクトリをファンクション・フォルダに変更し、func.pyファイルを編集します。次のコード・スニペットをコピーして貼り付けます。

    イメージ 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"}
                )
    
    
    • インポート

      • iojsonloggingdatetimetimebase64: I/O、JSONデータ、ロギング、日時操作およびbase64エンコーディングを処理するための標準Pythonライブラリ。
      • jwt: JSON Webトークン(JWT)のエンコーディングおよびデコード用のライブラリ。
      • requests: HTTPリクエストを行うためのライブラリ。
      • HTTPBasicAuth: HTTP Basic認証を処理するためのクラス。
      • serializationdefault_backend:暗号化操作の処理に使用される暗号化ライブラリから。
      • ociVault: OCI Vaultと対話するためのカスタム・モジュール。
    • グローバル変数

      • oauth_apps:アプリケーション構成を格納するディクショナリ。
    • 関数

      • initContext(context):このファンクションの目的は、OCI Vaultのコンテキスト・データおよびシークレットを使用してアプリケーション構成を初期化することです。メイン・ハンドラ・メソッドの最初のものとして呼び出されるコンテキスト・ディクショナリ・オブジェクトを受信し、タスク5で説明したgetSecret()関数を使用してOCI Vaultからシークレットを取得します。

      • getAuthContext(token, client_apps): OCI APIゲートウェイの認証コンテキストを移入して返します。アクセス・トークンを抽出およびデコードします。introspectToken()ファンクションをコールして、Entra IDでトークンを検証します。トークンが有効な場合は、認証コンテキストを設定し、getBackEndAuthToken()ファンクションをコールして、OCI IAMからバックエンド・トークンを取得し、有効期限を設定します。トークンが有効でない場合は、wwwAuthenticateヘッダーを設定して認証エラーを示します。

      • introspectToken(access_token, introspection_endpoint, client_id, client_secret):指定されたintrospection_endpointでトークンを検証します。トークンを使用してイントロスペクション・エンドポイントにGETリクエストを行います。イントロスペクションまたは検証エンドポイントからレスポンスを返します。Microsoft Entra IDにはOAuthイントロスペクションAPIエンドポイントがないため、入力として受信したトークンを使用して構成済エンドポイントを起動します。

      • getBackEndAuthToken(token_endpoint, client_id, client_secret, scope, alias, principal): PEMファイルから秘密キーをロードします。JWTクレームを作成し、JWTアサーションにエンコードします。トークン・リクエストのペイロードおよびヘッダーを準備します。バックエンド・トークンを取得するためにトークン・エンドポイントにPOSTリクエストを行い、バックエンド・トークンをgetAuthContext()関数に返します。

      • handler(ctx, data: io.BytesIO = None):ファンクションの実行を処理するメイン・ファンクション。initContext()関数を使用してOAuthコンテキストを初期化し、getAuthContext()関数をコールして認証コンテキストを取得します。トークンが有効な場合は 200応答を返し、それ以外の場合は 401応答を返します。エラーの場合、500応答を記録して返します。

タスク5: ociVault.pyファイルの作成と構成

同じフォルダにociVault.pyファイルを作成し、次のコード・スニペットを貼り付けます。このユーティリティ関数は、OCI Vaultサービスからシークレットを読み取ります。

# 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

ノート:タスク2のprivate_key.pemファイルを同じフォルダに保持します。

イメージ 10

タスク5: 関数のテスト

ファンクションをテストするには、ファンクションをデプロイし、Microsoft Entra IDトークンを入力として渡して起動する必要があります。

  1. 関数フォルダに移動し、次のコマンドfn -v deploy --app MyCustomAuthorizerを実行してデプロイします。Fn ProjectのCLIコマンドは、ファンクションを構築し、OCI Functionsアプリケーションに同じものをデプロイします。

    イメージ 11

    ノート: ファンクション・アプリケーションをデプロイする前に、requirements.txtファイルにfdk>=0.1.74requestsocipyjwtserializationを含めます。

  2. Postmanクライアントを使用して、OAuth 2.0 ROPCフローを使用してMicrosoft Entra IDからアクセス・トークンを生成します。

    イメージ 12

  3. アクセス・トークンを書き留めて、OCI関数をテストするための入力として渡されるpayload.jsonを生成します。JSONファイルは同じファンクション・ディレクトリに保管してください。

    イメージ 13

  4. ペイロードを保存したら、次のコマンドを実行して、OCI API Gateway (cat payload.json | fn invoke <AppName> <function name>)を介して起動されるファンクションの実行を模倣できます(次の図を参照)。

    イメージ 14

    Microsoft Entra IDトークンが有効な場合は、次のイメージに示すようにレスポンスが表示され、コンテキスト構造のback_end_token値にOCI IAMトークン値が表示されます。

タスク6: OCI APIゲートウェイの構成

OCI API Gatewayは、迅速なAPIデプロイメントからライフサイクル管理、バックエンド・サービス統合まで、一連のサービスを提供する、完全に管理されたスケーラブルなクラウドネイティブAPI管理プラットフォームです。APIゲートウェイを利用して、Microsoft Entra IDなどの外部アイデンティティ・プロバイダを使用してOracle Integrationの認可を仲介します。

まず、新しいAPIゲートウェイを作成してから、APIゲートウェイに新しいデプロイメントを作成します。

  1. 「開発者サービス」「API管理」および「ゲートウェイ」に移動します。次の情報を入力して、「ゲートウェイの作成」をクリックします。

    イメージ 15

    イメージ 16

  2. 「ゲートウェイの詳細」ページで、「デプロイメントの作成」をクリックし、APIデプロイメントの次の必須情報を入力します。

    • 名前:名前を入力します。
    • パス接頭辞:パスを定義します。
    • コンパートメント: APIデプロイメントに適したコンパートメントを選択します。

    イメージ 17

  3. 認証ポリシーの詳細を追加します。ここでは、カスタム認可プロバイダとして起動されるOCI関数を構成します。タスク4で作成した機能を選択します。

    イメージ 18

  4. 「ルート」ページで、バックエンド・サービスへのAPIルーティングを構成します。このチュートリアルでは、Oracle Integrationエンドポイントへのルーティングを定義します。

    イメージ 19

  5. 「ルート・リクエスト・ポリシーの表示」をクリックします。ここでは、ユーザーがOCI Functionsレスポンスからリクエストの認証ヘッダーへの認証トークンのスワップを実行します。

    イメージ 20

    このステップでは、バックエンド・アイデンティティ・プロバイダに基づいてバックエンド・サービスの認証トークンを設定します。このシナリオでは、カスタム認可プロバイダOCI関数から受信したOCI IAMのベアラー・トークンを設定します。ここでは、認可ヘッダーを値${request.auth[back_end_token]}でオーバーライドするように構成します。back_end_tokenは、Oracleファンクションのレスポンス構造のコンテキストの一部です。カスタム認可プロバイダOCI関数の完了後に、この式が正常に評価されることを確認します。

  6. 構成を正常に確認した後、「変更の保存」をクリックしてデプロイメントを保存し、デプロイメントの状態が「アクティブ」に変わるまで待機します。

    イメージ 21

    APIデプロイメントをアクティブ化した後、「デプロイメント情報」セクションから「エンドポイント」(ベースURL)をコピーします。このURLは、ビジネス・プロセスまたはアプリケーションがMicrosoft Entra ID Bearerトークンを使用してOracle Integrationエンドポイントを起動するデプロイメントのエンドポイントとして機能します。次のタスクではベースURLを使用します。

    イメージ 22

タスク7: APIのテスト

まず、Postmanクライアントを使用してMicrosoft Entra IDからアクセス・トークンを取得します。ROPCフローを使用して、アクセス・トークンに必要なアイデンティティ情報が含まれていることを確認します。

  1. APIゲートウェイからのAPIの起動時と同じように、アクセス・トークンをコピーします。

    イメージ 12

  2. 次の図に示すように、APIゲートウェイおよびOracle Integrationエンドポイントからタスク6でコピーされたベース・エンドポイントURLを組み合せた新しいRESTリクエストを作成します。リクエスト・ヘッダーでbearerトークンを使用します。

    イメージ 23

  3. 「送信」をクリックしてAPIリクエストを起動すると、Oracle Integrationが実行され、正常に出力されます。

    イメージ 24

次のステップ

Microsoft Entra IDからOAuthトークンを使用してOracle API GatewayでAPIを正常に起動し、Oracle Integration RESTトリガー・フローからレスポンスを受信しました。この統合は、異なるクラウド・ベンダー間でデジタル・サービスを接続するお客様にとって非常に重要です。

確認

その他の学習リソース

docs.oracle.com/learnの他のラボを確認するか、Oracle Learning YouTubeチャネルで無料のラーニング・コンテンツにアクセスしてください。また、education.oracle.com/learning-explorerにアクセスしてOracle Learning Explorerになります。

製品ドキュメントは、Oracle Help Centerを参照してください。