2024年1月29日月曜日

OpenAIのText to speech APIを呼び出して写真の説明を読み上げる

前回の記事「写真の動物の説明をOpenAI GPT-4 Turbo with visionにしてもらう」で作成したAPEXアプリケーションでは、写真の説明を文章として表示していました。この文章を、OpenAIのText to speech APIを呼び出して、音声に変換して読み上げるようにします。

作成したアプリケーションは以下のように動作します。


ORDSのREST APIにテンプレートとしてanimal-speechを作成し、POSTハンドラに以下のコードを記述します。パッケージUTL_OPENAI_VISIONに、OpenAIのspeechエンドポイントを呼び出すプロシージャとしてSPEECHを追加し、それをコード中で呼び出しています。


APEXアプリケーションのアプリケーション定義の置換文字列G_REQUEST_URLを、新しく作成した音声を返すRESTサービスの完全なURLに置き換えます。


リージョンResponseに音声要素を埋め込みます。

ソースHTMLコードに以下を記述します。

<audio controls id="my-audio"></audio>


写真の選択を変更したときに、応答の文章であるページ・アイテムP1_RESPONSEをクリアする代わりに、音声要素を非表示にします。

TRUEアクションのクリアを非表示に変更し、影響を受ける要素選択タイプJavaScript式を選択し、JavaScript式として以下を記述します。

document.getElementById("my-audio")


選択タイプjQueryセレクタを選択し、jQueryセレクタとして#my-audioを指定することもできます。

ボタンSUBMIT(ラベルは問い合わせる)を押した時に、音声要素を表示するTRUEアクションを追加します。

TRUEアクション表示影響を受ける要素は先ほどの非表示のアクションと同じです。ページが最初に表示されるときは音声要素は表示しないため、初期化時に実行オフにします。


TRUEアクションJavaScriptコードの実行設定コードを、以下に記述に変更します。macOSのChromeで実行するとelem.play()を呼び出した時点で再生が開始されますが、iPhoneのSafariでは、elem.play()では再生されないようです。再生ボタンをクリックする必要がありました。

影響を受ける要素選択タイプ- 選択 -に戻し、無指定にします。


ページ・アイテムP1_RESPONSEを削除します。


以上で写真の説明を音声として読み上げる実装ができました。

少しだけAPEXアプリケーションを改良します。

ページ・アイテムP1_IMAGEに表示されているアイコンを変更してみます。

FontAPEXのページを開きます。


cameraのアイコンを検索します。fa-cameraを開きます。


表示したいアイコンを修飾し、HTMLまたはCSSとしての記述を取得します。今回は、SizeLargeScale5xを選択します。Iconの記述は以下になります。

fa-camera fa-5x fa-lg

Iconの記述をコピーします。


ページ・アイテムP1_IMAGE外観アイコンに、コピーした文字列を設定します。


以上でアプリケーションは完成です。実行すると記事の先頭のGIF動画のように動作します。

OpenAIのText-to-speechのドキュメントに記載があるように、現在は英語に最適化されているようです。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/text-to-speech.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。

2024年1月26日金曜日

写真の動物の説明をOpenAI GPT-4 Turbo with visionにしてもらう

以前にこちらの記事「Google Gemini Pro Visionを呼び出して写真の動物を説明してもらうアプリを作る」で作成したAPEXアプリケーションの問い合わせ先を、Google Gemini Pro VisionからOpenAI GPT-4 Turbo with visionに変えてみました。

APEXアプリケーション自体は変更せず、ORDS REST APIの呼び出し先だけを変更します。

PCでアプリケーションを実行してみました。


OpenAI GPT-4 Turbo with visionを呼び出すパッケージUTL_OPENAI_VISIONを作成します。パッケージのコードは以下になります。

ORDS REST APIのPOSTハンドラのコードを以下に置き換えます。以下のコードでは、OpenAIのAPIを呼び出すWeb資格証明OPENAI_API_KEYとして登録されていることを前提にしています。



APEXアプリケーションではORDSのREST APIの呼び出しURLを、置換文字列G_REQUEST_URLに設定しています。Google Geminiを呼び出すPOSTハンドラを直接置き換えるよりは、新たにOpenAIを呼び出すモジュールを作成し、そのURLを呼び出すようにG_REQUEST_URLを書き換える方がよいでしょう。

OpenAI GPT-4Vの場合、プロンプトに回答は小学生にわかるような文章でお願いします。を追加したら、それなりに柔らかい回答になりました。


以上になります。

Oracle APEXのアプリケーション作成の参考になれば幸いです。

OpenAIのDALL-Eを呼び出して画像を生成するAPEXアプリケーションを作る

OpenAIのDALL-Eを呼び出して画像を生成するAPEXアプリケーションを作ってみました。

アプリケーションを動かすには、OpenAIのAPIを呼び出すためのAPIキーをAPEXのWeb資格証明として作成済みであることと、DALL-EのAPI呼び出しは課金されるので、購入したクレジットが残っていることが必要です。

作成したAPEXアプリケーションは以下のように動作します。


空のAPEXアプリケーションを作成し、デフォルトで作成されるホーム・ページに機能を実装しています。

以下より作成したアプリケーションを簡単に紹介します。

モデルを選択するページ・アイテムP1_MODELを作成しています。タイプ選択リストラベルmodelです。

LOVタイプ静的値として、dall-e-2dall-e-3を選択できるようにしています。デフォルトdall-e-2にしています。


サイズを選択するページ・アイテムP1_SIZEも同様に、LOVタイプ静的値として、1024x10241024x17921792x1024のどれかを選択するようにしています。デフォルト1024x1024です。

この他にも指定可能なパラメータnqualitystyleなど)はありますが、これだけ設定していれば最低限の画像は生成できるので、他は省略します。


プロンプトを入力するページ・アイテムP1_PROMPTを作成します。ラベルPromptタイプテキスト領域を選択します。


DALL-Eによる画像生成を呼び出すボタンGENERATEを作成します。

外観ホットオンにし、テンプレート・オプションWidthStretchを選択します。

動作アクションはデフォルトのページの送信とします。


OpenAIのCreate Image APIは、リクエストのresponse_formatで明示的にb64_jsonを指定していないときは、生成された画像が置かれている位置のURLを返します。

返されたURLを保持するページ・アイテムP1_IMAGEを作成します。タイプイメージの表示を選択します。

設定基準ページ・アイテム値に格納されたイメージURLを選択することにより、ページ・アイテムの値であるURLより取得できる画像をページに表示します。


デバッグ用にCreate Image APIのレスポンスをそのまま表示するページ・アイテムを作成します。

識別名前P1_RESPONSEタイプテキスト領域とします。


ボタンGENERATEがクリックされたときに、OpenAIのDALL-Eを呼び出すプロセスを作成します。

識別名前Generate Imageとします。タイプコードを実行です。ソースPL/SQLコードとして以下を記述します。


サーバー側の条件ボタン押下時GENERATEを指定します。


コードに含まれるバインド変数G_REQUEST_URLG_CREDENTIALの値を、アプリケーション定義置換に設定します。

G_CREDENTIALには、OpenAIのAPIキーが設定されたWeb資格証明静的IDを設定します。G_REQUEST_URLはAPIのエンドポイントである以下のURLを設定します。

https://api.openai.com/v1/images/generations


以上でアプリケーションは完成です。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/sample-openai-dall-e.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。

2024年1月25日木曜日

Google Gemini Pro Visionを呼び出して写真の動物を説明してもらうアプリを作る

スマホで撮影した写真に写っている動物について、Google Gemini Pro Visionを呼び出して説明してもらう簡単なアプリを作成してみました。

Google Gemini Pro Visionを呼び出すために、以前の記事「Google Geminiを呼び出すAPEXアプリケーションを作る」で紹介しているパッケージUTL_GOOGLE_GEMINI_APIを使っています。Google AI Studioから取得したAPIキーWeb資格証明として使っています。Google Geminiの扱いについてはすでにパッケージに実装済みであるため、本記事の内容には含まれていません。

本記事では主にSPA(Single Page Application)のAPEXアプリケーションの作り方を紹介します。

作成したAPEXアプリケーションは以下のように動作します。


以下よりAPEXアプリケーションの作り方を紹介します。

最初にAPEXのアプリからアップロードされる写真データを受け取って、Gemini Pro Visionを呼び出すORDSのRESTサービスを作成します。

モジュールテンプレートを作成し、以下のコードをソースとしたPOSTメソッドのハンドラを作成します。今回の記事ではモジュール名Google Gemini Pro Visionモジュール・ベース・パス/gemini-pro-vision/テンプレートanimalとしています。完全なURLはあらかじめ記録しておきます。後で、APEXアプリケーションの置換文字列G_REQUEST_URL置換値として設定します。



ORDSのRESTサービスはAPEXのセッションで保護します。

ロールとしてRESTful ServicesモジュールとしてGoogle Gemini Pro Visionを割り当てた権限を作成します。


以上でRESTサービスの準備はできました。

アプリケーション作成ウィザードを起動します。作成するアプリケーションの名前この動物は何?としています。Google Gemini Pro Visionを呼び出す際にプロンプトとして「この画像に写っている動物の名前と生息地および生態を教えてください。」を与えていますが、プロンプトはどのように変更してもよいので、プロンプトに合わせてアプリの名前を変えても良いでしょう。

スマホにインストールして使用することを想定しているため、機能プログレッシブWebアプリケーションのインストールチェックします。


アプリケーションが作成されます。すべての機能はホーム・ページに実装します。

アプリケーション定義置換を開き、置換文字列G_REQUEST_URL置換値として、ORDSのRESTサービスの完全なURLを設定します。APEXアプリケーションとORDSのRESTサービスは同じワークスペースに実装されていることが前提なので(そうでないとAPEXセッションで認証できない)、プロトコルやホストを除いて設定します。


ページ・デザイナホーム・ページを開きます。

ブレッドクラムはいらないので削除します。


写真を保持するページ・アイテムを画面の中央に配置するため、静的コンテンツのリージョンを作成します。タイトルRequestとします。

外観テンプレートとしてBlank with Attributes (No Grid)を選択し、CSSクラスとしてu-flex u-justify-content-centerを設定します。このCSSクラスはAPEXのUniversal Themeにより定義されています。説明は、Universal ThemeのLayout Modifiersのリファレンスを参照してください。


Gemini Pro Visionの応答を表示するページ・アイテムも画面中央に配置するため、同じ設定で静的コンテンツのリージョンを作成します。タイトルResponseとします。


ボタンSUBMITを作成します。

ラベル問い合わせるとします。外観ホットオンにし、CSSクラスにはbutton-submitを設定します。このCSSクラスは後ほど定義し、問い合わせボタンを画面下部に固定します。

動作アクション動的アクションで定義を選択します。SPAのアプリを作成するときは、ほとんどの処理を動的アクションとJavaScriptで実装することになります。


リージョンResponseの下に、Google Gemini Pro Visionの応答を保持するページ・アイテムP1_RESPONSEを作成します。タイプリッチ・テキスト・エディタを選びます。

テンプレートhiddenを選択します。テンプレート・オプションLeft MarginRight MarginLargeを指定します。セッション・ステートデータ型CLOBを選択し、ストレージリクエストごと(メモリーのみ)を選択します。ページが送信されなければ、セッション・ステートにページ・アイテムの値が保存される機会はありません。そのためストレージセッションごと(永続)を選んでも動作は変わりません。

読取り専用タイプ常時を指定します。


リージョンRequestの下に送信する写真を保持するページ・アイテムP1_IMAGEを作成します。タイプイメージ・アップロードを選択します。

表示表示形式としてアイコン・ドロップ・ゾーンを選択します。アイコンとして写真がプレビューされます。プレビュー・サイズ特大にします。キャプチャに使用メイン・カメラを選択します。アプリがスマホで実行されているときは、ファイル選択のダイアログが開く代わりに、背面カメラを起動するようにします。

ストレージファイルをパージするタイミングリクエストの終わりタイプとして表APEX_APPLICATION_TEMP_FILESを選択します。ただし、ページの送信は行わないため、イメージが表APEX_APPLICATION_TEMP_FILESに保存されることはありません。

サイズ変更最大ファイル・サイズ1000を入力します。トリミングトリミングの許可オンにし、アスペクト比として1:1(正方形)を選択します。

トリミングオンにしていると写真のデータはPNG形式に変換され最大ファイル・サイズに収まるように解像度を落としてサイズを縮小します(JPEGがPNGに変換されるのは現行のAPEXのバージョンの不具合かも知れず、今後のバージョンで動作が変わる可能性もあります)。Google Gemini Pro Visionにリクエストを送信する際に、イメージのサイズおよび送信するリクエストのサイズが制限を超えないようにしています。

外観テンプレートとしてHiddenセッション・ステートストレージとしてリクエストごと(メモリーのみ)を選択します。


ページ・アイテムP1_IMAGEに動的アクションを作成し、値が変更されたときにページ・アイテムP1_RESPONSEクリアします。

動的アクションのタイミング変更です。


TRUEアクションクリアを選択し、影響を受ける要素として選択タイプアイテムアイテムとしてP1_RESPONSEを指定します。


TRUEアクションを追加します。写真が変更されたときに、ボタンSUBMIT表示します。

アクション表示影響を受ける要素選択タイプボタンボタンとしてSUBMITを選択します。初期化時に実行オフにします。


ボタンSUBMITをクリックしたときに実行される動的アクションを作成します。

タイミングイベントクリックです。


TRUEアクションとしてJavaScriptコードの実行を選択します。設定コードに以下を記述します。

影響を受ける要素選択タイプアイテムアイテムとしてP1_RESPONSEを選択します。Gemini Pro Visionのレスポンスを表示するページ・アイテムを指定しています。


TRUEアクションを追加します。写真を送信した後は、ボタンSUBMIT非表示にします。

アクション非表示影響を受ける要素選択タイプボタンボタンとしてSUBMITを選択します。ページが初めて表示されたときに非表示とするため、初期化時に実行オンにします。


以上でアプリケーションの動作の部分は実装できました。


フッター部分を非表示にするのと、ボタンを修飾するCSSを、ページ・プロパティCSSインラインに記述します。


ページ・プロパティ外観ページ・テンプレートMinimal (No Navigation)に切り替えます。また、テンプレート・オプションDeferred Page Renderingオンにします。この設定により、ページ・ロード時に一瞬ボタンSUBMITが表示される現象を抑制します。


アプリケーションに移り、テーマ・ローラを開いて見かけをカスタマイズします。

プライマリ・アクセント色ボタンの枠線の角の丸め20pxホットの色をに変更しています。このあたりはお好みで設定されるとよいでしょう。

変更したテーマを保存します。


以上でアプリケーションは完成です。実行すると記事の先頭のGIF動画のように動作します。

写真のデータは一旦APEXにアップロードされ、その後、Geminiに渡されます。そのため、データはブラウザからORDSとオラクル・データベースを通ります。単にGeminiを呼び出すだけであれば、ブラウザから直接APIを呼び出せば、ORDSからデータベースへのアクティブな接続が不要になります。通信するデータ量が多いためCPU負荷は低くてもセッションは長時間アクティブなままになります。データベースへの負荷を減らすには、写真はオブジェクト・ストレージにアップロードして、ファイルの位置をAPEXに渡すといった実装も考慮する必要があります。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/ask-animal.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。

2024年1月19日金曜日

ORDS 23.3で追加されたOAUTH.CREATE_JWT_PROFILEを使ってRESTサービスを保護する

Oracle REST Data Services 23.3から、サードパーティのIdPが生成したOAuth2のアクセス・トークンによる保護が実装されています。

ドキュメントの以下に説明が記載されています。

2.8 JWT Bearer Token Authentication and Authorization Using JWT Profile

保護を設定するために、OAUTH.CREATE_JWT_PROFILEを呼び出します。

Oracle APEXのアプリケーションの認証スキームをOpen ID Connectで構成して、動作を確認してみました。

認証プロバイダとしては、Okta、Microsoft Entra IDそれとOracle IDCSを試してみました。Oracle IDCSとOktaは動作させることができましたが、Microsoft Entra IDはできませんでした。(Microsoft Entra IDでの確認については、こちらの記事を参照してください。)

以下より、動作確認の手順を紹介します。この保護はどのように使うものか、確認手順から理解していきます。

最初に保護の対象とするORDSのRESTサービスを作成します。このRESTサービスは、このRESTサービスを呼び出すAPEXアプリケーションとは異なるインスタンスに作成します。APEXアプリケーションと同じインスタンス(厳密にいうとワークスペース・スキーマ)のRESTサービスであれば、こちらの記事で紹介しているApex-Sessionヘッダーによる認証を使うことができます。

以下のRESTfulデータ・サービスを作成します。

モジュール名: print
モジュール・ベース・パス: /print/
URLテンプレート: env
メソッド: GET
ソース・タイプ: PL/SQL

ソースにとして以下を記述します。

最初にバインド変数current_userの値を表示しています。current_userの値はJWTのサブジェクト(属性sub)の値が設定されることになっています。また、この値はCGI変数のREMOTE_IDENTにも設定されます。

続けて、RESTサービスが受け取ったHTTPリクエストのヘッダーを出力します。
begin
    htp.p('<p>' || coalesce(:current_user, 'no_current_user') || '</p>');
    owa_util.print_cgi_env;
end;

完全なURLをブラウザで開いて、HTTPヘッダーの情報が表示されることを確認します。

認証が必要な保護がかけられていないため、バインド変数current_userはNULLになっています。同様にCGI変数REMOTE_IDENTも空白になっています。


権限を作成し、作成したRESTサービスprintを保護します。

権限の名前openidとします。権限の名前はOAuth2のアクセス・トークン(=JWT)のスコープ(属性名としてはscope)に含まれている必要があります。本来であれば独自のスコープとなる名前にすべきですが、そうするとIdPに、この権限をカスタム・スコープとして作成しなければなりません。今回はRESTサービスの保護が確認できればよいので、Open ID Connectのアクセス・トークンに必ず含まれているopenidを権限の名前にします。

タイトルは任意の文字列です。

ロールとしてRESTful Services保護されたモジュールprintを選択します。

以上の設定で権限を作成します。


再度、完全なURLにアクセスします。HTTP 401、Unauthorizedのエラーが返されます。


Oracle IDCSにアプリケーションを登録します。

OCIコンソールのアイデンティティとセキュリティよりフェデレーションを開きます。


OracleIdentityCloudServiceを開きます。


Oracle Identity Cloud Service Consoleを開きます。

このコンソールのURLに含まれるホスト部分は、Open ID Connectの検出URLやJSON Web Key (JWK) URL - JWTの署名の検証に使う公開鍵を取得するURL - のホスト部分になるため、メモしておきます。


IDCSの画面が開きます。


最初に設定デフォルト設定を開き、署名証明書へのアクセスをOracle Identity Cloud Serviceにログインせずにできるように、スイッチをオンに切り替えます。IDCSのデフォルトでは、JWK URLへのアクセスは保護されています。

Microsoft Entra IDやOktaでは、JWK URLはデフォルトで保護なしでした。


アプリケーション追加を行います。認証プロトコルにはOpen ID Connectを使います。

あらかじめAPEX側を呼び出すコールバックURLを確認しておきます。Open ID Connectの場合は末尾がapex_authentication.callbackになります。

https://ホスト名/パス/apex_authentication.callback


機密アプリケーションを選択します。


動作確認に必要な、最低限の設定を行います。

アプリケーションの名前ORDS JWT Testとします。

へ進みます。


認可許可される権限付与タイプ認可コードチェックを入れます。

リダイレクトURLを入力し、へ進みます。


リソース・サーバーの設定はスキップします。

へ進みます。


Web層ポリシーの構成もスキップします。

へ進みます。


認可についても設定せず、終了をクリックします。


アプリケーションが追加されます。クライアントIDクライアント・シークレットが表示されるので、これをコピーします。APEX側でWeb資格証明を作成するときに使用します。


作成したアプリケーションがアクティブ化されていない場合は、右端のメニューよりアクティブ化をします。


アプリケーションがアクティブ化されれば、IdP側の準備は完了です。


APEX側の作業を行います。

IDCSが発行するトークンを保存するWeb資格証明を作成します。

名前Oracle IDCS JWT Test静的IDORACLE_IDCS_JWT_TESTとします。

認証タイプとしてOAuth2クライアント資格証明フローを選択し、クライアントIDまたはユーザー名およびクライアント・シークレットまたはパスワードとして、IDCSへのアプリケーション登録時に表示されたクライアントIDクライアント・シークレットを設定します。


空のAPEXアプリケーションを作成します。名前ORDS JWT Testとします。

ホーム・ページに、IdPが返すレスポンスとREST APIのレスポンスを表示します。

ホーム・ページは以下のように表示されます。IdPが返すaccess_tokenと、REST APIのレスポンスに含まれるAuthorizationヘッダに含まれるBearerトークンは、同じ文字列になります。


作成したアプリケーションには記事「Open ID ConnectまたはOAuth2による認証の応答を印刷する」で紹介している、デバッグのための仕組みを組み込みます。ファンクションdump_signon_responseが作成済みとします。

ファンクションdump_signon_responseの出力を保存するアプリケーション・アイテムとしてG_USER_INFOを作成します。


アプリケーション定義置換置換文字列としてG_REST_URLを設定します。置換値は、別インスタンスに作成したRESTサービスprint/envを呼び出す完全なURLです。


Oracle IDCSをIdPとした認証スキームを作成します。

名前Oracle IDCSとします。スキーム・タイプソーシャル・サインインです。

設定資格証明ストアとしてOracle IDCS JWT Test認証プロバイダOpenID Connectプロバイダを選択します。

Oracle IDCSのテナント識別子を含む、以下の形式のURLを検出URLとして設定します。

https://[IDCSテナント識別子].identity.oraclecloud.com/.well-known/openid-configuration

有効範囲profileユーザー名#sub#を設定します。有効範囲はスコープのことですが、openidはデフォルトで追加されるため、有効範囲に含める必要はありません。


IdPのレスポンスをアプリケーション・アイテムG_USER_INFOに保存するコードを、ソースPL/SQLコードに記述し、ログイン・プロセス認証後のプロシージャ名post_authを設定します。
procedure post_auth is
begin
  :G_USER_INFO := dump_signon_response;
end post_auth;

以上で、Oracle IDCSをIdPとして使用する認証スキームの設定ができました。

ホーム・ページにIdPのレスポンスを表示するページ・アイテムを作成します。

名前P1_USER_INFOタイプとしてテキスト領域を選択します。外観高さ20を設定し、テキスト領域の高さを大きくしておきます。

ソースタイプアイテムを選択し、アイテムとしてG_USER_INFOを指定します。データ型CLOBを選択します。


REST APIのレスポンスを表示するリージョンを作成します。

識別タイトルREST API Responseタイプとして動的コンテンツを選択します。

ソースCLOBを返すPL/SQLファンクション本体に、以下のコードを記述します。
declare
    l_static_id varchar2(80);
    l_response clob;
begin
    /* get static id of web credentials of current authentication scheme */
    select c.static_id into l_static_id
    from APEX_APPLICATION_AUTH a join APEX_WORKSPACE_CREDENTIALS c on a.attribute_01 = c.credential_id
    where a.application_id = :APP_ID and a.is_current_authentication = 'Y';
    apex_web_service.clear_request_headers();
    l_response := apex_web_service.make_rest_request(
        p_url => :G_REST_URL
        ,p_http_method => 'GET'
        ,p_credential_static_id => l_static_id
    );
    return l_response;
end;
表示されるデータ量が多いため、テンプレート・オプションBody Heightとして320pxにしています。


以上でアプリケーションは完成です。

アプリケーションを実行すると、Oracle Cloudへのサインインが要求されます。


ユーザー認証に成功すると、作成したアプリケーションのホーム・ページが開きます。

User Infoのページ・アイテムにaccess_tokenが含まれています。REST API ResponseにはUnauthorizedのエラーが表示されています。


https://jwt.ioデバッガを開き、User Infoに含まれているaccess_tokenをデコードします。Encodedaccess_tokenの値をペーストします。

HEADERに含まれるkidは、認証がうまくいかないときに確認することがあります。


PAYLOADからOAUTH.CREATE_JWT_PROFILEの呼び出しに使用する値を見つけます。

属性issは引数p_issuerの値になります。属性audは引数p_audicenceの値になります。

今回は権限の名前openidとしているため気にする必要はありませんが、属性scopeに権限の名前が含まれていることも確認します。


引数p_jwk_urlに与える値は、認証スキームOracle IDCSの検出URLにアクセスして、レスポンスに含まれる属性jwks_uriから取得できます。

通常は以下の形式になります。

https://[IDCSテナント識別子].identity.oraclecloud.com:443/admin/v1/SigningCert/jwk


RESTサービスを実装したインスタンスのSQLコマンドを開いて、OAUTH.CREATE_JWT_PROFILEを実行します。
begin
    oauth.create_jwt_profile(
        p_issuer => 'issの値'
        ,p_audience => 'audの値'
        ,p_jwk_url => 'jwks_uriの値'
    );
    commit;
end;

OAUTH.CREATE_JWT_PROFILEを実行した後に、APEXアプリケーションのホーム・ページを再読み込みすると、JWTによる認証に成功してREST API Responseが正常に表示されます。


認証がうまくいかない場合は、引数p_jwk_urlとして与えたJWK URLを直接開いてみます。IDCSの設定署名証明書へのアクセスオンにするのを忘れていると、認証エラーが発生して公開キーの取得に失敗します。

Oracle IDCSの場合はkidが固定値のSIGNING_KEYであるため気にする必要はありませんが、他のIdPではJWKのkidと、access_tokenのヘッダーに含まれるkidが一致していることも確認すべき点です。


以上が、ORDS 23.3で追加されたOAUTH.CREATE_JWT_PROFILEを使う設定の紹介になります。

Oracle IDCSでは属性subの値がメール・アドレスであるため、current_userもメール・アドレスになりますが、IdPによってはsubの値がGUIDのような数値の場合もあります。APEXのAPP_USERは大体メール・アドレスとするため、属性emailAPP_USERにするように設定しますが、OAUTH.CREATE_JWT_PROFILEによる保護では今のところcurrent_userを属性emailの値にすることはできなさそうです。


Oktaでの確認



サイド・メニューのApplicationsからApplicationを開き、アプリケーションを作成します。

Create App Integrationをクリックします。


Sign-in methodOIDC - OpenID Connectを選択します。Application typeWeb Applicationを選択します。

Nextに進みます。


App integration nameORDS JWT Testとします。

Sign-in redirect URIsにはAPEXを呼び出すコールバックURLを設定します。Sign-out redirect URIsX をクリックして削除し、無設定にします。

今回は単にテストなので、AssignmentsControlled accessAllow everyone in your organization to accessを選択します。

以上でSaveします。


アプリケーションORDS JWT Testが登録されます。

Client IDSecretをコピーします。APEX側でWeb資格証明を作成するときに使います。


Oktaに場合、defaultのAuthorization Serverではアクセス・トークンのkidとJWK URLのkidが一致しませんでした。そのため、SecurityAPIよりAuthorization Serverを追加しています。


追加したAuthorization ServerNameORDSAudienceordsとしています。Metadata URIはOpenID Connectの検出URLになるため、コピーして保存しておきます。


Access Policiesのタブを開き、Add New Access Policyをクリックして、Assigned toAll Clientsとするポリシーを作成します。以下ではNameDescriptionともにAll Clientとしたポリシーを作成しています。


ほとんどすべてを許可するルールを追加しています。


以上でOkta側の準備は完了です。

APEX側でWeb資格証明を作成します。名前Okta JWT Testとします。

クライアントIDまたはユーザー名およびクライアント・シークレットまたはパスワードには、Oktaに作成したアプリケーションのClient IDSecretを設定します。


認証スキームOracle IDCSをコピーして、OktaをIdPとする認証スキームOktaを作成します。

資格証明ストアOkta JWT Test検出URLAuthorization ServerMetadata URIに変更します。以下のような形式になります。

https://[Oktaの認証サーバー]/[認証サーバーのID]/.well-known/oauth-authorization-server

変更を適用した後、カレント・スキームに変更します。


以上でOktaをIdPとした認証スキームへの切り替えが完了しました。

ブラウザを再起動し、APEXアプリケーションを実行します。

Oktaへのサインインが求められます。


APEXアプリケーションの画面が開きます。サインインには成功しているため、access_tokenは表示されますが、REST APIはUnauthorizedになります。


今までの作業と同様に、access_tokenをhttps://jwt.ioのデバッガにかけてissaudを取得します。

これらの値は、Authorization ServerAudienceIssuer URIの値でした。


Authorization ServerのMetadata URIをアクセスし、jwks_uriの値を取得します。以下のような形式になります。

https://[認証サーバーのID].okta.com/oauth2/[Authorization ServerのID]/v1/keys

これらの値を与えて、OAUTH.CREATE_JWT_PROFILEを実行します。


OktaでもREST APIは認証されません。

Oktaのアクセス・トークンのスコープも属性scpで渡されている上、スコープが空白区切りではなく、JSON配列になっています

Oktaの開発者フォーラムにscope claimを追加する方法が紹介されていました。
Scope/scp and space delimited string in access token

Authorization ServerClaimsタブを開き、Add Claimを実行します。


追加するClaimのNamescopeになります。Include into token typeAccess TokenValue typeExpressionを選択し、Valueとして以下を記述します。

String.replace(Arrays.toCsvString(access.scope),","," ")

Saveをクリックします。


上記の手順にてscopeを追加すると、REST APIの認証に成功しました。


今回の検証で使用したAPEXアプリケーションのエクスポートを以下に置きました。OpenID Connectの検出URLと呼び出すREST APIのURLは伏せ字にしているため、環境に合わせて設定し直す必要があります。
https://github.com/ujnak/apexapps/blob/master/exports/ords-jwt-test.zip

以上になります。

Oracle APEXのアプリケーション作成の参考になれば幸いです。