Google Cloud:PHPの標準機能でIAM認証する

Google Cloud:PHPの標準機能でIAM認証する

わけあって IAM 認証の Cloud Run サービスに Google Auth Library for PHP を使わずにアクセスしなくてはいけなくなり、あれこれやってやっとこさ可能になりました。結果、認証について少し理解が進みました。

01IAM 認証手順

まず、手順を Gemini に聞いてみました。

「外部の PHP 環境からライブラリを使わずに PHP の標準機能だけで IAM 認証がかかった Cloud Run サービスへアクセスする手順を教えてください」

準備:サービスアカウントとJSONキーの取得

  1. Cloud Runを呼び出す権限(roles/run.invoker)を持つサービスアカウントを作成。
  2. そのJSON形式の秘密鍵(JSONファイル)をダウンロードし、外部サーバーに配置する。

PHP実装手順

以下の手順で、標準機能だけで署名付きJWT(JSON Web Token)を作成・送信します。

  1. JSONキーの読み込み:file_get_contents() でダウンロードしたキーを読み込む。
  2. JWTの作成:ヘッダー(alg, typ)とペイロード(iss, sub, aud, iat, exp)をJSON化し、Base64Urlエンコードする。
    aud (Audience) にはCloud RunのサービスURL(https://…run.app)を設定する。
  3. JWTの署名:openssl_sign() を使用し、秘密鍵で署名を作成、Base64Urlエンコードしてヘッダーとペイロードの後に結合する。
  4. Googleへトークン要求:curl を使用し、作成したJWTを POST https://googleapis.com に送信し、id_token を取得する。
  5. Cloud Runへのアクセス:取得した id_token を Authorization ヘッダー(Authorization: Bearer )として設定し、curl でターゲットのCloud Run URLを呼び出す。

実装のポイント

  • Base64Urlエンコード:base64_encode() 後の +, /, = を -, _, に置換・削除するカスタム関数が必要。
  • 有効期限(exp):JWTの有効期限は短く(例: 1時間以内)設定する。

と、答えてきました。

ただ、一部これではうまくいきません。JWT(JSON Web Token)を送信するトークン発行エンドポイントが間違っています。尋ね方に問題があるのかはわかりませんが、とにかくうまくいく方法は以下にあります。

02PHP 実装手順

Gemini が教えてくれた PHP 実装手順を順番にやっていきましょう。

まず最初の「準備:サービスアカウントとJSONキーの取得」はすでに過去記事で完了しています。

上の記事の「02. Google Cloud 側の準備」で実際に JSON キーを取得する方法をやっています。現在、Google Cloud では JSON キーファイルの作成が無効になっていますのでその解除の方法も手順を追って書いています。

JSONキーの読み込み

JSON キーは安全管理に注意する必要があります。サイト外に置くか、環境変数で管理する必要があります。

<?php
// JSON キーの読み込み
$keyFile = '/path/to/your-service-account-key.json';
$cloudRunUrl = 'https://your-cloud-run-url-xxxx.a.run.app';
$keyData = json_decode(file_get_contents($keyFile), true);

json_decode では第二引数に true を指定して連想配列に変換します。

JWTの作成

JWT は「ヘッダ.ペイロード.署名」の3つのセクションで構成され、それぞれドットで接続された文字列です。私が下手な説明をするよりも JWT の詳しい解説を読んでいただいたほうがいいかと思います。

// JWT (JSON Web Token) の作成
$now = time();
$header = json_encode(['alg' => 'RS256', 'typ' => 'JWT']);
$payload = json_encode([
    'iss' => $keyData['client_email'], // サービスアカウントのメール
    'sub' => $keyData['client_email'],
    'aud' => 'https://oauth2.googleapis.com/token', // トークン発行エンドポイント
    'iat' => $now,
    'exp' => $now + 3600, // 1時間有効
    'target_audience' => $cloudRunUrl,
]);

ヘッダーで指定するハッシュアルゴリズムは RS256 です。

ペイロードではクレームを指定します。このクレーム(claim)という言葉は日本語の「クレームをつける」の元となった単語ですが、正しくは「主張する」といった意味です(今知ったんだけど(笑)…)。日本ではネガティブな言葉になっちゃっていますので一瞬、え? と思いました。

JWT(JSON Web Token)における「クレーム(Claim)」とは、トークンの保持者(ユーザーやエンティティ)に関する情報を伝える、JSON形式の「名前と値のペア」のことです。ログインID、発行者、有効期限など、ユーザーの属性や権限を記述するデータであり、トークンの「ペイロード」部分に格納されます。
(Gemini)

クレームには登録クレームとカスタムクレームがあり、登録クレームには次の7つの予約クレームがあります。詳しく知りたい方は autho0 Docs をご覧ください。

この内、今回指定するクレームについては次のリンク先に説明があります。

  • aud:オーディエンス/クライアントがアクセスできる API エンドポイント、scope が指定されていない場合にのみ有効です
  • exp:有効期限/トークンの有効期限(Unix エポック時間形式)
  • iat:問題の時間/トークンが発行された時刻(Unix エポック時間形式)
  • iss:発行元/トークンの発行者(サービス アカウント自体)
  • scope:OAuth スコープ/クライアントがアクセスできる API のセットOAuth スコープで識別されますaud が指定されていない場合にのみ有効です
  • sub:サブジェクト/認証されたプリンシパル(サービス アカウント自体)

で、この内の aud について上の Gemini の答えでは「aud (Audience) にはCloud RunのサービスURL(https://…run.app)を設定する」と言ってきますが、そんなわけないんで、実際にやってみても id_token は返ってきません。

じゃあエンドポイントはどこかですが、ダウンロードした JSON 形式の秘密鍵(JSONファイル)に "token_uri": "https://oauth2.googleapis.com/token" と記載されています。これで無事に id_token が返ってきます。

JWT の署名

// Base64エンコード用関数
function base64UrlEncode($data) {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

$encodedHeader = base64UrlEncode($header);
$encodedPayload = base64UrlEncode($payload);

// JWT の署名
$signature = '';
openssl_sign(
    $encodedHeader . '.' . $encodedPayload,
    $signature,
    $keyData['private_key'],
    'SHA256'
);

$jwt = $encodedHeader . '.' . $encodedPayload . '.' . base64UrlEncode($signature);

Google へトークン要求

curl を使って JWT をエンドポイントに送り id_token と交換します。これも Gemini の答えは間違っており、トークンエンドポイントは JWT の aud(オーディエンス) https://oauth2.googleapis.com/token です。

// JWT をエンドポイントに送り id_tokenを取得
$ch = curl_init('https://oauth2.googleapis.com/token');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
    'assertion' => $jwt,
]));

$response = curl_exec($ch);
curl_close($ch);
$tokenData = json_decode($response, true);
$idToken = $tokenData['id_token'];

これで id_token が取得できました。

Cloud Runへのアクセス

後は Authorization ヘッダに id_token を付与して Cloud Run サービスにリクエスト送信するだけです。今回の場合は JSON データを送る必要がありますので次のようになります。

// 送信データ設定
$data = ['data' => '123456'];
$jsonPayload = json_encode($data);

// Cloud Runにリクエスト送信
$ch = curl_init($cloudRunUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonPayload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer ' . $idToken
]);

$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "Status Code: " . $httpCode . "\n";
echo "Response: " . $result . "\n";

これで Google Auth Library for PHP を使わずに Cloud Run のサービスに JSON データを送ることができました。

03まとめ

これまでのコードをまとめますと次のようになります。

<?php
// JSON キーの読み込み
$keyFile = '/path/to/your-service-account-key.json';
$cloudRunUrl = 'https://your-cloud-run-url-xxxx.a.run.app';
$keyData = json_decode(file_get_contents($keyFile), true);

// JWT (JSON Web Token) の作成
$now = time();
$header = json_encode(['alg' => 'RS256', 'typ' => 'JWT']);
$payload = json_encode([
    'iss' => $keyData['client_email'], // サービスアカウントのメール
    'sub' => $keyData['client_email'],
    'aud' => 'https://oauth2.googleapis.com/token', // トークン発行エンドポイント
    'iat' => $now,
    'exp' => $now + 3600, // 1時間有効
    'target_audience' => $cloudRunUrl,
]);

// Base64エンコード用関数
function base64UrlEncode($data) {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

$encodedHeader = base64UrlEncode($header);
$encodedPayload = base64UrlEncode($payload);

// JWT の署名
$signature = '';
openssl_sign(
    $encodedHeader . '.' . $encodedPayload,
    $signature,
    $keyData['private_key'],
    'SHA256'
);

$jwt = $encodedHeader . '.' . $encodedPayload . '.' . base64UrlEncode($signature);

// JWT をエンドポイントに送り id_tokenを取得
$ch = curl_init('https://oauth2.googleapis.com/token');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
    'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
    'assertion' => $jwt,
]));

$response = curl_exec($ch);
curl_close($ch);
$tokenData = json_decode($response, true);
$idToken = $tokenData['id_token'];

// 送信データ設定
$data = ['data' => '123456'];
$jsonPayload = json_encode($data);

// Cloud Runにリクエスト送信
$ch = curl_init($cloudRunUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonPayload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json',
    'Authorization: Bearer ' . $idToken
]);

$result = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "Status Code: " . $httpCode . "\n";
echo "Response: " . $result . "\n";

実際に Cloud Run サービスにアクセスしてみます。

$ php xxxxxxxx.php
Status Code: 200
Response: {"message":"Data received successfully","data":{"data":"123456"}}

Google Auth Library for PHP を使って同じサービスにアクセスした次の記事の結果と同じです。

認証って難しいですね。あれこれ調べて理解するのに随分時間がかかりました。