Microsoft Entra IDが発行するアクセス・トークンでORDSを保護するには、APIの公開からスコープを設定する必要があります。本記事ではこの手順を確認します。しかし、APIの公開から定義したスコープも、アクセス・トークンのscope属性ではなくscp属性にスコープが設定されているため、ORDSのバージョンアップの伴いscope属性だけではなくscp属性も見るように変更されたのではないか、と考えています。
今回も、以前と同じAPEXアプリケーションORDS JWT Testを使います。
https://github.com/ujnak/apexapps/blob/master/exports/ords-jwt-test.zip
APEXアプリケーションを実行する環境として、Always FreeのAutonomous Database - Oracle Database 23aiとOracle APEX 24.1を使います。せっかく23aiを使うので、APEXアプリケーションで受信したアクセス・トークンのデコードをAPEXアプリケーションで実施するよう、ファンクションdump_signon_responseを更新します。
https://github.com/ujnak/apexapps/blob/master/exports/ords-jwt-test.zip
APEXアプリケーションを実行する環境として、Always FreeのAutonomous Database - Oracle Database 23aiとOracle APEX 24.1を使います。せっかく23aiを使うので、APEXアプリケーションで受信したアクセス・トークンのデコードをAPEXアプリケーションで実施するよう、ファンクションdump_signon_responseを更新します。
最初にESモジュールのjwt-decodeとcore-jsからMLEモジュールを作成し、それを使ってJWTのデコードを行うファンクションPARSE_JWTを作成します。
以下をSQLスクリプトとして実行します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* jwt-decodeからMLEモジュールMOD_JWT_DECODEを作成する。 | |
*/ | |
create or replace mle module mod_jwt_decode | |
language javascript using clob ( | |
select apex_web_service.make_rest_request( | |
p_url => 'https://cdn.jsdelivr.net/npm/jwt-decode@latest/+esm' | |
,p_http_method => 'GET' | |
) | |
) | |
/ | |
/* | |
* core-jsからMLEモジュールMOD_CORE_JSを作成する。 | |
*/ | |
create or replace mle module mod_core_js | |
language javascript using clob ( | |
select apex_web_service.make_rest_request( | |
p_url => 'https://cdn.jsdelivr.net/npm/core-js@latest/+esm' | |
,p_http_method => 'GET' | |
) | |
) | |
/ | |
/* | |
* ファンクションparseJWTをエクスポートするMLEモジュールMOD_PARSE_JWTを作成する。 | |
*/ | |
create or replace mle module mod_parse_jwt | |
language javascript using clob ( | |
select q'~ | |
import * as decode from "mod_core_js"; | |
import * as e from "mod_jwt_decode"; | |
export function parseJWT(jwt) { | |
const decoded = e.jwtDecode(jwt); | |
return JSON.stringify(decoded, null, 2); | |
} | |
~' | |
) | |
/ | |
/* | |
* 作成したモジュールをMLE環境JWTENVにまとめてインポート可能にする。 | |
*/ | |
create or replace mle env jwtenv | |
imports ( | |
'mod_core_js' module MOD_CORE_JS, | |
'mod_jwt_decode' module MOD_JWT_DECODE, | |
'mod_parse_jwt' module MOD_PARSE_JWT | |
) | |
/ | |
/* | |
* parseJWTをPL/SQLファンクションPARSE_JWTとして作成する。 | |
*/ | |
create or replace function parse_jwt( | |
p_jwt in varchar2 | |
) | |
return varchar2 | |
as mle module MOD_PARSE_JWT env JWTENV | |
signature 'parseJWT(string)'; | |
/ | |
/* | |
初期化コード | |
drop function parse_jwt; | |
drop mle env jwtenv; | |
drop mle module mod_parse_jwt; | |
drop mle module mod_jwt_decode; | |
drop mle module mod_core_js; | |
*/ |
MLEモジュールとしてMOD_JWT_DECODE、MOD_CORE_JS、MOD_PARSE_JWTが作成されます。MLE環境としてJWTENV、PL/SQLファンクションとしてPARSE_JWTが作成されます。
作成したファンクションPARSE_JWTを呼び出し、アクセス・トークンとIDトークンのデコードを行うように、ファンクションDUMP_SIGNIN_RESPONSEを更新します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
create or replace function dump_signon_response | |
return clob | |
as | |
k varchar2(32767); | |
v apex_json.t_value; | |
t apex_json.t_kind; | |
res clob := ''; | |
begin | |
k := apex_json.g_values.first; | |
while k is not null loop | |
v := apex_json.get_value(k); | |
res := res || k || ' = '; | |
/* decoce access_token and id_token */ | |
if k = 'access_token' then | |
res := res || parse_jwt( v.varchar2_value); | |
res := res || apex_application.LF; | |
elsif k = 'id_token' then | |
res := res || parse_jwt( v.varchar2_value); | |
res := res || apex_application.LF; | |
end if; | |
/* end */ | |
case v.kind | |
when apex_json.c_null then | |
res := res || '(NULL)'; | |
when apex_json.c_true then | |
res := res || '(TRUE)'; | |
when apex_json.c_false then | |
res := res || '(FALSE)'; | |
when apex_json.c_number then | |
res := res || to_char(v.number_value); | |
when apex_json.c_varchar2 then | |
res := res || v.varchar2_value; | |
when apex_json.c_object then | |
res := res || '(OBJECT[' || apex_string.join(v.object_members, ':') || '])'; | |
when apex_json.c_array then | |
res := res || '(ARRAY[' || to_number(v.number_value) || '])'; | |
when apex_json.c_clob then | |
res := res || v.clob_value; | |
else | |
res := res || '(OTHER)'; | |
end case; | |
res := res || apex_application.LF; | |
k := apex_json.g_values.next(k); | |
end loop; | |
return res; | |
end dump_signon_response; | |
/ |
APEXアプリケーションは特に変更はしません。
テストで呼び出すORDSのREST APIの権限の名前はmyordsappに変更します。Microsoft Entra IDよりAPIの公開を行い、スコープとして割り当てる名前もmyordsappとします。
以上でテストに使用するAPEXアプリケーションの準備はできました。
Microsoft Entra IDにアプリを登録します。
AzureのコンソールからMicrosoft Entra IDの画面を開きます。
サイド・メニューよりアプリの登録を開きます。
新規登録をクリックします。
Microsoft Entra IDに登録するアプリケーションの名前はORDS JWT Testとします。サポートされているアカウントの種類はこの組織ディレクトリのみに含まれるアカウント(既存のディレクトリのみ - シングル・テナント)を選んでいますが、これは契約に依存するかと思います。今回の作業は、Microsoft Entra IDの無料枠の範囲で実施しています。
アプリケーションのタイプとしてWebを選択し、リダイレクトURIは以下を指定します。
https://[ホスト名]/ords/apex_authentication.callback
以上を設定し、登録をクリックします。
アプリケーションが登録されます。
画面に表示されているアプリケーション(クライアント)IDは、APEXのWeb資格証明の作成時にクライアントIDとして与える値になります。コピーして保存しておきます。
証明書またはシークレットの追加をクリックし、シークレットを作成します。
概要に戻り、エンドポイントを開きます。
APIの公開を開き、Scopeの追加をクリックします。
アプリケーションIDのURIの設定を求められます。この値はJWTのaud属性の値、つまりaudienceの値になります。
保存してから続けるをクリックします。
以上で、スコープの追加をクリックします。
スコープが追加されます。追加されたスコープ(api:で始まる値)をコピーして保存します。この値は、APEXアプリケーションの認証スキームの有効範囲に設定します。
Microsoft Entra IDでの作業は以上です。
Microsoft Entra IDが発行するトークンを保存するWeb資格証明を作成します。
名前はMicrosoft Entra ID JWT Test、静的IDはMS_ENTRA_JWT_TESTとします。
認証タイプとしてOAuth2クライアント資格証明フローを選択し、クライアントIDまたはユーザー名およびクライアント・シークレットまたはパスワードとして、Microsoft Entra IDのアプリのアプリケーション(クライアント)IDとシークレットを設定します。
APEXアプリケーションORDS JWT Testの共有コンポーネントの認証スキームを開きます。
認証スキームの一覧より、作成をクリックします。
次へ進みます。
認証スキームの名前はMicrosoft Entra IDとし、スキーム・タイプにソーシャル・サインインを選択します。
設定の資格証明ストアはMicrosoft Entra ID JWT Test、認証プロバイダはOpenID Connectプロバイダ、検出URLはMicrosoft Entra IDのエンドポイントで確認したOpenID Connectメタデータドキュメントの値を設定します。有効範囲はprofile,email,につづけてapi:で始まるスコープを設定します。
ユーザー名として#email#を設定します。ユーザー名の大文字の変換はいいえにしていますが、標準のOracle APEXアカウントに動作を合わせたい場合は、はいを選択します。
認証スキームの作成をクリックします。
ソースのPL/SQLコードに以下を記述します。ユーザー認証が成功した時に、IdPより受信した応答をアプリケーション・アイテムG_USER_INFOに保存しています。
procedure post_auth is
begin
:G_USER_INFO := dump_signon_response;
end post_auth;
ログイン・プロセスの認証後のプロシージャ名としてpost_authを設定します。
変更の適用をクリックします。
以上で認証スキームの設定は完了です。
認証スキームの一覧画面に戻るので、再度認証スキームMicrosoft Entra IDを開き、カレント・スキームに切り替えます。確認のためのポップアップが開くので、OKをクリックします。
以上でAPEXアプリケーションORDS JWT Testは、Microsoft Entra IDでユーザー認証を行うようになりました。
APEXアプリケーションを実行すると、Microsoft Entra IDによるサインインが求められます。
Microsoft Authenticatorでの承認を求められます。
jwt-decodeを組み込んでアクセス・トークンをデコードしているため、access_token内のaud属性とiss属性の値が画面に表示されます。
aud属性は、ORDS REST APIの呼び出し許可を与えるプロシージャOAUTH.CREATE_JWT_PROFILEの引数p_audienceに与える値になります。iss属性は引数p_issuerに与える値になります。
ORDS REST APIを実行しているインスタンス移り、作業を行います。
すでにプロファイルが設定されている場合は、OAUTH.DELETE_JWT_PROFILEを呼び出します。
begin
oauth.delete_jwt_profile;
end;
3rdパーティのJWTでORDS REST APIの呼び出しを認証するために、OAUTH.CREATE_JWT_PROFILEを実行します。
begin
oauth.create_jwt_profile(
p_issuer => 'iss属性の値'
,p_audience => 'aud属性の値'
,p_jwk_url => 'jwks_uriの値'
);
end;
APEXアプリケーションを再ロードすると、401 Unauthorizedが返されていたREST API Responseのリージョンに、ORDS REST APIの応答が表示されます。
Microsoft Entra IDが発行したアクセス・トークンを使って、ORDSのREST APIを認証できることが確認できました。
完