Cloud Run functions × TypeScript 開発をしよう！【発展編：Cloud Run functions を認証付きで呼び出す】

# はじめに
この記事では以下のことについて説明しています。
- Cloud Run functions を認証付きで呼び出す方法
- Terraform で認証付きのCloud Run functions を構成する

---
以前の記事にて「公開」APIに対してAPIキーで認証を付与する方法について紹介しました。今回は Cloud Run functions に認証を付与して、呼び出し元を限定する方法について紹介します。


# なぜこれをするのか？
実は今まで作ってきた ESPv2 Cloud Run サービス 経由で Cloud Run functions を呼び出す方法ですが、ESPv2 Cloud Run サービスのエンドポイントにはAPIキー認証が付与されていますが、Cloud Run functions のエンドポイントに直接アクセスしてもデータが取得できてしまいます。

![espv2-serverless-cloud-functionsのコピー3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374872/e47e500c-39ea-5c56-42fe-4699c152757b.png)


# Cloud Run functions を認証付きで呼び出すには？
ここまでの記事で全員が呼び出すことのできる Cloud Run functions の構成方法について紹介してきましたが、呼び出しに認証を付与することもできます。


![スクリーンショット 2024-12-23 12.29.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374872/bf5a56bc-d93f-a6e1-4865-a8b881605730.png)

認証のある関数は、

1. その関数を呼び出すサービスアカウント、もしくはユーザアカウントが
1. 関数を呼び出す権限を持っていて
1. 関数を呼び出すときに ID トークンを提供する

これを満たすと呼び出せます。
と言うことで一回認証付きの関数リソースを作成しましょう。

# Cloud Run functions を認証付きに修正する
```tf: Cloud Run functions のIAM binding 定義を修正
resource "google_cloud_run_v2_service_iam_member" "member" {
  project  = local.project
  name     = google_cloudfunctions2_function.default.name
  location = google_cloudfunctions2_function.default.location
  role     = "roles/run.invoker"
  # member   = "allUsers"
  # 認証付きの関数を呼び出せるサービスアカウントを限定
  member   = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com"
}
```

こうすることで、ESPv2 Cloud Run サービス経由で Cloud Run functions にアクセスすることはできますが、Cloud Run functions のエンドポイントには直接アクセスできないようになりました。

![espv2-serverless-cloud-functionsのコピー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374872/a46f2726-9de4-cf72-dbbc-8c3d28b51b3c.png)

> 引用：https://cloud.google.com/endpoints/docs/openapi/set-up-cloud-functions-espv2

試しに Cloud Run functions のエンドポイントを直接呼び出してみます。

```bash
$ curl https://xxx.a.run.app

<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>403 Forbidden</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Forbidden</h1>
<h2>Your client does not have permission to get URL <code>/</code> from this server.</h2>
<h2></h2>
</body></html>
```

きちんと403エラーが発生していますね。

# 認証つきの関数を呼び出す方法
以下の三つの方法があります。
1. 認証付き関数を呼び出すことのできるIAMを呼び出し元のサービスアカウントに付与する(紹介済み)
1. Authorization ヘッダーにIDトークンを含めてリクエストする
1. Google Auth クライアント ライブラリ を使用して呼び出す

ここでは後半の2つの方法について説明します。

（注意：ローカルで認証付きエンドポイント自体を立ち上げる方法はありません。認証付き関数をデプロイしてそれを呼び出すようにしましょう。）

### Authorization ヘッダーにIDトークンを含めてリクエストする
以下を実行します。注意としては、**gcloud CLIを実行するアカウントが Cloud Run functions の呼び出しロール（`roles/run.invoker`）を持つ必要があります。**
```bash
curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" \
[Cloud Run functions のエンドポイント]
```

### Google Auth クライアント ライブラリを利用して呼び出す
以下の手順を踏襲したコードを書きます。
1. audience フィールド（aud）に受信側関数の URL を設定して
1. Google によって署名された ID トークンを作成し
1. 関数に対するリクエストの Authorization: Bearer ID_TOKEN ヘッダーに ID トークンを含めて呼び出す

これを実現する上でGoogleの認証クライアントライブラリが必要です。Node.js用のライブラリは以下になります。

https://github.com/googleapis/google-auth-library-nodejs

```bash
npm install google-auth-library
```

実際に呼び出すコード `auth-test.ts` を作成します。プロジェクトルートに配置します。
呼び出すURLと `targetAudience` は必ず同じにしましょう。

```ts: auth-test.ts
import { GoogleAuth } from 'google-auth-library';

const auth = new GoogleAuth();

const targetAudience = `https://${process.env.REGION}-${process.env.PROJECT}.cloudfunctions.net/${process.env.auth}`;
const url = targetAudience; // Cloud Run functions では url = targetAudience にする

export const authRequest = async () => {
  console.info(`request ${url} with target audience ${targetAudience}`);
  const client = await auth.getIdTokenClient(targetAudience);
  try {
    const res = await client.request({url});
    console.info(res.data);
  } catch (e: unknown) {
    console.error(e);
    process.exitCode = 1;
  }
}

await authRequest();
```

ローカルでの環境変数は、別途環境変数ファイル `.env.yml` を読み込むことで設定/取得します。

```yml: .env.yml
REGION=リージョン
PROJECT=プロジェクト名
AUTH_FUNC=関数名
```

この環境変数をnpmスクリプト起動時に読み込んでもらえるようにします。

```bash
npm install --save-dev env-cmd
```

```js: package.json
  "scripts": {
    "auth-test": "env-cmd -f ./.env.yml tsx auth-test.ts",
  },
```

実行してみましょう。

```bash
$ npm run auth-test

> advent-calendar-2024@1.0.0 auth-test
> env-cmd -f ./.env.yml tsx ./src/auth.ts

request https://xxx.cloudfunctions.net/sample-crf with target audience https://xxx.cloudfunctions.net/sample-crf
Hello, World!
```

呼び出しに成功しましたね。環境変数ファイルはきっちり `.gitignore` に記載しておくのを忘れずに。

# 終わりに
認証ありの Cloud Run functions の呼び出し方とその必要性について説明しました。結論、認証はつけとこうぜって言う話です。

# 参考
### 認証付き Cloud Run functions を呼び出す Colabo チュートリアル
https://codelabs.developers.google.com/codelabs/how-to-invoke-authenticated-cloud-function?hl=ja#0

### ローカルで認証付き Cloud Run functions 関数を呼ぶ方法

https://cloud.google.com/functions/docs/securing/authenticating?hl=ja#example_1_authenticate_developer_testing