ドキュメントの以下に説明が記載されています。
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はそもそも、ドキュメントの2.8.4 JWT Identity Provider Detailsに記載がないので、仕方ないのかもしれません。
以下より、動作確認の手順を紹介します。この保護はどのように使うものか、確認手順から理解していきます。
最初に保護の対象とする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のエラーが返されます。
OCIコンソールのアイデンティティとセキュリティよりフェデレーションを開きます。
OracleIdentityCloudServiceを開きます。
Oracle Identity Cloud Service Consoleを開きます。
このコンソールのURLに含まれるホスト部分は、Open ID Connectの検出URLやJSON Web Key (JWK) URL - JWTの署名の検証に使う公開鍵を取得する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を入力し、次へ進みます。
リソース・サーバーの設定はスキップします。
次へ進みます。
次へ進みます。
アプリケーションが追加されます。クライアントIDとクライアント・シークレットが表示されるので、これをコピーします。APEX側でWeb資格証明を作成するときに使用します。
作成したアプリケーションがアクティブ化されていない場合は、右端のメニューよりアクティブ化をします。
APEX側の作業を行います。
IDCSが発行するトークンを保存するWeb資格証明を作成します。
名前はOracle IDCS JWT Test、静的IDはORACLE_IDCS_JWT_TESTとします。
認証タイプとしてOAuth2クライアント資格証明フローを選択し、クライアントIDまたはユーザー名およびクライアント・シークレットまたはパスワードとして、IDCSへのアプリケーション登録時に表示されたクライアントIDとクライアント・シークレットを設定します。
空のAPEXアプリケーションを作成します。名前はORDS JWT Testとします。
ホーム・ページに、IdPが返すレスポンスとREST APIのレスポンスを表示します。
ホーム・ページは以下のように表示されます。IdPが返すaccess_tokenと、REST APIのレスポンスに含まれるAuthorizationヘッダに含まれるBearerトークンは、同じ文字列になります。
ファンクション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のエラーが表示されています。
属性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が一致していることも確認すべき点です。
Oracle IDCSでは属性subの値がメール・アドレスであるため、current_userもメール・アドレスになりますが、IdPによってはsubの値がGUIDのような数値の場合もあります。APEXのAPP_USERは大体メール・アドレスとするため、属性emailをAPP_USERにするように設定しますが、OAUTH.CREATE_JWT_PROFILEによる保護では今のところcurrent_userを属性emailの値にすることはできなさそうです。
アプリケーション(クライアント)IDは、APEXのWeb資格証明の作成時にクライアントIDとして与える値になります。コピーして保存しておきます。
説明を入力し、追加をクリックします。
作成されたシークレットの値は、APEXのWeb資格証明の作成時にクライアント・シークレットとして与える値になります。コピーして保存しておきます。
Microsoft Entra IDでの作業は以上です。
"scope":"email profile phone address"
Sign-in methodにOIDC - OpenID Connectを選択します。Application typeはWeb Applicationを選択します。
アプリケーションORDS JWT Testが登録されます。
追加したAuthorization ServerのNameはORDS、Audienceはordsとしています。Metadata URIはOpenID Connectの検出URLになるため、コピーして保存しておきます。
これらの値を与えて、OAUTH.CREATE_JWT_PROFILEを実行します。
Authorization ServerのClaimsタブを開き、Add Claimを実行します。
Microsoft Entra IDでの確認
認証できなかったですが、Microsoft Entra IDでの作業を記録しておきます。
Microsoft Entra IDでアプリケーションを登録します。アプリケーションの名前はORDS JWT Testとしました。リダイレクトURIはOracle IDCSでの作業と同じ値を与えます。
証明書またはシークレットの追加をクリックし、シークレットを作成します。
新しいクライアントシークレットをクリックします。
概要に戻り、エンドポイントを開きます。
OpenID Connectメタデータドキュメントの値をコピーして保存します。この値が、APEXアプケーションの認証スキームを作成する際の検出URLになります。
Microsoft Entra IDが発行するトークンを保存するWeb資格証明を作成します。
名前はMicrosoft Entra ID JWT Test、静的IDはMS_ENTRA_JWT_TESTとします。
認証タイプとしてOAuth2クライアント資格証明フローを選択し、クライアントIDまたはユーザー名およびクライアント・シークレットまたはパスワードとして、Microsoft Entra IDのアプリのアプリケーション(クライアント)IDとシークレットを設定します。
作成済みの認証スキームOracle IDCSをコピーして、Microsoft Entra IDをIdPとする認証スキームを作成します。
共有コンポーネントの認証スキームを開き、作成をクリックします。
スキームの作成として既存の認証スキームのコピーとしてを選びます。
次へ進みます。
アプリケーションからコピーに、このアプリケーション(ORDS JWT Test)を選択します。
次へ進みます。
コピー元がOracle IDCSのコピー先をMicrosoft Entra IDとし、コピーをはいに変更します。
スキームのコピーをクリックします。
認証スキームMicrosoft Entra IDがOracle IDCSのコピーとして作成されました。
編集画面を開いて、Microsoft Entra IDをIdPとするように変更します。
資格証明ストアを先ほど作成したWeb資格証明Microsoft Entra ID JWT Testに変更し、検出URLもMicrosoft Entra IDのOpenID Connectメタデータドキュメントの値に変更します。
有効範囲はprofile,email、ユーザー名は#email#に変更します。
以上で変更の適用を行い、その後にカレント・スキームに変更します。
以上で認証スキームをMicrosoft Entra IDを使うように切り替えました。
Oracle IDCSに紐づいたクッキーを取り消すため、ウィンドウを閉じるだけではなく、ブラウザを起動し直します。
APEXアプリケーションを実行すると、Microsoft Entra IDによるサインインが求められます。
サインインに成功すると、APEXアプリケーションの画面が開きます。
IDCSのときと同様にaccess_tokenをjwt.ioのデバッガにかけて、PAYLOADからissとaudの値を取得します。
私の環境に依存しているのか不明なのですが、属性issは以下の値でした。
https://sts.windows.net/テナントID/
属性audは以下の値でした。
00000003-0000-0000-c000-000000000000
Microsort Entra IDのOpenIDメタデータカタログの検出URLから確認できるjwks_uriは以下のような形式です。しかしこのURLのレスポンスには、access_tokenのヘッダーに含まれるkidと一致するkidが含まれていません。
https://login.microsoftonline.com/テナントID/discovery/v2.0/keys
access_tokenのヘッダーに含まれるkidに一致するkidが得られるjwks_uriは以下でした。
https://sts.windows.net/テナントID/discovery/keys
JWTによる保護を切り替えます。最初に以下のコードを実行し、今までの保護を削除します。
begin
oauth.delete_jwt_profile;
end;
Microsoft Entra IDのアクセス・トークンから得たiss、aud、jwks_uriの値を使って、OAUTH.CREATE_JWT_PROFILEを呼び出し、JWTによる保護をかけます。
以上で設定は完了ですが、REST APIのアクセスは許可されません。
Microsoft Entra IDが生成するアクセス・トークンでは、スコープの値が属性scopeではなく、属性scpになっている点が気になります。RFC8693 4.2. "scope" (Scopes) Claimの例は以下になっています。
"scope":"email profile phone address"
Oktaでの確認
サイド・メニューのApplicationsからApplicationを開き、アプリケーションを作成します。
Create App Integrationをクリックします。
Nextに進みます。
App integration nameはORDS JWT Testとします。
Sign-in redirect URIsにはAPEXを呼び出すコールバックURLを設定します。Sign-out redirect URIsは X をクリックして削除し、無設定にします。
今回は単にテストなので、AssignmentsのControlled accessにAllow everyone in your organization to accessを選択します。
以上でSaveします。
Client IDとSecretをコピーします。APEX側でWeb資格証明を作成するときに使います。
Oktaに場合、defaultのAuthorization Serverではアクセス・トークンのkidとJWK URLのkidが一致しませんでした。そのため、SecurityのAPIよりAuthorization Serverを追加しています。
Access Policiesのタブを開き、Add New Access Policyをクリックして、Assigned toをAll Clientsとするポリシーを作成します。以下ではName、DescriptionともにAll Clientとしたポリシーを作成しています。
ほとんどすべてを許可するルールを追加しています。
以上でOkta側の準備は完了です。
APEX側でWeb資格証明を作成します。名前はOkta JWT Testとします。
クライアントIDまたはユーザー名およびクライアント・シークレットまたはパスワードには、Oktaに作成したアプリケーションのClient IDとSecretを設定します。
認証スキームOracle IDCSをコピーして、OktaをIdPとする認証スキームOktaを作成します。
資格証明ストアはOkta JWT Test、検出URLはAuthorization ServerのMetadata URIに変更します。以下のような形式になります。
https://[Oktaの認証サーバー]/[認証サーバーのID]/.well-known/oauth-authorization-server
変更を適用した後、カレント・スキームに変更します。
以上でOktaをIdPとした認証スキームへの切り替えが完了しました。
ブラウザを再起動し、APEXアプリケーションを実行します。
Oktaへのサインインが求められます。
APEXアプリケーションの画面が開きます。サインインには成功しているため、access_tokenは表示されますが、REST APIはUnauthorizedになります。
今までの作業と同様に、access_tokenをhttps://jwt.ioのデバッガにかけてissとaudを取得します。
これらの値は、Authorization ServerのAudienceとIssuer URIの値でした。
Authorization ServerのMetadata URIをアクセスし、jwks_uriの値を取得します。以下のような形式になります。
https://[認証サーバーのID].okta.com/oauth2/[Authorization ServerのID]/v1/keys
OktaでもREST APIは認証されません。
Oktaのアクセス・トークンのスコープも属性scpで渡されている上、スコープが空白区切りではなく、JSON配列になっています。
Oktaの開発者フォーラムにscope claimを追加する方法が紹介されていました。
Scope/scp and space delimited string in access tokenAuthorization ServerのClaimsタブを開き、Add Claimを実行します。
追加するClaimのNameがscopeになります。Include into token typeはAccess Token、Value typeはExpressionを選択し、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のアプリケーション作成の参考になれば幸いです。
完