ドキュメントの以下に説明が記載されています。
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のエラーが返されます。
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の値にすることはできなさそうです。
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を実行します。
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のアプリケーション作成の参考になれば幸いです。
完