2024年6月25日火曜日

Microsoft Entra IDのOIDC認証にてAPEXアプリとそれから呼び出すORDSのREST APIを認証する

Oracle REST Data Services 23.3から、サードパーティのIdPが生成したOAuth2のアクセス・トークンによる保護が実装されました。以前の記事「ORDS 23.3で追加されたOAUTH.CREATE_JWT_PROFILEを使ってRESTサービスを保護する」にて、Oracle IDCS、Microsoft Entra ID、OktaをIdPとして動作確認を行いましたが、その時点ではMicrosoft Entra IDではORDSの保護はできませんでした。

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を更新します。

最初にESモジュールのjwt-decodecore-jsからMLEモジュールを作成し、それを使ってJWTのデコードを行うファンクションPARSE_JWTを作成します。

以下をSQLスクリプトとして実行します。

/*
* 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_DECODEMOD_CORE_JSMOD_PARSE_JWTが作成されます。MLE環境としてJWTENVPL/SQLファンクションとしてPARSE_JWTが作成されます。


作成したファンクションPARSE_JWTを呼び出し、アクセス・トークンIDトークンのデコードを行うように、ファンクションDUMP_SIGNIN_RESPONSEを更新します。

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として与える値になります。コピーして保存しておきます。

証明書またはシークレットの追加をクリックし、シークレットを作成します。


新しいクライアント・シークレットをクリックします。


説明を入力し、追加をクリックします。


作成されたシークレットの値は、APEXのWeb資格証明の作成時にクライアント・シークレットとして与える値になります。コピーして保存しておきます。


概要に戻り、エンドポイントを開きます。


OpenID Connectメタデータドキュメントの値をコピーして保存します。この値が、APEXアプリケーションの認証スキームを作成する際の検出URLになります。


APIの公開を開き、Scopeの追加をクリックします。


アプリケーションIDのURIの設定を求められます。この値はJWTのaud属性の値、つまりaudienceの値になります。

保存してから続けるをクリックします。


スコープ名ORDSに設定した権限と同じ名前であるmyordsappとします。同意できるのはだれですか?管理者とユーザー管理者の同意の表示名ORDS REST APIの呼び出し、管理者の同意の説明ORDS REST APIの呼び出しを許可します。とします。状態有効とします。

以上で、スコープの追加をクリックします。


スコープが追加されます。追加されたスコープ(api:で始まる値)をコピーして保存します。この値は、APEXアプリケーションの認証スキーム有効範囲に設定します。


Microsoft Entra IDでの作業は以上です。

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

名前Microsoft Entra ID JWT Test静的IDMS_ENTRA_JWT_TESTとします。

認証タイプとしてOAuth2クライアント資格証明フローを選択し、クライアントIDまたはユーザー名およびクライアント・シークレットまたはパスワードとして、Microsoft Entra IDのアプリのアプリケーション(クライアント)IDシークレットを設定します。


APEXアプリケーションORDS JWT Test共有コンポーネント認証スキームを開きます。


認証スキームとしてMicrosoft Entra IDがすでに作成済みの場合は、それを開いて編集します。以下より新規作成の手順を示します。

認証スキームの一覧より、作成をクリックします。


スキームの作成ギャラリからの事前構成済みスキームに基づくを選択します。

へ進みます。


認証スキーム名前Microsoft Entra IDとし、スキーム・タイプソーシャル・サインインを選択します。

設定資格証明ストアMicrosoft Entra ID JWT Test認証プロバイダOpenID Connectプロバイダ検出URLはMicrosoft Entra IDのエンドポイントで確認したOpenID Connectメタデータドキュメントの値を設定します。有効範囲profile,email,につづけてapi:で始まるスコープを設定します。

ユーザー名として#email#を設定します。ユーザー名の大文字の変換いいえにしていますが、標準のOracle APEXアカウントに動作を合わせたい場合は、はいを選択します。

認証スキームの作成をクリックします。


認証スキームとしてMicrosoft Entra IDが作成されます。認証後のプロシージャを追加するために、編集画面を開きます。


ソース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によるサインインが求められます。


構成に依存しますが、MFAのための通知の送信を行います。


Microsoft Authenticatorでの承認を求められます。


サインインの状態を維持しますか?と確認されます。テストでのサインインなので、はいいいえのどちらを選択しても大きな違いはありません。


この後、APEXアプリケーションの画面が開きます。

jwt-decodeを組み込んでアクセス・トークンをデコードしているため、access_token内のaud属性とiss属性の値が画面に表示されます。

aud属性は、ORDS REST APIの呼び出し許可を与えるプロシージャOAUTH.CREATE_JWT_PROFILEの引数p_audienceに与える値になります。iss属性は引数p_issuerに与える値になります。


Microsoft Entra IDのOpenID ConnectメタデータカタログのURLをブラウザで開き、jwks_uriの値を確認します。


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を認証できることが確認できました。