2025年12月15日月曜日

Oracle IAMの統合アプリにカスタム・クレームを追加しRole based JWT profileによる保護を確認する

Oracle REST Data Servicesが提供しているRole based JWT profileによるRESTモジュールの保護をOracle IAMで実施するには、ユーザーが所属しているグループの一覧を返すカスタム・クレームを作成する必要があります。

ORDSのPMのChris Hoinaさんによる以下のブログ記事では、カスタム・クレームとしてiam_groupsを作成しています。

How to Secure Oracle Database REST APIs with OCI IAM (IDCS) JSON Web Tokens and Role-Based Access Claims, Part One
https://blogs.oracle.com/database/ords-apis-iam-jwts-role-based-claims-part-one

上記の記事では、curlコマンドよりOracle IAMのREST APIを呼び出してカスタム・クレームを追加しています。調べた範囲では、Oracle IAMのカスタム・クレームを追加するユーザー・インターフェスは見つけられませんでした。

そのため、Chris Hoinaさんの記事では以下のチュートリアルを参考にして、REST APIを呼び出してカスタム・クレームを作成するように案内されています。

Using OAuth 2 to Access the REST API
https://docs.oracle.com/en-us/iaas/Content/Identity/api-getstarted/OATOAuthClientWebApp.htm

curlコマンドでは手作業による間違いも起こりやすいので、Oracle IAMの統合アプリケーションにカスタム・クレームを追加するAPEXアプリケーションを作成しました。

以下にアプリケーションのエクスポートを置いています。
https://github.com/ujnak/apexapps/blob/master/exports/oracle-iam-custom-claim.zip

このアプリケーションの使い方について説明します。

カスタム・クレームを追加するREST APIを呼び出すにあたって、このAPI呼び出しをOAuth2で認証する必要があります。そのために記事「Oracle IAMのOIDC認証にてAPEXアプリとそれから呼び出すORDSのREST APIを認証する」で作成した統合アプリケーションORDS JWT Testを流用します。

本アプリケーションではREST API呼び出しに使用するアクセス・トークンの取得に、Client Credentials Grantを使用します。そのため、統合アプリケーションORDS JWT TestOAuth構成を編集します。


クライアント構成認可に含まれるクライアント資格証明チェックします。


アプリケーション・ロールとしてIdentity Domain Administratorを割り当てます。

以上の変更を実施し、送信します。


アプリケーションoracle-iam-custom-claim.zipを、APEXワークスペースにインポートします。インポートしたAPEXアプリケーションを実行すると、以下のようなホーム・ページが表示されます。


ホーム・ページ上には、アクセス・トークンの取得カスタム・クレームの追加、作成済みのカスタム・クレームの一覧取得、指定したカスタム・クレームの削除を行うボタンがあります。

アプリケーションを実行するにあたって、アプリケーション定義置換文字列を更新します。


置換文字列G_DOMAIN_URLに、Oracle IAMの該当ドメインのドメインURLを設定します。


置換文字列G_CREDENTIALには、統合アプリケーションのクライアントIDクライアント・シークレットが設定されているWeb資格証明静的IDを設定します。Web資格証明が作成されていない場合は、新規に作成します。その際、G_CREDENTIALに設定した値を、Web資格証明静的IDに設定します。

OAuth2のトークン・エンドポイントの呼び出しに使用するWeb資格証明なので、認証タイプ基本認証を選択します。


クライアントIDクライアント・シークレットには、統合アプリケーションORDS JWT TestOAuth構成一般情報にあるクライアントIDクライアント・シークレットの値を設定します。


以上でAPEXアプリケーションの設定は完了です。

APEXアプリケーションを使って、カスタム・クレームを追加します。

ボタンGet Access Tokenをクリックすると、トークン・エンドポイントが呼び出され、レスポンスがAccess Token Responseに設定されます。


以下のコードが実行されています。

ボタンUpdate Custom Claimをクリックすると、取得されたアクセス・トークンをAuthorizationヘッダーに含め、Custom Claim Requestを送信します。Custom Claim Requestのデフォルトとして以下を設定し、iam_groupsというクレームを作成します。
{
    "schemas": [
        "urn:ietf:params:scim:schemas:oracle:idcs:CustomClaim"
    ],
    "name": "iam_groups",
    "value": "$user.groups.*.display",     
    "expression": true,
    "mode": "always",
    "tokenType": "AT",
    "allScopes": true
}
Chris Hoinaさんの記事ではスコープを定義していますが、以下ではallScopestrueを設定し、スコープの指定がなくてもカスタム・クレームiam_groupsがアクセス・トークンに含まれるようにしています。


以下のコードが実行されています。

ボタンList Custom Claimをクリックすると、作成済みのカスタム・クレームを一覧します。Custom Claim ResponseにはJSONのレスポンスをそのまま表示しています。Custom Claim Listには、カスタム・クレームの名前と(削除の際に設定する)ロケーションを一覧しています。


以下のコードが実行されています。

ボタンDelete Custom Claimをクリックすると、Custom Claim Locationに設定したロケーションのカスタム・クレームが削除されます。


以下のコードが実行されています。

以上の手順にて、Oracle IAMの統合アプリケーションにカスタム・クレームiam_groupsが追加されます。カスタム・クレームiam_groupsの値は$user.groups.*.displayなので、サインインしたユーザーが所属しているグループがiam_groupsに含まれます。

サインインするユーザーにグループとしてAdministratorsを割り当てます。


アプリケーションORDS JWT TestをOracle IAMで認証すると、アクセス・トークンにカスタム・クレームiam_groupsが含まれていることが確認できます。
  "iam_groups": [
    "Administrators"
  ],

そのためRole based JWT profileを作成する、ORDS_SECURITY.CREATE_JWT_PROFILEの引数p_role_claim_nameの値は/iam_groupsになります。

ただし、認証スキーム有効範囲よりカスタム・スコープapi://ords/myordsappを除き、標準のスコープprofileとemailだけになると、audienceの値(aud属性)がapi://ords/からデフォルトのドメインURLに変わります

カスタム・スコープを除外した場合、ORDS_SECURITY.CREATE_JWT_PROFILEを以下のように呼び出して、Role based JWT profileを作成します。
begin
    ords_security.delete_jwt_profile;
    ords_security.create_jwt_profile(
        p_issuer => 'https://identity.oraclecloud.com/'
        ,p_audience => 'ドメインURL'
        ,p_jwk_url => 'ドメインURL/admin/v1/SigningCert/jwk'
        ,p_role_claim_name => '/iam_groups'
    );
end;
/

ORDS側で、Oracle IAMのグループ名Administratorsに一致するロールを作成し、それを保護する権限(名前はAdministratorsにしていますが、権限名はIAMのグループ名に一致させる必要はありません)を作成します。
declare
    l_roles    owa.vc_arr;
    l_modules  owa.vc_arr;
    l_patterns owa.vc_arr;
begin
    ords.create_role(
        p_role_name => 'Administrators'
    );
    l_modules(1) := 'print';
    l_roles(1)   := 'Administrators';
    ords.define_privilege(
        p_privilege_name => 'Administrators',
        p_roles          => l_roles,
        p_modules        => l_modules,
        p_patterns       => l_patterns    -- no assignment
    );
end;
/

以上で、Oracle IAMの統合アプリにカスタム・クレームを追加し、Role based JWT profileによる保護を確認できました。


今回の記事は以上になります。

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

2025年12月12日金曜日

Oracle REST Data ServicesのRole based JWT profileによる保護を確認する

Oracle REST Data ServicesのRESTモジュールを保護するJWTプロファイルには、以下の2つの構成があります。
  • スコープ・ベースJWTプロファイル
  • ロール・ベースJWTプロファイル
公式ドキュメントの以下で説明されています。

Oracle REST Data Services, Release 25.3
Developer's Guide

注意12 Developing Oracle REST Data Services Applicationsの先頭にTopicsのリストがありますが、2.9 JWT Profile and JWT Profile RBACはTopicsに含まれていません。Configuring Secure Access to RESTful ServicesJWT Bearer Token Authentication and Authorization Using JWT Profileの間にこのトピックがあります。

注意2:最新のORDSではパッケージOAUTHの代わりにORDS_SECURITYの使用が推奨されています。そのためJWTプロファイルの作成にはORDS_SECURITY.CREATE_JWT_PROFILEを呼び出すべきですが、このAPIのリファレンスにはRBACの実装に必要な引数p_role_claim_nameが含まれていません。OAUTH.CREATE_JWT_PROFILEのリファレンスには引数p_role_claim_nameが含まれています。これはドキュメントの問題で、ORDS_SECURITY.CREATE_JWT_PROFILEは、引数p_role_claim_nameをサポートしています。

公式ドキュメント以外に参考になるドキュメントとして、ORDSのプロダクト・マネージャであるChris Hoinaさんによるブログ記事があります。
Chris Hoinaさんの記事では、テストで使用するアプリケーションをNode.js / Express.js で作成していて、APEXアプリケーション開発者にはハードルが高いです。

本記事では、APEXアプリケーションを使って、ロール・ベースJWTプロファイルによる保護を実装します。以下の記事では、すべてスコープ・ベースJWTプロファイルによって、ORDSのRESTモジュールを保護しています。
それぞれのアイデンティティ・プロバイダにて、スコープ・ベースからロール・ベースJWTプロファイルによる保護に切り替えてみます。

最初に、ORDSのスコープ・ベースJWTプロファイルとロール・ベースJWTプロファイルによる保護の違いを説明します。

スコープ・ベースJWTプロファイルは、ORDS_SECURITY.CREATE_JWT_PROFILEを、引数p_role_claim_name無しで呼び出して作成します。このとき、RESTモジュールを保護するORDSの権限は、ORDS.DEFINE_PRIVILEGEを以下の引数で呼び出して作成します。権限名はmyordsapp、RESTモジュール名はprintとします。

スコープ・ベースJWTプロファイルによる保護では、権限名がアクセス・トークンのscopeまたはscpクレームに含まれていれば、権限に紐づいているRESTモジュールへのアクセスが認可されます。ロールを指定する必要はなく、指定があったとしても認可の可否に影響を与えません。
declare
    l_roles    owa.vc_arr;
    l_modules  owa.vc_arr;
    l_patterns owa.vc_arr;
begin
    l_modules(1) := 'print';
    ords.define_privilege(
        p_privilege_name => 'myordsapp',
        p_roles          => l_roles,      -- no assignment
        p_modules        => l_modules,
        p_patterns       => l_patterns    -- no assignment
    );
end;
/
ロール・ベースJWTプロファイルによる保護では、アクセス・トークンに含まれるロールに、ORDSの権限に紐づけられたロールが含まれていれば、権限に紐づいているRESTモジュールへのアクセスが認可されます。権限名がスコープに含まれている必要はありません。アクセス・トークンのロールを含むクレームは、ORDS_SECURITY.CREATE_JWT_PROFILEの引数p_role_claim_nameに、JSONパスで指定します。
begin
    ords_security.delete_jwt_profile;
    ords_security.create_jwt_profile(
        p_issuer => 'https://sts.windows.net/********-****-****-****-************/'
        ,p_audience => 'api://********-****-****-****-************'
        ,p_jwk_url => 'https://login.microsoftonline.com/********-****-****-****-************/discovery/v2.0/keys'
        ,p_role_claim_name => '/roles'
    );
end;
/
上記ではp_role_claim_nameに/rolesを指定して、JWTプロファイルを作成しています。アクセス・トークンのrolesクレームに、ORDS側でロールとして作成したords_user_roleが含まれていれば、RESTモジュールprintへのアクセスが認可されます。スコープに権限名であるords_user_privilegeが含まれている必要はありません。
declare
    l_roles    owa.vc_arr;
    l_modules  owa.vc_arr;
    l_patterns owa.vc_arr;
begin
    ords.create_role(
        p_role_name => 'ords_user_role'
    );
    l_modules(1) := 'print';
    l_roles(1)   := 'ords_user_role';
    ords.define_privilege(
        p_privilege_name => 'ords_user_privilege',
        p_roles          => l_roles,
        p_modules        => l_modules,
        p_patterns       => l_patterns    -- no assignment
    );
end;
/
ORDSのスコープ・ベースJWTプロファイルとロール・ベースJWTプロファイルによる保護には、以上のような違いがあります。

これらを踏まえて、それぞれのアイデンティティ・プロバイダーごとにロール・ベースJWTプロファイルの保護を実装してみます。すべてのケースで、スコープ・ベースJWTプロファイルによる保護が実装できている状態から作業を始めます。


Microsoft Entra ID



Entra IDのコンソールよりグループを開きます。Entra IDで作成するグループを、ORDSのロールに紐付けます。


新しいグループを作成します。


グループの種類セキュリティを選びます。グループ名ORDSUsersグループの説明は「ORDS REST APIへのアクセスを許可」と記述します。

以上でグループを作成します。


グループORDSUsersが作成されます。

すべてのグループからORDSUsersを開き、メンバーを追加します。


メンバーを開き、メンバーの追加を実行します。


グループに追加するメンバーを選択します。


ページを再ロードし、追加されたメンバーを確認します。


グループORDSUsersの概要を開きオブジェクトIDをコピーします。

ロール名にグループ名であるORDSUsersを使用したいのですが、Entra IDでグループ名をロールに含める方法を見つけられませんでした。そのため、ロール名にオブジェクトID(グループID)を使用します。


アプリケーションORDS JWT Testトークン構成を開きます。

グループ要求の追加を実行します(翻訳はグループ要求ですが、ここでの要求はclaimのことで、groups claimの追加という意味です。)


ドロワーとして、グループの要求の編集が開きます。

先ほど作成したグループORDSUsersタイプセキュリティなので、アクセス、ID、SAMLトークンに含めるグループの種類を選択してください、のセキュリティチェックします。

種類別にトークンのプロパティをカスタマイズする、ではIDアクセスともにグループIDを選択し、グループをロール要求として生成するをチェックします。

グループID以外を選択して動作を確認しましたが、何を設定してもアクセス・トークンにはグループ名ではなくグループIDがrolesクレームに含まれていたため、グループIDを選択しています。

グループをロール要求として生成するをチェックすると、アクセス・トークンにはrolesクレームにグループが含まれるようになります。未チェックの場合は、groupsクレームにグループが含まれます。ORDS_SECURITY.CREATE_JWT_PROFILEのp_role_claim_name/rolesと指定するか/groupsと指定するかの違いになりますが、今回はチェックを入れて、rolesクレームとなるようにしています。

詳しく調べていませんが、IDトークンとアクセス・トークンで異なる設定をすると、どちらかの設定が優先されるような動作になりました。IDトークンとアクセス・トークンで設定を変える必然性はないと思うので、同じ設定にするのが望ましいでしょう。

以上で追加をクリックします。


オプションの要求としてgroupsが追加されます。


以上でEntra ID側の設定は完了です。

ORDSのRESTモジュールを、ロール・ベースJWTプロファイルによる保護に変更します。

スコープ・ベースで保護されている状態で、APEXアプリケーションにサインインします。アクセス・トークンにrolesクレームとして、グループIDの配列が含まれていることが確認できます。
  "roles": [
    "aeb7eee2-b1ee-4127-91b5-58aaa9df63bd",
    "aa5a12e0-9c13-4a01-9a3f-95f978f2ce06"
  ],

ロール・ベースのJWTプロファイルを設定します。引数p_role_claim_name/rolesの指定を追加して、ORDS_SECURITY.CREATE_JWT_PROFILEを呼び出します。
begin
    ords_security.delete_jwt_profile;
    ords_security.create_jwt_profile(
        p_issuer => 'https://sts.windows.net/********-****-****-****-************/'
        ,p_audience => 'api://********-****-****-****-************'
        ,p_jwk_url => 'https://login.microsoftonline.com/********-****-****-****-************/discovery/v2.0/keys'
        ,p_role_claim_name => '/roles'
    );
end;
/

スコープ・ベースからロール・ベースの保護に切り替わりました。そのため、APEXアプリケーションをリロードすると、REST API Responseの表示はUnauthorizedに変わります。


ロール名がグループORDSUsersのグループIDであるロールを作成し、そのロールとRESTモジュールprintを紐づけた権限として、ORDSUsesを作成します。権限名のORDSUsersをアクセス・トークンのスコープに含む必要はありません。
declare
    l_roles    owa.vc_arr;
    l_modules  owa.vc_arr;
    l_patterns owa.vc_arr;
begin
    ords.create_role(
        p_role_name => 'aa5a12e0-9c13-4a01-9a3f-95f978f2ce06'
    );
    l_modules(1) := 'print';
    l_roles(1)   := 'aa5a12e0-9c13-4a01-9a3f-95f978f2ce06';
    ords.define_privilege(
        p_privilege_name => 'ORDSUsers',
        p_roles          => l_roles,
        p_modules        => l_modules,
        p_patterns       => l_patterns    -- no assignment
    );
end;
/

上記の権限ORDSUsersが作成されるとREST APIの呼び出しは認可され、REST API Responseに呼び出し結果が表示されます。


以上でロール・ベースJWTプロファイルによる保護に切り替わったので、スコープ・ベースの設定は削除できるはずです。実際、ORDSの権限myordsappは削除できます。

ただし、認証スキーム有効範囲に設定したアプリケーションのURIを含むカスタム・スコープapi://********-****-****-****-****-***********/myordsappを削除すると、アクセス・トークンのiss属性やaud属性が変わる上に、rolesクレームがアクセス・トークンに含まれなくなります。結果として、RESTモジュールprintへのアクセスが認可されません。


認証スキーム有効範囲からカスタム・スコープを削除したいのですが、Microsoft Entra IDでどのようにすれば良いのかは、今のところ見つけられていません。


Okta



OktaのAdmin Consoleよりグループを開き、グループを追加します。


グループの名前ORDSUsers説明は「ORDS REST APIへのアクセスを許可」と記述します。

保存をクリックします。


グループに再度アクセスし、グループ一覧を更新します。

作成したグループORDSUsersを開き、ユーザーを割り当てます。


ユーザーを割り当てをクリックします。


グループORDSUsersにユーザーを割り当てます。


ユーザーの割り当てを完了します。


セキュリティAPIより、作成済みの認可サーバーORDSを開きます。


クレームタブを開き、クレームを追加します。


追加するクレームの名前rolesとします。トークンタイプに含めるアクセス・トークンです。

値タイプグループを選択し、フィルター次で始まるORDSを設定します。このフィルターにより、rolesクレームに先ほど作成したグループORDSUsersが含まれます。

クレームを無効化チェックせず含めるいずれかのスコープを選択します。

以上で作成します。


以上でOktaの設定は完了です。

ロール・ベースJWTプロファイルを設定します。
begin
    ords_security.delete_jwt_profile;
    ords_security.create_jwt_profile(
        p_issuer => 'https://integrator-********.okta.com/oauth2/******************'
        ,p_audience => 'api://ords'
        ,p_jwk_url => 'https://integrator-********.okta.com/oauth2/******************/v1/keys'
        ,p_role_claim_name => '/roles'
    );
end;
/

スコープ・ベースからロール・ベースの保護に切り替わりました。そのため、APEXアプリケーションをリロードすると、REST API Responseの表示はUnauthorizedに変わります。

User Infoに表示されているアクセス・トークンの内容に、rolesクレームとして、ORDSUsersが含まれていることが確認できます。


ロールとしてORDSUsersを作成し、そのロールとRESTモジュールprintを紐づけた権限として、ORDSUsesを作成します。権限名のORDSUsersをアクセス・トークンのスコープに含む必要はありません。
declare
    l_roles    owa.vc_arr;
    l_modules  owa.vc_arr;
    l_patterns owa.vc_arr;
begin
    ords.create_role(
        p_role_name => 'ORDSUsers'
    );
    l_modules(1) := 'print';
    l_roles(1)   := 'ORDSUsers';
    ords.define_privilege(
        p_privilege_name => 'ORDSUsers',
        p_roles          => l_roles,
        p_modules        => l_modules,
        p_patterns       => l_patterns    -- no assignment
    );
end;
/

上記の権限ORDSUsersが作成されるとREST APIの呼び出しは認可され、REST API Responseに呼び出し結果が表示されます。


Oktaの場合は、認証スキーム有効範囲からスコープmyordsappを削除できます。


また、ORDSの権限myordsappも不要なので削除できます。


Oktaの認可サーバーORDSに設定したスコープmyordsappも同様に削除できます。



Auth0


Auth0は趣が異なっていて、スコープ・ベースでもロール・ベースでも、Auth0側の設定は同じです。

Auth0で作成するのはパーミッションであり、パーミッションとして設定した名前が、スコープとpermissionsクレームに含まれます。Auth0のアクセス・トークンは以下のようになっています。
{
  "iss": "https://dev-************.us.auth0.com/",
  "sub": "google-oauth2|******************",
  "aud": [
    "api://ords/",
    "https://dev-************.us.auth0.com/userinfo"
  ],
  "iat": 1765528368,
  "exp": 1765614768,
  "scope": "openid profile myordsapp",
  "azp": "J3FejeK46E2EH8AhsDq53HylfgB30wyJ",
  "permissions": [
    "myordsapp"
  ]
}
そのため、ORDS側ではAuth0に作成したパーミッションmyordsappをscopeとして評価することもできるし、permissionsクレームに含まれた名前として評価することもできます。

以上よりAuth0の設定は変更せず、引数p_role_claim_name/permissionsを指定して、ロール・ベースJWTプロファイルを作成します。
begin
    ords_security.delete_jwt_profile;
    ords_security.create_jwt_profile(
        p_issuer => 'https://dev-************.us.auth0.com/'
        ,p_audience => 'api://ords/'
        ,p_jwk_url => 'https://dev-************.us.auth0.com/.well-known/jwks.json'
        ,p_role_claim_name => '/permissions'
    );
end;
/

permissionsクレームに含まれる名前myordsappを、ORDS側でロールとして作成します。
declare
    l_roles    owa.vc_arr;
    l_modules  owa.vc_arr;
    l_patterns owa.vc_arr;
begin
    ords.create_role(
        p_role_name => 'myordsapp'
    );
    l_modules(1) := 'print';
    l_roles(1)   := 'myordsapp';
    ords.define_privilege(
        p_privilege_name => 'ORDSUsers',
        p_roles          => l_roles,
        p_modules        => l_modules,
        p_patterns       => l_patterns    -- no assignment
    );
end;
/

 以上でORDSのRESTモジュールの保護がスコープ・ベースからロール・ベースに変更されました。

スコープは使用しないので、APEXアプリケーションの認証スキーム有効範囲からmyordsappを削除できます。


ORDSに権限として作成されているmyordsappも削除できます。


今回作成した権限ORDSUsersによって、RESTモジュールprintが保護されます。ロールとしてmyordsappを作成しています。


以上で、権限ORDSUsersによりREST APIの呼び出しは認可され、REST API Responseに呼び出し結果が表示されます。


Auth0にはロールの設定がありますが、これはパーミッションをまとめてユーザーに割り当てられるようにするための設定で、アクセス・トークンのpermissionsクレームには直接かロール経由かに関わらず、パーミッションの名前だけが含まれるようです。そのため、ORDSの保護という面では、意識する必要はなさそうです。

Oracle IAMについては、以下の記事で確認しています。

Oracle IAMの統合アプリにカスタム・クレームを追加しRole based JWT profileによる保護を確認する
https://apexugj.blogspot.com/2025/12/protecting-ords-by-role-based-jwt-profile-with-oracle-iam.html

今回の記事は以上になります。

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