本記事は、Paul Parkinsonによる”Passwordless Java Connections to Oracle AI Database with OCI IAM and Microsoft Entra ID“を翻訳したものです。(原文での読了時間:34分)

2026年11月8日

OCIとMicrosoft Entra IDのトークンを使って、Javaからデータベースへ接続する手順を、実行可能なサンプルとともに説明します。トークンファイルを使う方法、SDKで取得したトークンを渡す方法、JDBCプロバイダーに取得を任せる方法、接続プールの利用を扱います。データベースのセットアップと、Microsoft Entra側のID設定も含めたガイドです。

新しい物理接続を作成するたびに、クラウドのID、データベースのスキーマへの対応付け、TLS、トークンの有効期間と更新を組み合わせて認証します。

主なポイント

  • 同じ読み取り専用のJavaアプリケーションを、OCIまたはEntraのトークンファイル、アプリケーションがSDKで取得したトークンを供給する処理、JDBCプロバイダーのいずれを使う場合でも実行できます。
  • トークンでログインするには、データベース側でIDプロバイダーを信頼する設定、認証対象のID(プリンシパル)とスキーマの対応付け、クラウド側のアクセス許可、TCPS接続の設定を、すべて正しくそろえる必要があります。
  • リポジトリには、完全なソースコード、JDBC標準のプロパティファイルを含む設定プロファイル、SQLの設定用テンプレート、UCPで複数タスクを並行実行する例、自動テストを用意しています。
  • トークンの更新が機能するかは、新しい物理接続を作成して確認します。接続プール内の既存セッションを再利用できただけでは、トークンを更新できたとは判断できません。

JavaアプリケーションからOracle AI Databaseへ接続する際に、データベースのパスワードを渡す代わりに、OCI IAMまたはMicrosoft Entra IDのトークンを使って認証できます。OCI IAMの場合は、JDBCを通じて、トークンに対応する秘密鍵を持っていることを証明します。Entra IDの場合は、データベースリソース向けに発行されたアクセストークンを提示します。データベース側では、そのIDを検証し、アクセスが許可されたスキーマに対応付けます。

本記事では、以前紹介したIAMのCLI/SDKと接続プールの例に、JDBC拡張を使う方法を組み合わせ、実際に動かせるアプリケーションで一連の手順を説明します。具体的なサーバー設定は、Autonomous AI Database Serverlessを対象としています。ほかのOracle Database環境を使う場合は、その環境でサポートされるID連携を、別途設定する必要があります。Javaの例では、記事で使用する現行のJDBCリリースを特定のバージョンに固定しています。なお、データベースはOracle AI Database 26aiです。

まず、利用するIDプロバイダーを1つ選んで設定します。
次に、関連するJavaアプリケーションをビルドし、使用する設定プロファイルを選びます。
ここで紹介するJavaのトークン認証では、いずれの方法でもデータベースのユーザー名やパスワードを渡しません。
本記事の内容:

トークンの取得方法を選ぶ

OCIとEntraのどちらでも、外部で作成したトークンファイルを使う方法、アプリケーションからSDKで取得する方法、JDBCドライバーが選ぶプロバイダーを使う方法を選べます。ただし、認証に使う形式は異なります。OCIではトークンとともに所持証明(PoP)用の秘密鍵を使い、EntraではBearer形式のJWTを使います。Entraへの接続に、OCIの秘密鍵を渡さないでください。

方法トークンを取得する処理トークンを更新・置換する処理デモ用プロファイル
OCIのファイル方式OCI CLI、または外部の生成処理生成側で、トークンと対応するPoP鍵の組み合わせを保って置換するoci-file.properties
Entraのファイル方式承認済みのAzure CLI、SDK、または外部の生成処理生成側で、アクセスを制限したJWTファイルを置換するentra-file.properties
OCI SDK方式アプリケーションからOCIのIDデータプレーンを呼び出すトークン取得処理を包むJDBCのキャッシュで管理するoci-sdk.properties
Entra SDK方式アプリケーションからAzure Identityを呼び出すトークン取得処理を包むJDBCのキャッシュで管理するentra-sdk.properties
OCIプロバイダー方式JDBCドライバーが選択したOCI拡張を使うプロバイダーとキャッシュで管理するoci-config-file.propertiesと、IDの種類に応じたプロファイル
Entraプロバイダー方式JDBCドライバーが選択したAzure拡張を使うプロバイダーとキャッシュで管理するentra-service-principal.propertiesと、IDの種類に応じたプロファイル

プロバイダーを使う例では、oracle.jdbc.tokenAuthentication=OCI_*またはAZURE_*を設定し、対応する拡張を選択します。プロバイダーのJARは必要ですが、oracle.jdbc.provider.accessTokenを明示的に設定する必要はありません。ファイル方式ではOCI_TOKENまたはOAUTHを指定します。アプリケーションからSDKを使う方式では、トークンを取得して渡す処理(トークンサプライヤー)を設定します。1つのデータソースに、複数の取得方法を混在させないでください。詳細はJDBC接続プロパティのリファレンスを参照してください。

背景となる記事:JavaからIAMトークンでAutonomous Databaseにアクセスする(Jean de Lavarene、Michael McMahon、Nirmala Sundarappa)、23c JDBCでOCI IAMとAzure ADによるシームレスな認証を行う(Jean de Lavarene)。

前提条件と、動作確認したライブラリのバージョン

JDK 17または21、Maven 3.9以降、接続可能なAutonomous Databaseを用意してください。また、利用するIDとスキーマの対応付けを設定できるID管理者が必要です。セットアップ後にデモを動かす際は、CREATE SESSION権限だけを使ってDUALを問い合わせます。アプリケーション用のテーブルは作成しません。ファイル方式の例にはOCI CLIも必要です。ワークロードのIDを使う場合は、そのIDを利用できる実際のホスト環境も用意します。

この例では、JDBCとUCPは23.26.3.0.0、OCIおよびAzureのプロバイダーは1.1.0、OCI SDKは3.86.2、Azure Identityは1.18.0を使います。これらのバージョンを組み合わせて、ビルドとテストを行っています。JDBCの配布ライブラリに付いているバージョン番号と、データベースの製品名は別のものです。

JDBC、接続プール、ウォレット用のライブラリ

Mavenプロジェクトに、次の依存関係を追加します。接続プールの例を使う場合はucp17を、ここで扱うウォレットを使った接続にはoraclepkiを含めてください。

<dependencies>
  <!-- JDBC driver -->
  <dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc17</artifactId>
    <version>23.26.3.0.0</version>
  </dependency>
  <!-- For the UCP examples -->
  <dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ucp17</artifactId>
    <version>23.26.3.0.0</version>
  </dependency>
  <!-- For Oracle wallet support -->
  <dependency>
    <groupId>com.oracle.database.security</groupId>
    <artifactId>oraclepki</artifactId>
    <version>23.26.3.0.0</version>
  </dependency>
</dependencies>

使用するIDサービスのプロバイダーを追加する

OCIまたはMicrosoft Entra IDのうち、使用するものを選びます。すべての例を実行する場合は、両方を追加します。プロバイダーの依存ライブラリには各クラウドのSDKも含まれ、実行時に読み込まれます。

<dependencies>
  <!-- OCI IAM -->
  <dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc-provider-oci</artifactId>
    <version>1.1.0</version>
    <scope>runtime</scope>
  </dependency>
  <!-- Microsoft Entra ID -->
  <dependency>
    <groupId>com.oracle.database.jdbc</groupId>
    <artifactId>ojdbc-provider-azure</artifactId>
    <version>1.1.0</version>
    <scope>runtime</scope>
  </dependency>
</dependencies>

ojdbc8を明示的に除外する設定は不要です。ここで使うバージョンのプロバイダーでは、Mavenのprovidedスコープで宣言されているため、アプリケーションの依存ライブラリとして追加されません。アプリケーションでは、上で明示的に指定したojdbc17ドライバーを使います。

OCI SDKを直接呼び出す場合に追加するライブラリ

次の依存関係が必要なのは、アプリケーション側でSDKを使ってトークンを取得する例(OciTokens.java)だけです。1つ目を追加すると、Javaのコンパイル時にトークン要求APIを参照できます。2つ目は、実行時にHTTP通信を行うためのライブラリです。プロバイダーだけを使うアプリケーションでは、これらを省略できます。

<dependencies>
  <dependency>
    <groupId>com.oracle.oci.sdk</groupId>
    <artifactId>oci-java-sdk-identitydataplane</artifactId>
    <version>3.86.2</version>
  </dependency>
  <dependency>
    <groupId>com.oracle.oci.sdk</groupId>
    <artifactId>oci-java-sdk-common-httpclient-jersey</artifactId>
    <version>3.86.2</version>
    <scope>runtime</scope>
  </dependency>
</dependencies>

必要な依存関係を、既存の<dependencies>セクションにまとめて追加します。リポジトリ内の完全なpom.xmlには、SDKのバージョンをそろえる設定と、実行可能なデモをコンパイル、テスト、パッケージ化するためのビルド設定も含めています。

Entra SDKを直接呼び出す場合に追加するライブラリ

EntraTokens.javaではAzure Identityを直接使います。プロバイダーを介して実行時の依存関係に含まれるだけでは足りないため、コンパイル時にも参照できるよう、次のように明示的な依存関係として追加します。

<dependency>
  <groupId>com.azure</groupId>
  <artifactId>azure-identity</artifactId>
  <version>1.18.0</version>
</dependency>

データベースとIDの連携を設定する

ここでの設定例は、Autonomous AI Database Serverlessを対象としています。JDBCのバージョンを選ぶだけでは、データベース側のID連携を有効にできません。ほかのOracle Database環境では、その環境に対応したサーバー設定が必要です。検証用のデータベースを用意し、管理者がセットアップしてください。Javaのデモでは、CREATE SESSION権限だけを使ってDUALを読み取ります。

外部IDプロバイダーには、OCI IAMかEntraのどちらかを選んで設定します。両方を試す場合は、別々の検証用データベースを使ってください。付属のSQLスクリプトでは、既存設定を強制的に置き換えず、エラーが起きたら停止します。これらは初回セットアップ用であり、繰り返し適用する移行スクリプトではありません。TOKEN_DEMOがすでにある場合は、削除せず、どのIDに対応付けられているかを確認してください。

OCI IAMの設定

  1. IAMユーザーとグループを作成するか、既存のものを選び、そのユーザーをグループに追加します。グループ名の例はTokenDemoUsersです。デフォルト以外のIDドメインを使う場合は、テナンシでサポートされている、ドメイン名を付けた名前を使ってください。
  2. 対象データベースに適用範囲を限定したポリシーを作成します。次の例の仮の値は、すべて実際の値に置き換えてください。
Allow group TokenDemoUsers to use autonomous-database-family in compartment DemoCompartment
  where target.id = '<database-ocid>'
  1. SQLcl、SQL*Plus、またはDatabase ActionsからADMINとして接続し、OCI IAMを有効にしてユーザーを対応付けます。SQLclでは@sql/oci-setup.sqlを実行します。Database ActionsではSQL文を実行し、入力を求める変数を手動で置き換えてください。IAMの名前は正確に指定し、デフォルト以外のドメインではdomain/name形式を使います。スクリプトではTOKEN_DEMOを作成し、CREATE SESSIONを付与します。代わりに使える共有グループの対応付けも含まれていますが、このデモではどちらか1つを選びます。
  2. ローカル環境でAPIキー認証を使う場合は、IAMユーザーにAPI署名用の公開鍵を登録し、oci setup configで対応する秘密鍵を設定します。秘密鍵はリポジトリの外に保存してください。OCIの設定プロファイルには、user、tenancy、region、fingerprint、key_fileを指定します。このAPI署名鍵でOCI APIへの要求を認証します。後で使う、データベーストークンごとの所持証明用の鍵とは別のものです。
  3. コンパートメントとAutonomous DatabaseのOCIDを記録します。OCI_DB_SCOPEにurn:oracle:db::id::<compartment-ocid>::<database-ocid>を設定します。トークンの対象範囲、クラウド側のポリシー、データベースの対応付けは、それぞれ独立した要件として満たす必要があります。
  4. インスタンスプリンシパルを使う場合は、対象のComputeインスタンスに一致するルールを持つIAM動的グループを作成します。リソースプリンシパルを使う場合は、対象となるサポート対象リソースをそのグループに含めます。動的グループに、対象データベースに範囲を限定したポリシーを付与し、IAM_GROUP_NAME=<dynamic-group-name>で共有のデータベース対応付けを作成します。実行は、実際のワークロード環境で行ってください。ノートPCでプロファイルを選んだだけでは、その環境のIDを利用できるようにはなりません。インスタンスプリンシパルやリソースプリンシパルを、IAMユーザーとして排他的に対応付けようとしないでください。

セッショントークンのプロファイルを使う場合は、まずoci session authenticateで認証します。その後、CLIでは--auth security_token --profile <profile>を、SDKの例ではapp.oci.authentication=session-tokenを指定します。データベーストークンを更新しても、その取得元となるログインセッションの期限切れには対処できません。ログインセッション自体も更新してください。このリリースのリソースプロバイダーでconfig-file方式を選ぶ場合は、APIキーの設定を使います。設定ファイルにセッショントークンが含まれているだけで、それを読み込むとは考えないでください。これ以外に対応しているユーザーセッションの利用方法は、Cloud Shell用と対話認証用のプロファイルで示しています。

出典:IAMの有効化、ポリシーと動的グループ、ユーザーの対応付け、OCI SDKの認証。

データベース管理者として次のSQLを実行します。IAMの名前は、使用するものに置き換えてください。

BEGIN
  DBMS_CLOUD_ADMIN.ENABLE_EXTERNAL_AUTHENTICATION(type => 'OCI_IAM');
END;
/
CREATE USER token_demo IDENTIFIED GLOBALLY AS
  'IAM_PRINCIPAL_NAME=<domain>/<user>';
GRANT CREATE SESSION TO token_demo;
SELECT name, value FROM v$parameter WHERE name = 'identity_provider_type';

デフォルトのIDドメインを使う場合は、設定に応じて、ドメインの接頭辞を付けずにIAMユーザー名を指定します。リポジトリには、値の入力を求めるSQLclスクリプトと、代わりに使える共有グループの対応付けを用意しています。APIキーはOCI APIへの要求に署名するために使います。各データベーストークンに対応するPoP鍵は、それとは別の目的で使います。

Microsoft Entra IDの設定

  1. Entraテナントに、データベースリソース用のアプリケーションを登録します。そのアプリケーションIDとApplication ID URIを記録してください。対話型クライアント向けに、session:scope:connectなど、データベースにアクセスするための委任スコープを公開します。このURIで識別するのはデータベースリソースであり、Javaクライアントではありません。
  2. データベース用に登録したアプリケーションで、アプリロールの値をTokenDemo.Connectとして定義します。表示名は別でも構いません。想定する使い方に応じて、クライアント資格情報による認証にはApplicationsを、対話するユーザーにはUsers/Groupsを許可します。
  3. Javaクライアント用のアプリケーションを、データベース用とは別に登録します。サービスプリンシパル認証では、データベース用アプリケーションのApplication権限TokenDemo.Connectを追加し、管理者の同意を取得します。証明書を設定するか、クライアントシークレットを作成してください。AZURE_CLIENT_IDにはJavaクライアントのIDを指定し、ENTRA_DB_APP_ID_URIには、追加のスコープを付けないデータベースリソース自体のURIを指定します。SDK方式とファイル方式でトークンを取得する際には、完全なスコープであるENTRA_DB_SCOPEを使います。ワークロードの認証では、末尾を/.defaultとします。
  4. ブラウザーで認証する場合は、クライアントをパブリック/ネイティブクライアントとして設定し、リダイレクトURIにhttp://localhost:8400を登録します。データベースへの委任権限と必要な同意を設定し、ユーザーまたはグループを、データベース用アプリケーションのロールに割り当てます。デバイスコード認証では、対応するパブリッククライアントのフローを有効にしてください。どちらもテナントの条件付きアクセスポリシーに従います。v2の対話型トークンでは、Oracleのガイドに従って、オプションのクレームであるupnを設定します。
  5. マネージドIDを使う場合は、そのIDのサービスプリンシパルに、データベース用アプリケーションのロールを割り当てます。Azure VMのIDを有効にするだけでは、データベースへのアクセス権限は得られません。ユーザー割り当てマネージドIDでは、そのIDのクライアントIDを使います。システム割り当てマネージドIDでは、無関係なアプリケーションIDを選ばないよう、AZURE_CLIENT_IDを未設定にしてください。
  6. Entra用の検証データベースにADMINとして接続し、@sql/entra-setup.sqlを実行します。テナントの値と、データベース用アプリケーションの登録値を指定してください。スクリプトではAZURE_ADを有効にし、AZURE_ROLE=TokenDemo.ConnectをTOKEN_DEMOに対応付け、CREATE SESSIONを付与します。製品名がEntra IDに変わっていても、SQLで指定するプロバイダーの識別子はAZURE_ADのままです。
  7. データベースで、すでに対象のEntraリソースを信頼する設定を行っている場合は、既存のプロバイダー設定とDDSロールを維持します。デモ専用のロールと割り当てだけを追加してください。また、TOKEN_DEMOがまだ存在しないことを確認したうえで、CREATE USERとGRANTの文を追加します。すでに連携済みの環境で、初回有効化の処理を再実行しないでください。
  8. v1のトークンでは、登録したデータベースのApplication ID URIに合わせます。v2ではOracleのaudience設定のガイドに従ってください。トークンのaudienceには、アプリケーションIDを使います。Microsoft Graphのスコープで代用せず、管理者と、トークンのバージョンおよびリソースの設定を確認してください。

出典:登録、ロール、同意、トークン版、サーバー設定、スキーマ対応付け、マネージドIDへのアプリロール付与。

対応するデータベース側のSQLでは、Javaクライアントではなく、データベースリソース用アプリケーションの登録情報を指定します。

BEGIN
  DBMS_CLOUD_ADMIN.ENABLE_EXTERNAL_AUTHENTICATION(
    type => 'AZURE_AD',
    params => JSON_OBJECT(
      'tenant_id' VALUE '<tenant-id>',
      'application_id' VALUE '<database-application-id>',
      'application_id_uri' VALUE 'https://<tenant-domain>/<database-application-id>'),
    force => FALSE);
END;
/
CREATE USER token_demo IDENTIFIED GLOBALLY AS 'AZURE_ROLE=TokenDemo.Connect';
GRANT CREATE SESSION TO token_demo;

OCI IAMとEntraを有効にする2つの手順は、どちらかを選んで使うものです。設定を追加するつもりで、同じデータベースに両方を実行しないでください。付属のスクリプトは、互換性のない別のプロバイダーが有効になっている場合、処理を停止します。その場合は管理者が、別の検証用データベースを使うか、意図した移行として計画するかを判断します。

TLSとウォレットの設定

データベースのTCPS接続用のホスト、ポート、サービス名を指定し、EZConnect+形式で接続します。この例で固定したバージョンのJDBCドライバーでは、接続先サーバーのID照合をデフォルトで行うため、DN照合用のフラグをURLに重ねて指定する必要はありません。検証を無効にしたり、接続先を示す別名を使ったりしないでください。

jdbc:oracle:thin:@tcps://<adb-host>:1522/<service>?connect_timeout=20

mTLSを使う場合は、取得したリポジトリのディレクトリ外にウォレットを保存し、Gitの追跡対象から除外したローカルの設定プロファイルに、次のプロパティを追加します。ウォレットのディレクトリを指定する環境変数には、絶対パスを設定してください。

oracle.net.wallet_location=(SOURCE=(METHOD=FILE)(METHOD_DATA=(DIRECTORY=${ADB_WALLET_DIR})))

必要な場合は、実行環境のシークレット管理機能を通じて、oracle.net.wallet_passwordを渡します。ウォレットはTLS接続の認証情報です。OCIトークンを格納するディレクトリや、EntraのJWTファイルとは別のものです。TLSのみを使う接続先では、JVMがサーバーの認証局(CA)を信頼していれば、クライアントウォレットは不要です。利用環境で求められている通信上の要件は維持してください。

アプリケーションをビルドして実行する

git clone https://github.com/oracle-samples/oracle-db-examples.git
cd oracle-db-examples/java/jdbc-token-auth
mvn -B verify
./run.sh --help

# Replace with the host, TCPS port and service from Database Connection.
export ADB_JDBC_URL='jdbc:oracle:thin:@tcps://<adb-host>:1522/<service>'
export OCI_COMPARTMENT_ID='<compartment-ocid>'
export OCI_DATABASE_ID='<database-ocid>'
export OCI_CONFIG_FILE="$HOME/.oci/config"
export OCI_PROFILE=DEFAULT
export OCI_DB_SCOPE='urn:oracle:db::id::<compartment-ocid>::<database-ocid>'

使用するデータベースの、実際のTCPS接続用ホスト、ポート、サービス名を指定します。mTLSの場合は、セットアップの節で説明したように、プロファイルのローカルコピーをGitの追跡対象から除外し、ウォレットの設定を追加します。このデモでは、アプリケーション内で${VARIABLE}を実際の値に置き換えます。必要な変数が未設定の場合は、値をログに出さずにエラーとします。.envファイルを自動で読み込むことはありません。

./run.sh config/oci-config-file.properties check
./run.sh config/oci-config-file.properties jdbc
./run.sh config/oci-config-file.properties ucp

checkを指定すると、設定だけを検証し、クラウドやデータベースには接続しません。jdbcでは、実行ラウンドごとに新しい物理接続を開きます。ucpでは、接続数が最大2つのプールを使い、4つのタスクを並行実行します。各タスクの処理で発生した失敗を呼び出し元へ伝え、終了時にはプールを破棄します。Windowsでは、Mavenでビルドした後に、java -cp "target/oracle-jdbc-token-auth-demo-1.0.0.jar;target/lib/*" demo.TokenAuthDemo ...を実行できます。

実行が成功しても、何を確認できたのかを区別してください。
TokenAuthDemo.query()では、セッションユーザー、認証されたID、認証方式を表示しますが、それぞれが期待した値と一致するかまでは検証しません。終了コードが0であれば、接続とクエリの実行には成功しています。ただし、トークン認証を確認できたと判断する前に、userが想定したデータベーススキーマであり、authenticationがTOKEN_GLOBALであることを手動で確認してください。別途用意したOciWorkloadPrincipalDemoでは、この一致を検証し、期待した値だったときにだけPASSを表示します。

すべてのJavaサンプルの接続では、Oracle JDBC Thinのv$session.programにdevrel-developerhub-java-jdbc-token-authを設定しています。この値によって、データベース側から、このサンプルによる接続であることを識別できます。値は、指定されたdevrel-developerhub-<content-name>の命名規則に従っています。Oracle JDBCのドキュメントでは、このプロパティを、V$SESSION.PROGRAMに表示される値と説明しています(JDBC接続プロパティのリファレンス)。

方法1:OCI CLIで作成したトークンファイルを使う

umask 077
oci iam db-token get --profile "$OCI_PROFILE"
export OCI_DB_TOKEN_DIR="$HOME/.oci/db-token"
./run.sh config/oci-file.properties jdbc
./run.sh config/oci-file.properties ucp

# Alternatives: run only in a configured identity environment.
oci --auth instance_principal iam db-token get
oci --auth resource_principal iam db-token get
# A session-token profile must be created/renewed with oci session authenticate.
oci --auth security_token --profile SESSION iam db-token get

ディレクトリには、tokenと、それに対応するoci_db_key.pemを配置する必要があります。oracle.jdbc.tokenAuthentication=OCI_TOKENとoracle.jdbc.tokenLocationを設定すると、JDBCからこれらを読み込めます。ここで使うのは、データベーストークンと、その所持証明用の鍵です。API署名鍵やTLSのウォレットではありません。ファイル方式では、JDBCからCLIを呼び出したり、新しいトークンを取得したりすることはありません。このデモでは、アプリケーションを停止し、トークンと鍵を再生成してから再起動します。長時間稼働させるトークン生成処理を用意する場合は、2つのファイルを、組み合わせが崩れないように読み書きする必要があります。個別に置き換えるだけでは、2つを一度に切り替える不可分な更新にはなりません。継続して稼働するサービスには、SDK方式かプロバイダー方式を優先してください。

次のファイル方式のプロファイルには、必要な設定を一式含めているため、そのまま起動プログラムに渡せます。

# Use an EZConnect+ TCPS URL with the host, port, and service from Database Connection.
app.url=${ADB_JDBC_URL}
app.mode=file
oracle.jdbc.tokenAuthentication=OCI_TOKEN
oracle.jdbc.tokenLocation=${OCI_DB_TOKEN_DIR}

実際にJDBCを呼び出す部分は、短いコードで記述できます。次の抜粋では、1つの物理接続を作成します。実行可能なアプリケーション全体では、これに加えて、設定の検証と認証されたIDの表示も行います。

OracleDataSource ds = new OracleDataSource();
ds.setURL(System.getenv("ADB_JDBC_URL"));
Properties properties = new Properties();
properties.setProperty("oracle.jdbc.tokenAuthentication", "OCI_TOKEN");
properties.setProperty("oracle.jdbc.tokenLocation", System.getenv("OCI_DB_TOKEN_DIR"));
ds.setConnectionProperties(properties);
try (Connection connection = ds.getConnection()) {
    // Execute the read-only identity query shown below.
}

方法2:SDKでOCIトークンを生成し、キャッシュする

./run.sh config/oci-sdk.properties builder
./run.sh config/oci-sdk.properties jdbc 3
./run.sh config/oci-sdk.properties ucp

OciTokens.javaでは、2048ビットのRSA鍵ペアを生成し、公開鍵とデータベースの対象範囲をOCIのIDデータプレーンに送ります。取得したJWTと秘密鍵から、AccessToken.createJsonWebToken(jwt, privateKey)でアクセストークンを作成します。秘密鍵はメモリ内に保持します。builderでは、createConnectionBuilder().accessToken(...)を通じて、新しく取得したトークンを1つ渡します。jdbcとucpでは、AccessToken.createJsonWebTokenCache(...)で作成した、キャッシュ付きのトークンサプライヤーをデータソースに設定します。独自にトークン更新用のスケジューラーを作る必要はありません。

SDKで別の種類のIDを使う場合は、プロファイルをconfig/oci-sdk.local.propertiesにコピーし、app.oci.authenticationをsession-token、instance-principal、resource-principalのいずれかに変更します。ワークロードのIDを使う場合は、不要なapp.oci.configFileとapp.oci.profileの行を削除してください。セッションの認証情報は、後で新しいトークンを要求するときにも有効である必要があります。同じデータソースで、このサプライヤーと、ユーザー名/パスワードやアクセストークンプロバイダーを併用しないでください。

アプリケーションで使用するトークンサプライヤーの実装全体は、次のとおりです。

package demo;

import com.oracle.bmc.auth.AbstractAuthenticationDetailsProvider;
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider;
import com.oracle.bmc.auth.InstancePrincipalsAuthenticationDetailsProvider;
import com.oracle.bmc.auth.ResourcePrincipalAuthenticationDetailsProvider;
import com.oracle.bmc.auth.SessionTokenAuthenticationDetailsProvider;
import com.oracle.bmc.identitydataplane.DataplaneClient;
import com.oracle.bmc.identitydataplane.model.GenerateScopedAccessTokenDetails;
import com.oracle.bmc.identitydataplane.requests.GenerateScopedAccessTokenRequest;
import oracle.jdbc.AccessToken;
import java.security.KeyPairGenerator;
import java.util.Base64;
import java.util.Properties;
import java.util.function.Supplier;

/** Application-owned SDK acquisition: the PoP key never leaves memory. */
public final class OciTokens {
    private OciTokens() {}

    public static Supplier<? extends AccessToken> cached(Properties settings) {
        return AccessToken.createJsonWebTokenCache(() -> request(settings));
    }

    public static AccessToken request(Properties settings) {
        try {
            String profile = settings.getProperty("app.oci.profile", "DEFAULT");
            String file = settings.getProperty("app.oci.configFile",
                    System.getProperty("user.home") + "/.oci/config");
            AbstractAuthenticationDetailsProvider identity = switch (
                    settings.getProperty("app.oci.authentication", "config-file")) {
                case "config-file" -> new ConfigFileAuthenticationDetailsProvider(file, profile);
                case "session-token" -> new SessionTokenAuthenticationDetailsProvider(file, profile);
                case "instance-principal" -> InstancePrincipalsAuthenticationDetailsProvider.builder().build();
                case "resource-principal" -> ResourcePrincipalAuthenticationDetailsProvider.builder().build();
                default -> throw new IllegalArgumentException("Unsupported app.oci.authentication");
            };
            try {
                var generator = KeyPairGenerator.getInstance("RSA");
                generator.initialize(2048);
                var pair = generator.generateKeyPair();
                var details = GenerateScopedAccessTokenDetails.builder()
                        .scope(Settings.required(settings, "app.oci.scope"))
                        .publicKey(Base64.getEncoder().encodeToString(pair.getPublic().getEncoded()))
                        .build();
                try (var client = DataplaneClient.builder().build(identity)) {
                    String jwt = client.generateScopedAccessToken(GenerateScopedAccessTokenRequest.builder()
                            .generateScopedAccessTokenDetails(details).build()).getSecurityToken().getToken();
                    return AccessToken.createJsonWebToken(jwt.toCharArray(), pair.getPrivate());
                }
            } finally {
                if (identity instanceof SessionTokenAuthenticationDetailsProvider session) session.close();
            }
        } catch (Exception failure) {
            // SDK exception messages can contain request details; the CLI prints only the type.
            throw new IllegalStateException("OCI token acquisition failed", failure);
        }
    }
}

接続を1つ作成する場合は、ビルダーを使います。物理接続を繰り返し作成する場合は、キャッシュ付きのサプライヤーをデータソースに設定します。次の2つは、同時に使うのではなく、どちらかを選ぶ設定例です。

// Single-token builder example
OracleDataSource ds = new OracleDataSource();
ds.setURL(config.url());
ds.setConnectionProperties(config.jdbc());
try (Connection c = ds.createConnectionBuilder()
        .accessToken(OciTokens.request(config.app())).build()) {
    // Run the identity query.
}

// Reusable data source example
OracleDataSource reusable = new OracleDataSource();
reusable.setURL(config.url());
reusable.setConnectionProperties(config.jdbc());
reusable.setTokenSupplier(OciTokens.cached(config.app()));
try (Connection c = reusable.getConnection()) {
    // Run the identity query.
}

configは、アプリケーションで検証済みのSettingsオブジェクトです。キャッシュ付きのサプライヤーは、複数の呼び出し元で共有します。トークンの有効期限を考慮する動作と、取得処理への呼び出しを順番に実行する仕組みは、OracleのAccessToken APIで定義されています。キャッシュを使っても、クラウド認証に失敗した状態でデータベースセッションを確立できるようにはなりません。トークンの取得に失敗した場合は、その失敗を確認できるようにしておく必要があります。

方法3:OCIプロバイダーにトークンの取得を任せる

Computeのインスタンスプリンシパルを使う手順全体、Data Scienceのリソースプリンシパルを使う例、Javaプログラム全体を、それぞれ参照できます。

./run.sh config/oci-config-file.properties jdbc
./run.sh config/oci-instance-principal.properties ucp
./run.sh config/oci-resource-principal.properties ucp
./run.sh config/oci-cloud-shell.properties jdbc
./run.sh config/oci-interactive.properties jdbc

各コマンドは、その認証方式を使うことを想定した実際の環境で実行してください。oracle.jdbc.tokenAuthenticationには、OCI_API_KEY、OCI_INSTANCE_PRINCIPAL、OCI_RESOURCE_PRINCIPAL、Cloud Shell用のOCI_DELEGATION_TOKEN、またはOCI_INTERACTIVEを設定します。この設定に応じてJDBCドライバーが拡張を自動選択するため、アクセストークンプロバイダーのプロパティを明示的に指定する必要はありません。トークンの取得とキャッシュは、プロバイダーで行います。Cloud Shellでは管理済みのセッションを使います。対話認証ではブラウザーを開くため、デスクトップ環境でコールバックを受け取れる必要があります。config-file方式のプロバイダーで使うのは、APIキーの設定です。OCI CLIのセッショントークンのプロファイルを使う場合は、SDKのsession-token方式を選んでください。

インスタンスプリンシパルまたはリソースプリンシパルを使う場合は、TCPSのEZConnect+ URLに、トークン認証のモードを直接指定します。

jdbc:oracle:thin:@tcps://<host>:<port>/<service>?token_auth=oci_instance_principal

リソースプリンシパルを使う場合は、oci_instance_principalをoci_resource_principalに置き換えます。データベースの対象範囲は、JDBC接続プロパティのoracle.jdbc.ociCompartmentとoracle.jdbc.ociDatabaseで指定します。OCI SDK/プロバイダーでは、実行環境のIDを使ってトークンを取得し、キャッシュします。クラスパスにはOCIプロバイダーの依存ライブラリを含めてください。このURLは、JDBCでもUCPでも利用できます。

APIキー用プロファイルでは、必要なプロバイダーのプロパティを、すべて次のように指定します。

# EZConnect+: jdbc:oracle:thin:@tcps://host:port/service
app.url=${ADB_JDBC_URL}
app.mode=provider
oracle.jdbc.tokenAuthentication=OCI_API_KEY
oracle.jdbc.ociCompartment=${OCI_COMPARTMENT_ID}
oracle.jdbc.ociDatabase=${OCI_DATABASE_ID}
oracle.jdbc.ociConfigFile=${OCI_CONFIG_FILE}
oracle.jdbc.ociProfile=${OCI_PROFILE}

インスタンスプリンシパル用とリソースプリンシパル用のプロファイルでは、oracle.jdbc.tokenAuthenticationを対応する値に変更し、設定ファイルやプロファイル名を指定する項目は省略します。既存のJavaアプリケーションに組み込む場合は、これらのoracle.jdbc.*プロパティをOracleDataSourceまたはUCPのデータソースに読み込み、OCIプロバイダーの依存関係を追加します。SQLやクエリのコードを変更する必要はありません。デモのプロファイルでは、Settingsを使って、アプリケーション固有のapp.*設定と環境変数の置換を処理しています。Propertiesオブジェクトをデータソースに直接渡しただけでは、${...}は展開されません。

別の方法:JDBCでプロパティファイルを読み込み、変数を展開する

Oracle JDBC標準のoracle.jdbc.configFileを使うと、プロパティファイル内の変数を置き換えられます。物理接続を作成するたびにJDBCドライバーでファイルを読み込み、${NAME}に対応する値を、JVMのシステムプロパティまたは環境変数から取得します。両方にある場合はシステムプロパティを優先し、どちらにもなければエラーにします。この標準機能で読み込むファイルには、JDBCのプロパティを記述します。デモ固有のapp.*設定は含めません。

# config/native-oci-instance.properties
oracle.jdbc.tokenAuthentication=OCI_INSTANCE_PRINCIPAL
oracle.jdbc.ociCompartment=${OCI_COMPARTMENT_ID}
oracle.jdbc.ociDatabase=${OCI_DATABASE_ID}
oracle.net.CONNECT_TIMEOUT=15000
oracle.jdbc.ReadTimeout=30000
OracleDataSource ds = new OracleDataSource();
ds.setURL(System.getenv("ADB_JDBC_URL")); // TCPS EZConnect+ URL
ds.setConnectionProperty(OracleConnection.CONNECTION_PROPERTY_CONFIG_FILE,
    Path.of("config/native-oci-instance.properties").toAbsolutePath().toString());
try (Connection connection = ds.getConnection()) {
    // Execute a read-only identity query.
}

サンプルプロジェクトには、demo.NativeConfigFileDemoと、JDBC標準の設定ファイル形式で記述したOCIインスタンスプリンシパル用およびEntraマネージドID用のプロファイルを用意しています。このデモでは、プロパティの読み込みをJDBCに任せ、TCPSのEZConnect+ URLを検証し、セッションのID情報だけを問い合わせます。プロジェクトをビルドし、プロファイルで使う環境変数を設定した後、次のコマンドを実行してください。

java -cp 'target/classes:target/lib/*' demo.NativeConfigFileDemo config/native-oci-instance.properties
# In an authorized Azure managed-identity runtime instead:
java -cp 'target/classes:target/lib/*' demo.NativeConfigFileDemo config/native-entra-managed.properties

信頼できるファイルだけを使い、シークレットとウォレットをGitに登録せず、TLSの検証を有効に保ってください。JDBC標準のファイル読み込みを利用するための組み込み部分は、単体テストで確認しています。ただし、これらの新しい起動方法について、実際のクラウドへのログインまで検証済みだと示しているわけではありません。

OCIの対話認証用プロファイルでも、同じデータベースの対象範囲を指定します。認証モードとして、OCI_INTERACTIVEを選択します。

# EZConnect+: jdbc:oracle:thin:@tcps://host:port/service
app.url=${ADB_JDBC_URL}
app.mode=provider
oracle.jdbc.tokenAuthentication=OCI_INTERACTIVE
oracle.jdbc.ociCompartment=${OCI_COMPARTMENT_ID}
oracle.jdbc.ociDatabase=${OCI_DATABASE_ID}

インスタンスプリンシパルとリソースプリンシパルを使う一連の手順

次の2つの手順では、同じJavaプログラム全体を、異なるOCIの実行環境で動かします。これらの例は、コンパイルと自動テストには成功しています。ただし、実際のCompute/Data Science環境からのログインや、トークンの更新は、ここではまだ検証していません。APIキーでログインできても、どちらかのワークロードプリンシパルで認証できた証拠にはなりません。ソースコード、バージョンを固定したMavenの依存関係、実行用プロファイルは、oracle-db-examples/java/jdbc-token-authに置いています。デプロイ手順全体のガイドには、どこまで検証しているかを記載しています。

専用のAutonomous AI Databaseを用意し、IAM認証を有効にしてください。そのデータベースのTLSエンドポイントに、ワークロードの実行環境から接続できる必要があります。IAMをまだ有効にしていない場合は、管理者が次の呼び出しで有効化できます。このテストのために、共有データベースやEntra設定済みのデータベースの認証方式を切り替えないでください。

BEGIN
  DBMS_CLOUD_ADMIN.ENABLE_EXTERNAL_AUTHENTICATION(type => 'OCI_IAM');
END;
/

どちらの手順でも、Autonomous固有の、動的グループを共有スキーマに対応付ける方法を使います。実際の識別子を指定し、必要に応じて、グループ名にはIDドメイン名も付けてください。一般のDatabase Security Guideには、プリンシパルのOCIDを使う排他的な対応付けも説明されています。ただし、どのサービスやバージョンでも、同じ選択肢を利用できるとは限りません。AutonomousのIAMポリシーのガイドと、一般的なプリンシパルの対応付けのガイドを参照してください。

OCI_INSTANCE_PRINCIPAL:OCI Compute

  1. データベースとOCI IAMにネットワーク接続できる、専用のCompute VMを選びます。そのVMだけに一致する動的グループJdbcInstanceDemoを作成します。一致ルールはinstance.id = '<compute-instance-ocid>'です。
  2. そのグループに、Autonomousのドキュメントで定められたトークンアクセス用ポリシーを、対象データベースに範囲を限定して付与します:Allow dynamic-group JdbcInstanceDemo to use autonomous-database-family in compartment id <database-compartment-ocid> where target.id = '<autonomous-database-ocid>'
  3. データベース管理者として、セッションへのアクセス権限だけを持つ、専用のグローバルスキーマを作成します:CREATE USER TOKEN_INSTANCE_DEMO IDENTIFIED GLOBALLY AS 'IAM_GROUP_NAME=JdbcInstanceDemo'; GRANT CREATE SESSION TO TOKEN_INSTANCE_DEMO;
  4. VMにJava 17以降とMavenをインストールし、次のコマンドでビルドして実行します:git clone https://github.com/oracle-samples/oracle-db-examples.git cd oracle-db-examples/java/jdbc-token-auth mvn -B -ntp verify export ADB_JDBC_URL='jdbc:oracle:thin:@tcps://<host>:<port>/<service>' export OCI_COMPARTMENT_ID='<database-compartment-ocid>' export OCI_DATABASE_ID='<autonomous-database-ocid>' export EXPECTED_DB_USER=TOKEN_INSTANCE_DEMO java -cp 'target/classes:target/lib/*' demo.OciWorkloadPrincipalDemo instance check java -cp 'target/classes:target/lib/*' demo.OciWorkloadPrincipalDemo instance jdbc java -cp 'target/classes:target/lib/*' demo.OciWorkloadPrincipalDemo instance ucp

このプログラムでは、OCI_INSTANCE_PRINCIPALを明示的に選択します。プロバイダーを通じてVMのインスタンスIDを使うため、OCI CLIでのログイン、APIキーのプロファイル、データベースパスワード、アクセストークンプロバイダーの明示的な設定は渡しません。VM上のプロセスでは同じプリンシパルを使える場合があるため、インスタンスメタデータへのアクセスを保護してください。この方式を選んでも、ノートPCでOCIインスタンスのIDを使えるようになるわけではありません。

OCI_RESOURCE_PRINCIPAL:OCI Data Scienceノートブック

  1. データベースとIAMに接続できる、専用のData Scienceノートブックセッションを使います。そのJupyterLabターミナルを開きます。そのノートブックだけを対象とする動的グループJdbcResourceDemoを作成します。一致ルールはALL {resource.type = 'datasciencenotebooksession', resource.id = '<notebook-session-ocid>'}です。
  2. 対象データベースに範囲を限定した、このグループ専用のポリシーを作成します:Allow dynamic-group JdbcResourceDemo to use autonomous-database-family in compartment id <database-compartment-ocid> where target.id = '<autonomous-database-ocid>'
  3. データベース管理者として、専用のグローバルスキーマを作成します:CREATE USER TOKEN_RESOURCE_DEMO IDENTIFIED GLOBALLY AS 'IAM_GROUP_NAME=JdbcResourceDemo'; GRANT CREATE SESSION TO TOKEN_RESOURCE_DEMO;
  4. ノートブック環境にJava 17以降とMavenをインストールし、ターミナルで次を実行します:git clone https://github.com/oracle-samples/oracle-db-examples.git cd oracle-db-examples/java/jdbc-token-auth mvn -B -ntp verify export ADB_JDBC_URL='jdbc:oracle:thin:@tcps://<host>:<port>/<service>' export OCI_COMPARTMENT_ID='<database-compartment-ocid>' export OCI_DATABASE_ID='<autonomous-database-ocid>' export EXPECTED_DB_USER=TOKEN_RESOURCE_DEMO java -cp 'target/classes:target/lib/*' demo.OciWorkloadPrincipalDemo resource check java -cp 'target/classes:target/lib/*' demo.OciWorkloadPrincipalDemo resource jdbc java -cp 'target/classes:target/lib/*' demo.OciWorkloadPrincipalDemo resource ucp

このプログラムでは、OCI_RESOURCE_PRINCIPALを明示的に選択します。SDKを通じて、サービスから提供されたリソースプリンシパル用の環境設定と認証情報を読み込みます。これらの認証情報をノートPCにコピーしたり、OCI_RESOURCE_PRINCIPAL_*の値を自分で作って設定したりしないでください。Data Scienceではリソースプリンシパルのトークンを15分間キャッシュするため、ポリシーを変更しても、反映を確認できるまで時間がかかる場合があります。ノートブック実行環境のドキュメントを参照してください。

これは、OKEやGKEのワークロードIDを使うための手順ではありません。
それらのKubernetesサービスアカウントのIDを、このプロバイダーが使う汎用のOCIリソースプリンシパル用環境設定と、そのまま置き換えることはできません。OKEやGKEとの連携では、トークンと、そのトークンをデータベースが受け入れるかどうかを、別途テストする必要があります。Base Database Serviceでは、上記のAutonomous用ポリシーではなく、target.database.idを指定するdatabase-connectionsポリシーを使ってください。

以下に示すのは、サンプルプロジェクトのdemo.OciWorkloadPrincipalDemoのソースそのものです。プロジェクト内のSettings.validateUrlを使い、接続URLをTCPSのEZConnect+形式に限定して、認証情報の指定、接続先の別名、安全でないURLパラメーターによる上書きを拒否します。このファイルだけでなく、プロジェクト全体をビルドしてください。tnsnames.oraや、URLへのdn_match設定は不要です。信頼できるTLSエンドポイントを使い、データベースでmTLSが必要な場合にだけ、ウォレットを別途設定します。Javaのソース全体では、使用するIDを明示し、読み取り専用で認証結果を確認し、接続数などに上限を設けてUCPを使います。

両方の手順で使うJDBCとUCPのプログラム全体

以下に示すのは、サンプルプロジェクトのdemo.OciWorkloadPrincipalDemoのソースそのものです。プロジェクト内のSettings.validateUrlを使い、接続URLをTCPSのEZConnect+形式に限定して、認証情報の指定、接続先の別名、安全でないURLパラメーターによる上書きを拒否します。このファイルだけでなく、プロジェクト全体をビルドしてください。tnsnames.oraや、URLへのdn_match設定は不要です。信頼できるTLSエンドポイントを使い、データベースでmTLSが必要な場合にだけ、ウォレットを別途設定します。

Javaのソース全体は次のとおりです。使用するIDを明示し、読み取り専用で認証結果を確認し、接続数などに上限を設けてUCPを使います。

package demo;

import java.sql.Connection;
import java.sql.SQLException;
import java.time.Duration;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import oracle.jdbc.datasource.impl.OracleDataSource;
import oracle.ucp.admin.UniversalConnectionPoolManagerImpl;
import oracle.ucp.jdbc.PoolDataSourceFactory;

/** Explicit OCI workload identity, with no database password or API-key fallback. */
public final class OciWorkloadPrincipalDemo {
    private OciWorkloadPrincipalDemo() { }

    record Config(String url, String expectedUser, Properties properties) { }

    static Config configure(String principal, Map<String, String> env) {
        String method = switch (principal) {
            case "instance" -> "OCI_INSTANCE_PRINCIPAL";
            case "resource" -> "OCI_RESOURCE_PRINCIPAL";
            default -> throw new IllegalArgumentException("Use instance or resource");
        };
        String url = required(env, "ADB_JDBC_URL");
        Settings.validateUrl(url);
        String expected = required(env, "EXPECTED_DB_USER");
        if (!expected.matches("[A-Z][A-Z0-9_]{0,127}"))
            throw new IllegalArgumentException("EXPECTED_DB_USER must be an uppercase unquoted schema name");
        Properties properties = new Properties();
        properties.setProperty("v$session.program", "devrel-developerhub-java-jdbc-token-auth");
        properties.setProperty("oracle.jdbc.tokenAuthentication", method);
        properties.setProperty("oracle.jdbc.ociCompartment", required(env, "OCI_COMPARTMENT_ID"));
        properties.setProperty("oracle.jdbc.ociDatabase", required(env, "OCI_DATABASE_ID"));
        properties.setProperty("oracle.net.CONNECT_TIMEOUT", "15000");
        properties.setProperty("oracle.jdbc.ReadTimeout", "30000");
        return new Config(url, expected, properties);
    }

    private static String required(Map<String, String> env, String name) {
        String value = env.get(name);
        if (value == null || value.isBlank()) throw new IllegalArgumentException("Set " + name);
        return value;
    }

    public static void main(String[] args) {
        try {
            if (args.length != 2 || !Set.of("check", "jdbc", "ucp").contains(args[1]))
                throw new IllegalArgumentException("Usage: OciWorkloadPrincipalDemo instance|resource check|jdbc|ucp");
            Config config = configure(args[0], System.getenv());
            if (args[1].equals("check")) {
                System.out.println("Configuration valid; no identity, token, or database test performed.");
                return;
            }
            // The runtime supplies instance metadata or resource-principal credentials.
            // Selecting a method does not create that identity on a developer laptop.
            if (args[1].equals("jdbc")) {
                OracleDataSource ds = new OracleDataSource();
                ds.setURL(config.url());
                ds.setConnectionProperties(config.properties());
                try (Connection connection = ds.getConnection()) {
                    verify(connection, config.expectedUser());
                }
            } else {
                var pool = PoolDataSourceFactory.getPoolDataSource();
                String name = "WorkloadPrincipalDemo";
                pool.setConnectionPoolName(name);
                pool.setConnectionFactoryClassName(OracleDataSource.class.getName());
                pool.setURL(config.url());
                pool.setConnectionProperties(config.properties());
                pool.setInitialPoolSize(0);
                pool.setMinPoolSize(0);
                pool.setMaxPoolSize(2);
                pool.setConnectionWaitDuration(Duration.ofSeconds(20));
                pool.setValidateConnectionOnBorrow(true);
                var manager = UniversalConnectionPoolManagerImpl.getUniversalConnectionPoolManager();
                try (Connection connection = pool.getConnection()) {
                    verify(connection, config.expectedUser());
                } finally {
                    if (java.util.Arrays.asList(manager.getConnectionPoolNames()).contains(name))
                        manager.destroyConnectionPool(name);
                }
            }
        } catch (Exception failure) {
            // Exception payloads can contain endpoints or credentials. Report codes only.
            if (failure instanceof IllegalArgumentException) System.err.println(failure.getMessage());
            else {
                System.err.println("Connection/identity check failed: " + failure.getClass().getSimpleName());
                for (Throwable cause = failure; cause != null; cause = cause.getCause())
                    if (cause instanceof SQLException sql)
                        System.err.println("Oracle error=" + sql.getErrorCode() + "; SQLState=" + sql.getSQLState());
            }
            System.exit(1);
        }
    }

    static void verifyIdentity(String expected, String actual, String authentication) throws SQLException {
        if (!expected.equals(actual) || !"TOKEN_GLOBAL".equals(authentication))
            throw new SQLException("Unexpected database identity or authentication method");
    }

    private static void verify(Connection connection, String expected) throws SQLException {
        try (var statement = connection.prepareStatement("""
                SELECT SYS_CONTEXT('USERENV','SESSION_USER'),
                       SYS_CONTEXT('USERENV','AUTHENTICATION_METHOD') FROM dual
                """)) {
            statement.setQueryTimeout(20);
            try (var rows = statement.executeQuery()) {
                if (!rows.next()) throw new SQLException("Identity query returned no row");
                verifyIdentity(expected, rows.getString(1), rows.getString(2));
                System.out.println("PASS: expected schema; authentication=TOKEN_GLOBAL; read-only query completed.");
            }
        }
    }
}

checkでは、OCIやデータベースに接続せず、設定を検証します。想定するスキーマとTOKEN_GLOBALを確認したうえでPASSを表示できるのは、jdbcとucpだけです。2つの対象範囲のプロパティを指定することで、広範なデータベースを対象にしたトークンの要求を防ぎます。JDBCの接続、読み取り、クエリのタイムアウトを設定しても、クラウドでトークンを取得する処理全体の所要時間まで制限できるわけではありません。

短時間の実行が成功しただけでは、トークンを更新できるかまでは確認できません。最初の接続に成功したら、対応するプリンシパルのプロファイルと、既存のucp-fresh検証機能を使い、トークンの有効期限をまたいで物理接続を作り直すテストを行えます。トークンと、加工前の例外情報は公開しないでください。拒否されることを確認するテストでは、本番で共有しているポリシーを取り消すのではなく、許可対象から外した別の実行環境を使います。終了後に削除するのは、この例のために用意した専用のポリシー、グループ、スキーマ、実行環境だけにしてください。

方法4:Entraプロバイダーにトークンの取得を任せる

Entraとの連携を設定したデータベースのURLを使います。次に、秘密情報ではない次の識別子を設定します。

export AZURE_TENANT_ID='<tenant-id>'
export AZURE_CLIENT_ID='<java-client-application-id>'
export ENTRA_DB_APP_ID_URI='https://<tenant-domain>/<database-application-id>'
# Full scope for SDK/file acquisition; provider profiles use the bare URI.
export ENTRA_DB_SCOPE="${ENTRA_DB_APP_ID_URI}/.default"

クライアントシークレットを使う場合は、実行環境のシークレット管理機能を通じてAZURE_CLIENT_SECRETを渡します。ローカルのBashで試す場合は、次のように入力を読み取り、ターミナルへの表示や、シェルの履歴への値の直接記録を避けてください。

read -r -s -p 'Client secret: ' AZURE_CLIENT_SECRET; echo
export AZURE_CLIENT_SECRET
./run.sh config/entra-service-principal.properties jdbc
./run.sh config/entra-service-principal.properties ucp
unset AZURE_CLIENT_SECRET

証明書で認証する場合は、AZURE_CLIENT_CERTIFICATE_PATHを設定します。暗号化されたPFXを使う場合は、AZURE_CLIENT_CERTIFICATE_PASSWORDも設定します。クライアントシークレットの設定を解除し、同じサービスプリンシパル用プロファイルを使ってください。対応する証明書は、クライアント用アプリケーションに登録します。ここで使うIDの認証情報は、データベースのパスワードではありません。

ほかの認証方式の実行例は、次のとおりです。

# On Azure with a configured managed identity and database app-role assignment:
# For a system-assigned identity, unset AZURE_CLIENT_ID first.
./run.sh config/entra-managed-identity.properties ucp

# Public client with delegated permission, user role assignment and redirect URI:
# Keep ENTRA_DB_APP_ID_URI as the bare resource URI for provider profiles.
./run.sh config/entra-interactive.properties jdbc
./run.sh config/entra-device-code.properties jdbc

ブラウザー認証とデバイスコード認証では、人間のユーザーを認証します。サービスプリンシパルとマネージドIDでは、アプリケーションなどのワークロードを認証します。JavaクライアントのIDとして、データベースリソースのIDを指定しないでください。登録、ロール、同意、トークンのバージョンについては、セットアップの節を参照してください。

各プロファイルでは、AZURE_SERVICE_PRINCIPAL、AZURE_MANAGED_IDENTITY、AZURE_INTERACTIVE、AZURE_DEVICE_CODEのいずれかを選択します。接続URLで、プロバイダーのモードを直接指定することもできます。

jdbc:oracle:thin:@tcps://<host>:<port>/<service>?token_auth=azure_managed_identity

ほかの方式を使う場合は、token_authをazure_service_principal、azure_interactive、azure_device_codeのいずれかに変更します。JDBC接続プロパティのoracle.jdbc.azureDatabaseApplicationIdUriには、末尾に/.defaultや/session:scope:connectを付けず、データベースリソースのURIだけを指定します。ユーザー割り当てマネージドIDや対話認証を使う場合は、クライアントとテナントのプロパティも必要になることがあります。クライアントシークレットや証明書の情報を、URLに含めないでください。

サービスプリンシパル用のプロパティは、次のように設定します。

# EZConnect+: jdbc:oracle:thin:@tcps://host:port/service
app.url=${ADB_JDBC_URL}
app.mode=provider
oracle.jdbc.tokenAuthentication=AZURE_SERVICE_PRINCIPAL
# Database application URI, without /.default or /session:scope:connect.
oracle.jdbc.azureDatabaseApplicationIdUri=${ENTRA_DB_APP_ID_URI}

ブラウザー認証用のプロファイルでは、クライアント、テナント、コールバックを明示的に指定します。

# EZConnect+: jdbc:oracle:thin:@tcps://host:port/service
app.url=${ADB_JDBC_URL}
app.mode=provider
oracle.jdbc.tokenAuthentication=AZURE_INTERACTIVE
# Database application URI, without /.default or /session:scope:connect.
oracle.jdbc.azureDatabaseApplicationIdUri=${ENTRA_DB_APP_ID_URI}
oracle.jdbc.tenantId=${AZURE_TENANT_ID}
oracle.jdbc.clientId=${AZURE_CLIENT_ID}
oracle.jdbc.redirectUri=http://localhost:8400

プロバイダーの詳細は、OracleのOCIモジュールとAzureモジュールで説明されています。「データベースへの接続にパスワードを使わない」ことは、「どこにもシークレットが不要」という意味ではありません。APIキーやクライアントシークレットを使う方式では、トークンの取得元となるIDサービスへの認証情報が必要です。対応する環境でワークロードのIDを使えば、その認証情報をアプリケーション側で管理せずに済むようにできます。

Entraで別の方法を使う:トークンファイルとSDKのサプライヤー

外部で作成したEntraのトークンファイルを使う

必要な権限を持つ外部の処理で、データベース向けのアクセストークンをUTF-8のJWTファイルに書き出せます。OCIのファイル方式とは異なり、Entraでは所持証明用の鍵は不要です。生成側では、Microsoft Graphではなくデータベースリソースを対象にトークンを要求し、その更新も行う必要があります。ディレクトリを保護し、ファイルのアクセス権を0600に設定してください。一時ファイルへの書き込みを完了してから、不可分な操作で対象ファイルと置き換えます。JWTを画面に表示したり、リポジトリへコミットしたりしないでください。

app.url=${ADB_JDBC_URL}
app.mode=file
oracle.jdbc.tokenAuthentication=OAUTH
oracle.jdbc.tokenLocation=${ENTRA_DB_TOKEN_FILE}
export ENTRA_DB_TOKEN_FILE='/private/path/to/entra-db-token'
./run.sh config/entra-file.properties jdbc
./run.sh config/entra-file.properties ucp

データベースへのアクセスに必要な同意を取得済みであれば、Azure CLIを使った生成処理で、az account get-access-token --resource "$ENTRA_DB_APP_ID_URI" --query accessToken -o tsvの出力を、保護した一時ファイルにリダイレクトできます。ただし、CLIでサインインできたことだけでは、データベースの利用権限があるとは確認できません。ファイル方式のテストで示すように、SDKで取得したトークンを書き出すこともできます。JDBCのファイル方式では、いずれの生成処理もJDBCから自動的には呼び出しません。

アプリケーションからAzure Identityを使って取得する

SDK方式では、AccessToken.createJsonWebToken(jwt.toCharArray())を使います。OCIの場合に指定する秘密鍵の引数は、ここでは不要です。ビルダー方式ではトークンを1つ渡し、JDBCとUCPでは、取得処理をAccessToken.createJsonWebTokenCacheで包んだ、キャッシュ付きサプライヤーを使います。ソースに含まれるプロファイルは、サービスプリンシパル用であり、アプリケーションのスコープとして/.defaultを使います。人間のユーザーがログインする場合は、代わりにデータベースへの委任スコープを使います。

ブラウザーで対話認証するプロファイルを実行するには、テナント、パブリッククライアントID、データベースリソースのURI、委任スコープを指定します。パブリック/ネイティブクライアントに、リダイレクトURIとしてhttp://localhost:8400を登録します。データベース用アプリケーションのsession:scope:connectスコープへの委任権限を付与し、同意を取得してください。さらに、ユーザーまたはグループに、データベース用アプリケーションで必要なロールを割り当てます。

config/entra-sdk.propertiesを基にする場合は、app.entra.authenticationをservice-principalからinteractiveに変更します。アプリケーション用の/.defaultスコープを、委任用の/session:scope:connectスコープに置き換え、テナントIDとパブリッククライアントIDを追加し、ブラウザーのリダイレクトURIを設定します。

export AZURE_TENANT_ID='<tenant-id>'
export AZURE_CLIENT_ID='<public-client-application-id>'
export ENTRA_DB_APP_ID_URI='https://<tenant-domain>/<database-application-id>'
export ENTRA_DB_SCOPE="${ENTRA_DB_APP_ID_URI}/session:scope:connect"
cat > config/entra-sdk-human.local.properties <<'EOF'
app.url=${ADB_JDBC_URL}
app.mode=entra-sdk
app.entra.authentication=interactive
app.entra.scope=${ENTRA_DB_SCOPE}
app.entra.tenantId=${AZURE_TENANT_ID}
app.entra.clientId=${AZURE_CLIENT_ID}
app.entra.redirectUri=http://localhost:8400
EOF
./run.sh config/entra-sdk-human.local.properties check
./run.sh config/entra-sdk-human.local.properties jdbc
./run.sh config/entra-sdk-human.local.properties ucp

デバイスコードでログインする場合は、テナント、クライアント、委任スコープの値はそのまま使います。app.entra.authentication=interactiveをapp.entra.authentication=device-codeに変更し、app.entra.redirectUriを削除します。デバイスコード認証では、ブラウザーからのコールバックURIは使いません。これらの認証情報の種類は、明示的に選択するものです。失敗したら自動的に次の方式へ切り替わる仕組みではありません。

EntraTokens.javaの実装全体は、次のとおりです。

package demo;

import com.azure.core.credential.TokenCredential;
import com.azure.core.credential.TokenRequestContext;
import com.azure.identity.*;
import oracle.jdbc.AccessToken;
import java.time.Duration;
import java.util.Properties;
import java.util.function.Supplier;

/** Explicit Azure Identity acquisition; no database password or OCI PoP key. */
public final class EntraTokens {
    private EntraTokens() {}

    public static Supplier<? extends AccessToken> cached(Properties settings) {
        return AccessToken.createJsonWebTokenCache(supplier(settings));
    }

    static Supplier<AccessToken> supplier(Properties settings) {
        TokenCredential credential = switch (Settings.required(settings, "app.entra.authentication")) {
            // Azure Identity reads AZURE_TENANT_ID, AZURE_CLIENT_ID and secret/certificate from the environment.
            case "service-principal" -> new EnvironmentCredentialBuilder().build();
            case "managed-identity" -> {
                var builder = new ManagedIdentityCredentialBuilder();
                String clientId = System.getenv("AZURE_CLIENT_ID");
                if (clientId != null && !clientId.isBlank()) builder.clientId(clientId);
                yield builder.build();
            }
            case "interactive" -> new InteractiveBrowserCredentialBuilder()
                    .tenantId(Settings.required(settings, "app.entra.tenantId"))
                    .clientId(Settings.required(settings, "app.entra.clientId"))
                    .redirectUrl(settings.getProperty("app.entra.redirectUri", "http://localhost:8400"))
                    .build();
            case "device-code" -> new DeviceCodeCredentialBuilder()
                    .tenantId(Settings.required(settings, "app.entra.tenantId"))
                    .clientId(Settings.required(settings, "app.entra.clientId"))
                    .challengeConsumer(c -> System.out.println(c.getMessage())).build();
            case "azure-cli" -> new AzureCliCredentialBuilder()
                    .tenantId(Settings.required(settings, "app.entra.tenantId")).build();
            default -> throw new IllegalArgumentException("Unsupported app.entra.authentication");
        };
        String scope = Settings.required(settings, "app.entra.scope");
        return () -> {
            try {
                var token = credential.getToken(new TokenRequestContext().addScopes(scope))
                        .block(Duration.ofSeconds(120));
                if (token == null) throw new IllegalStateException("No Entra token returned");
                return AccessToken.createJsonWebToken(token.getToken().toCharArray());
            } catch (Exception failure) {
                throw new IllegalStateException("Entra token acquisition failed", failure);
            }
        };
    }
}

UCPでサプライヤーを共有し、物理接続の作り直しをテストする

このアプリケーションでは、プールを1つ作成し、物理接続数を最大2つに制限します。接続を取得する際の待ち時間には、通常は20秒、接続を作り直す診断時には60秒の上限を設けます。また、プールから接続を借り出す際に、接続を検証します。プロバイダー方式とファイル方式では、設定プロパティをそのままJDBCに渡します。SDK方式では、代わりにキャッシュ付きのトークンサプライヤーを渡します。実際にプールを設定するメソッドは、次のとおりです。

static PoolDataSource pool(Settings config, Supplier<? extends AccessToken> tokens, boolean fresh) throws SQLException {
        PoolDataSource ds = PoolDataSourceFactory.getPoolDataSource();
        ds.setConnectionPoolName("TokenAuthDemo");
        ds.setConnectionFactoryClassName(OracleDataSource.class.getName());
        ds.setURL(config.url());
        ds.setConnectionProperties(config.jdbc());
        ds.setInitialPoolSize(0);
        ds.setMinPoolSize(0);
        ds.setMaxPoolSize(2);
        ds.setConnectionWaitDuration(java.time.Duration.ofSeconds(fresh ? 60 : 20));
        ds.setValidateConnectionOnBorrow(true);
        if (tokens != null) ds.setTokenSupplier(tokens);
        // Diagnostic mode purges only between completed rounds, with no borrowers.
        return ds;
    }

各ラウンドでは、このプールを使って4つのタスクを実行します。診断モードでは、4つすべてのタスクが接続を返却してからプール内の接続をすべて削除し、次のラウンドを始める前に、プールが空になったことを確認します。タスク内で発生した例外も確認できるようにし、借り出した接続はすべて閉じ、終了時にプールを破棄します。

private static void runPool(Settings config, Supplier<? extends AccessToken> tokens,
                                boolean fresh, int rounds, int pause) throws Exception {
        PoolDataSource ds = pool(config, tokens, fresh);
        var executor = Executors.newFixedThreadPool(2);
        try {
            for (int round = 1; round <= rounds; round++) {
                final int current = round;
                List<Callable<Void>> tasks = new ArrayList<>();
                for (int i = 0; i < 4; i++) tasks.add(() -> {
                    try (Connection c = ds.getConnection()) {
                        query(c, current);
                    }
                    return null;
                });
                for (var result : executor.invokeAll(tasks)) result.get();
                if (fresh) {
                    // Keep the same data source/token cache, but retire all physical
                    // sessions after all workers have returned their connections.
                    UniversalConnectionPoolManagerImpl.getUniversalConnectionPoolManager()
                            .purgeConnectionPool(ds.getConnectionPoolName());
                    if (ds.getAvailableConnectionsCount() != 0 || ds.getBorrowedConnectionsCount() != 0)
                        throw new IllegalStateException("Diagnostic purge did not empty the pool");
                    System.out.println("round=" + round + " poolPurged=true available=0 borrowed=0");
                }
                if (round < rounds) Thread.sleep(pause * 1000L);
            }
        } finally {
            executor.shutdownNow();
            if (java.util.Arrays.asList(UniversalConnectionPoolManagerImpl
                    .getUniversalConnectionPoolManager().getConnectionPoolNames()).contains(ds.getConnectionPoolName()))
                UniversalConnectionPoolManagerImpl.getUniversalConnectionPoolManager()
                        .destroyConnectionPool(ds.getConnectionPoolName());
        }
    }
# Four tasks per round; purge physical sessions between completed rounds.
# About 74 minutes between first and final rounds, spanning a typical OCI token lifetime.
./run.sh config/oci-config-file.properties ucp-fresh 75 60
# Or use config/oci-sdk.properties / config/entra-service-principal.properties.

トークンが有効期限内かどうかが問題になるのは、新しい物理接続を認証するときです。プール内ですでに開いているセッションは、ログイン時に使ったトークンの期限が切れた後も、そのまま使える場合があります。そのため、接続の借り出しを繰り返すだけでは、トークンの更新が機能したと確認できません。ucp-freshでは、各ラウンドの終了後、すべての接続が返却されている状態で、プール内の物理接続をすべて削除します。そして、利用可能な接続数と借り出し中の接続数が、どちらもゼロになったことを確認します。データソースとトークンキャッシュは同じものを使い続けますが、次のラウンドでは、新しいデータベースセッションを確立する必要があります。接続を作り直す際の待ち時間は、最大60秒です。診断用にセッションIDも表示しますが、サーバー側で同じIDが再利用される場合があります。実際に発行されるトークンの有効期間を超える時間、実行してください。単体テストだけでは、クラウドでのトークン更新が機能することは実証できません。

正常に実行できた場合の出力例です。実際の値は、使用するIDとサーバーによって異なります。

round=1 user=TOKEN_DEMO identity=<mapped-principal> authentication=<server-reported-method> sid=<session-id>

データベースにどのIDで接続したかを確認する

アプリケーションでは、プリペアドステートメントを使い、次の読み取り専用クエリを実行します。

SELECT SYS_CONTEXT('USERENV','SESSION_USER'),
       SYS_CONTEXT('USERENV','AUTHENTICATED_IDENTITY'),
       SYS_CONTEXT('USERENV','AUTHENTICATION_METHOD'),
       SYS_CONTEXT('USERENV','SID'), ? FROM dual

バインドする値は、実行ラウンドの番号です。SESSION_USERが、想定した対応先のスキーマになっていることを確認してください。IDや認証方式を示す文字列は、認証方式やサーバーによって異なる場合があります。ログのプリンシパル名を、確認せずに公開しないでください。このサンプルでは、トークンや秘密鍵は表示しません。

実行プログラム全体とソースファイルの役割

リポジトリには、必要なimport文、引数の確認、設定の読み込み、終了時の処理を、すべて含めています。参考として、メインクラス全体も以下に掲載します。

TokenAuthDemo.javaの実装全体は、次のとおりです。

package demo;

import oracle.jdbc.AccessToken;
import oracle.jdbc.datasource.impl.OracleDataSource;
import oracle.ucp.jdbc.PoolDataSource;
import oracle.ucp.jdbc.PoolDataSourceFactory;
import oracle.ucp.admin.UniversalConnectionPoolManagerImpl;
import java.nio.file.Path;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.function.Supplier;

/** Executable JDBC, builder, and concurrent UCP authentication demonstrations. */
public final class TokenAuthDemo {
    public static void main(String[] args) {
        if (args.length == 0 || args[0].equals("--help")) {
            System.out.println("Usage: run.sh CONFIG [check|jdbc|builder|ucp|ucp-fresh] [rounds=1] [pauseSeconds=0]");
            return;
        }
        try {
            if (args.length > 4) throw new IllegalArgumentException("Too many arguments");
            String action = args.length > 1 ? args[1] : "jdbc";
            if (!java.util.Set.of("check", "jdbc", "builder", "ucp", "ucp-fresh").contains(action))
                throw new IllegalArgumentException("Unknown action; use --help");
            int rounds = args.length > 2 ? Integer.parseInt(args[2]) : 1;
            int pause = args.length > 3 ? Integer.parseInt(args[3]) : 0;
            if (rounds < 1 || rounds > 10000 || pause < 0 || pause > 86400)
                throw new IllegalArgumentException("rounds: 1..10000; pauseSeconds: 0..86400");
            Settings config = Settings.load(Path.of(args[0]), System.getenv());
            if (action.equals("builder") && !java.util.Set.of("sdk", "entra-sdk").contains(config.mode()))
                throw new IllegalArgumentException("builder requires SDK mode");
            if (action.equals("check")) {
                System.out.println("Configuration valid: mode=" + config.mode() + "; no cloud or database connection attempted");
                return;
            }
            Supplier<? extends AccessToken> tokens = switch (config.mode()) {
                case "sdk" -> OciTokens.cached(config.app());
                case "entra-sdk" -> action.equals("builder") ? EntraTokens.supplier(config.app()) : EntraTokens.cached(config.app());
                default -> null;
            };
            if (action.startsWith("ucp")) runPool(config, tokens, action.equals("ucp-fresh"), rounds, pause);
            else {
                OracleDataSource ds = new OracleDataSource();
                ds.setURL(config.url());
                ds.setConnectionProperties(config.jdbc());
                if (tokens != null && !action.equals("builder")) ds.setTokenSupplier(tokens);
                for (int round = 1; round <= rounds; round++) {
                    try (Connection c = action.equals("builder")
                            ? ds.createConnectionBuilder().accessToken(config.mode().equals("sdk")
                                ? OciTokens.request(config.app()) : tokens.get()).build()
                            : ds.getConnection()) {
                        query(c, round);
                    }
                    if (round < rounds) Thread.sleep(pause * 1000L);
                }
            }
        } catch (Exception failure) {
            if (failure instanceof InterruptedException) Thread.currentThread().interrupt();
            if (failure instanceof IllegalArgumentException) System.err.println(failure.getMessage());
            else {
                Throwable cause = failure;
                while (cause.getCause() != null) cause = cause.getCause();
                System.err.println("Run failed: " + cause.getClass().getSimpleName());
                if (cause instanceof SQLException sql)
                    System.err.println("Oracle error code=" + sql.getErrorCode() + "; SQLState=" + sql.getSQLState());
                if (cause instanceof oracle.ucp.UniversalConnectionPoolException poolFailure)
                    System.err.println("UCP error code=" + poolFailure.getErrorCode());
                System.err.println("See docs/troubleshooting.md. Credential values and exception payloads are not logged.");
            }
            System.exit(1);
        }
    }

    static PoolDataSource pool(Settings config, Supplier<? extends AccessToken> tokens, boolean fresh) throws SQLException {
        PoolDataSource ds = PoolDataSourceFactory.getPoolDataSource();
        ds.setConnectionPoolName("TokenAuthDemo");
        ds.setConnectionFactoryClassName(OracleDataSource.class.getName());
        ds.setURL(config.url());
        ds.setConnectionProperties(config.jdbc());
        ds.setInitialPoolSize(0);
        ds.setMinPoolSize(0);
        ds.setMaxPoolSize(2);
        ds.setConnectionWaitDuration(java.time.Duration.ofSeconds(fresh ? 60 : 20));
        ds.setValidateConnectionOnBorrow(true);
        if (tokens != null) ds.setTokenSupplier(tokens);
        // Diagnostic mode purges only between completed rounds, with no borrowers.
        return ds;
    }

    private static void runPool(Settings config, Supplier<? extends AccessToken> tokens,
                                boolean fresh, int rounds, int pause) throws Exception {
        PoolDataSource ds = pool(config, tokens, fresh);
        var executor = Executors.newFixedThreadPool(2);
        try {
            for (int round = 1; round <= rounds; round++) {
                final int current = round;
                List<Callable<Void>> tasks = new ArrayList<>();
                for (int i = 0; i < 4; i++) tasks.add(() -> {
                    try (Connection c = ds.getConnection()) {
                        query(c, current);
                    }
                    return null;
                });
                for (var result : executor.invokeAll(tasks)) result.get();
                if (fresh) {
                    // Keep the same data source/token cache, but retire all physical
                    // sessions after all workers have returned their connections.
                    UniversalConnectionPoolManagerImpl.getUniversalConnectionPoolManager()
                            .purgeConnectionPool(ds.getConnectionPoolName());
                    if (ds.getAvailableConnectionsCount() != 0 || ds.getBorrowedConnectionsCount() != 0)
                        throw new IllegalStateException("Diagnostic purge did not empty the pool");
                    System.out.println("round=" + round + " poolPurged=true available=0 borrowed=0");
                }
                if (round < rounds) Thread.sleep(pause * 1000L);
            }
        } finally {
            executor.shutdownNow();
            if (java.util.Arrays.asList(UniversalConnectionPoolManagerImpl
                    .getUniversalConnectionPoolManager().getConnectionPoolNames()).contains(ds.getConnectionPoolName()))
                UniversalConnectionPoolManagerImpl.getUniversalConnectionPoolManager()
                        .destroyConnectionPool(ds.getConnectionPoolName());
        }
    }

    private static void query(Connection connection, int round) throws SQLException {
        try (var statement = connection.prepareStatement("""
                SELECT SYS_CONTEXT('USERENV','SESSION_USER'),
                       SYS_CONTEXT('USERENV','AUTHENTICATED_IDENTITY'),
                       SYS_CONTEXT('USERENV','AUTHENTICATION_METHOD'),
                       SYS_CONTEXT('USERENV','SID'), ? FROM dual
                """)) {
            statement.setQueryTimeout(20);
            statement.setInt(1, round);
            try (var rows = statement.executeQuery()) {
                if (!rows.next()) throw new SQLException("Identity query returned no rows");
                System.out.printf("round=%d user=%s identity=%s authentication=%s sid=%s%n",
                        rows.getInt(5), rows.getString(1), rows.getString(2), rows.getString(3), rows.getString(4));
            }
        }
    }
}
ファイル役割
TokenAuthDemo.javaJDBC、ビルダー、UCPの各方式での実行、タスクの並行実行、読み取り専用クエリ、終了処理を行う
OciTokens.javaSDKで使うIDの選択、対象範囲を指定したトークン要求、PoP鍵、キャッシュ付きサプライヤーを扱う
EntraTokens.javaAzure Identityの認証情報を明示的に選択し、データベースの対象範囲、Bearer JWT、キャッシュ付きサプライヤーを扱う
Settings.java環境変数の置換、TCPSとトークン認証モードの検証を行う
configファイル、SDK、OCI、Entraの各方式のプロファイルと、JDBC標準の設定ファイルの例を格納する
sqlIDプロバイダーごとに、管理者が使うセットアップ用テンプレートを格納する
troubleshooting問題の診断方法と、実際の環境で動作を確認する手順をまとめる

mvn verifyでは、設定上のセキュリティ制限、同梱するすべてのプロファイル、UCPの設定、実際にリリースされているOCI/Azureプロバイダーを検出できることを確認します。CIでは、JDK 17と21を使ってテストします。クラウド上のテストを行うには、外部IDを設定する必要があります。設定プロファイルを用意していることと、そのIDを使った認証方法をすべて実際に試していることは、別です。

問題が起きたときの確認方法

症状確認すること
設定チェックに失敗する指定された環境変数をすべて設定してください。EZConnect+形式のTCPS URLを使い、トークンの取得方法は1つに絞ります。このチェックでは、クラウドの認証情報までは検証しません。
プロバイダーが見つからない、またはクラスが見つからないmvn clean verifyを実行し、target/libのJARをすべて含めてください。実行時には元のJAR内のサービス記述子を使います。JARをまとめる際は、サービスリソースを統合せずにshadeしないでください。
ORA-01017トークンのaudienceとscope、外部IDプロバイダー、スキーマの対応付け、クラウドの権限、トークンの期限切れ、認証情報を確認します。このエラーコードだけでは、どこが間違っているかは特定できません。パスワードでの接続時にこのエラーが出ても、トークン認証を試したことにはなりません。
ORA-01045対応先のスキーマにCREATE SESSION権限を付与します。
セットアップ中のORA-20004別のIDプロバイダーがすでに設定されています。使用するプロバイダーに対応する検証用データベースを使ってください。付属スクリプトでは、既存設定を強制的に置き換えません。
タイムアウト、またはORA-125xxDNS、ルーティング、ACL、プライベートエンドポイントへのアクセス、TCPSポート、サービス名を確認します。
TLSまたはウォレットのエラー対象データベースの正しいウォレットを使っているか、CAを信頼しているか、ウォレットのパスと、必要な場合のパスワードが正しいかを確認します。サーバー名の検証は無効にしないでください。
OCIの認可に失敗するAPIプロファイルが正しいか、または実際のワークロードIDを使っているかを確認します。ポリシーの対象OCID、グループまたは動的グループ、トークンの対象範囲、データベースの対応付けが、一致している必要があります。
Entraでスコープが無効、またはクライアントが未認可とされるGraphではなく、データベースリソースのスコープを使います。クライアントIDとリソースIDを区別し、アプリロール、同意、テナントのポリシーを確認してください。
初回は接続できるが、その後、新しい物理接続に失敗するトークンの更新、取得元のID認証の期限切れ、IAMエンドポイントへの接続、CLIで生成したファイルの更新を確認します。既存のプールセッションを再利用している間は、問題に気付かない場合があります。

CLIには、例外の種類とOracle/UCPの数値コードを表示し、例外情報の全内容は表示しません。SDKのログには、IDや要求に関する情報が含まれる場合があるため、詳細な診断情報は、管理された環境内だけで確認してください。実際に有効なトークンを、オンラインのデコーダーに貼り付けないでください。IDを調べるクエリの出力にはプリンシパル名が含まれるため、ログを共有する前に伏せてください。

よくある質問

トークン認証でもウォレットは必要ですか

トークンによる認証と、TLSによる通信の保護は、別に設定します。mTLSを使うデータベースの接続先では、引き続きクライアントウォレットが必要です。対応しているTLSのみの接続先では、JVMがサーバーの証明書チェーンを信頼していれば、クライアントウォレットを使わずに接続できます。どちらの場合も、サーバーのID検証は有効に保ってください。

どのトークン取得方法を選べばよいですか

すでに外部のプロセスでトークンを生成している場合は、ファイル方式を使います。アプリケーションからOCIのトークン取得を直接制御したい場合は、SDKのサプライヤーを使います。トークン取得をアプリケーションのコードに書かず、クラウドの認証方式を明示的に選びたい場合は、プロバイダーのプロファイルを使います。

トークンの期限が切れても、接続プールを使えるのはなぜですか

すでに認証した物理セッションは、そのまま使える場合があるためです。トークンの期限切れが関係するのは、新しい物理接続で認証するときです。このデモのucp-freshモードでは、必ず接続を作り直すようにするため、長時間実行することで、トークン更新の失敗を検出できます。通常どおり接続を繰り返し借り出すだけでは、更新の確認としては不十分です。

同じデータベースに、OCIとEntraのプロファイルを切り替えて使えますか

接続時点で、選択したプロバイダーを使えるようにデータベースを設定している場合に限ります。本記事では、別々の検証用設定を使い、既存のプロバイダーを強制的に置き換えません。どちらの方法でも、クラウド側で認可されたプリンシパルを、CREATE SESSION権限を持つデータベーススキーマに対応付けておく必要があります。