HTTP Cloud Functions for Firebase で HTTP リクエストの発信元を認証する方法

こんにちは！ 本日より **令和** 時代の幕開けです🎌㊗️

ふだん Golang を主に使っていて Node.js は、はろーわーるど程度の戦闘力しかない @wezardnet です😨
現在は Node.js 以外の言語もサポートされている Cloud Functions ですが、この機会に学習も兼ねて体験してみようと思います。

さて、今年もまた Google Cloud Next ’19 に行くことができなかったので GCPUG の報告会に行ってきましたー🌬

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">酔いどれ GCPUG でお酒飲みながら Google Cloud Next &#39;19 の報告を聴いてますわ 🥴🍺<br>酔いどれだけあって酒の量がハンパない！ <a href="https://twitter.com/hashtag/gcpja?src=hash&amp;ref_src=twsrc%5Etfw">#gcpja</a><br><br>https://gcpug-tokyo.connpas <a href="https://t.co/o7rY9eIDvC">https://t.co/o7rY9eIDvC</a></p>&mdash; wezardnet (@wezardnet) <a href="https://twitter.com/wezardnet/status/1119184344176717829?ref_src=twsrc%5Etfw">April 19, 2019</a></blockquote>


# 0. 前提条件
* Google Cloud Platform(GCP) のプロジェクトが作成されている
* [Google Developer Console](https://console.cloud.google.com/) で Cloud Functions API が有効化されている
* Cloud Functions for Firebase に必要な環境（各種 SDK などもろもろ）が整っている


# 1. Cloud Functions HTTP Trigger
Cloud Functions の HTTP トリガーは URL さえ知っていれば、誰でも実行できてしまいます。しかしながら、特定の発信元だけにトリガーの実行を許可したい場合はちょっと困りますよね。。。😥

実は HTTP リクエストの発信元を認証してトリガー実行を制限する方法は公式で紹介されてますので、まずは [HTTP Cloud Function での認証](https://cloud.google.com/solutions/authentication-in-http-cloud-functions) をお読みください。仕組みとしては Cloud Storage のバケットを承認プロキシに利用するようです。バケットに対して読み取り権限を付与したサービス アカウントを用意し、そのアクセス トークンで認証させる格好になります。

本記事では、この方法を HTTP Cloud Functions for Firebase として実装した場合について書いています。



# 2. サービス アカウントを作成する
 [Google Developer Console](https://console.cloud.google.com/) から IAM & admin の Service accounts を選択します。
サービス アカウントを作る時は、何の目的で使われるモノなのか後で分かるように名前や説明をきちんと付けましょう。今回は「functions_service_account」という名前にし、説明に「Cloud Functions Http Trigger Account」として作りました。

![image02.png](https://qiita-image-store.s3.amazonaws.com/0/12023/86c9ffe0-2d87-6388-3263-fc9e9b4974c1.png)

サービス アカウントが作られたら、ケバブメニューアイコンからキーを作ります。

![image01.png](https://qiita-image-store.s3.amazonaws.com/0/12023/91f63431-0c48-c01c-7cc9-69abaf3ac05b.png)

JSON 形式で認証情報をダウンロードします。ダウンロードした JSON ファイルはアクセス トークンを入手する際に使うので大切に保管しておきましょう。



# 3. Cloud Storage にバケットを作る
[Google Developer Console](https://console.cloud.google.com/) から Cloud Storage を選択し、認証用のバケットを作ります。バケット内は **空** で使用するため、ストレージクラスは気にしなくて良いです💡

![image01.png](https://qiita-image-store.s3.amazonaws.com/0/12023/57e2606b-720a-2d7a-d2ed-644b80da9867.png)

バケットが作られたら、バケットのアクセス権限（パーミッション）に前項 2. で作成したサービス アカウントを追加します。バケットに対する読み取り権限さえあれば良いので `Storage Legacy Bucket Reader` ロールを付与します。

![image01.png](https://qiita-image-store.s3.amazonaws.com/0/12023/ed9a8f02-f542-3886-a5a6-34db24f09a49.png)



# 4. 関数の作成
はじめに以下のコマンドで初期設定を行います。

```terminal
$ firebase init functions
```

基本は公式の [HTTP Cloud Function での認証](https://cloud.google.com/solutions/authentication-in-http-cloud-functions) で紹介されている関数 `secureFunction` をそのまま流用していますが for Firebase ということで少々アレンジを加えてます。また、せっかくなので Express フレームワークを使ってルーティングさせるようにしました。これで各種 API を作ることができますね！

```javascript:./functions/index.js
const functions = require('firebase-functions');
const express = require('express');
const app = express();
const google = require('googleapis').google;
const storage = google.storage('v1');
const BUCKET = '{BUCKET_NAME}';

const getAccessToken = function (header) {
	if (!header) return null;

	const match = header.match(/^Bearer\s+([^\s]+)$/);
	return match ? match[1] : null;
}

app.all('*', (req, res, next) => {
	const accessToken = getAccessToken(req.get('Authorization'));
	// console.log('accessToken = ' + accessToken);

	const oauth = new google.auth.OAuth2();
	oauth.setCredentials({ access_token: accessToken });

	const permission = 'storage.buckets.get';
	storage.buckets.testIamPermissions(
		{ bucket: BUCKET, permissions: [permission], auth: oauth }, {}, (_, response) => {
			// console.log('response = ' + JSON.stringify(response));
			if (typeof response !== 'undefined' 
					&& (response.data && response.data['permissions'] && response.data['permissions'].includes(permission))) {
				return next();
			} else {
				res.status(403).send('The request is forbidden.');
			}
		});
});

app.get('/sample', (req, res) => {
	const data = [
		{ "id": 1, "name": "ねこぽん" },
		{ "id": 2, "name": "うさぽん" },
		{ "id": 5, "name": "ぺんぺん" }
	];
	res.send(JSON.stringify(data));
});

const api = functions.https.onRequest(app);
module.exports = { api };
```

参考までに package.json も載せておきます。

```json:./functions/package.json
{
	"name": "functions",
	"description": "Cloud Functions for Firebase",
	"engines": {"node": "8"},
	"scripts": {
		"lint": "eslint .",
		"serve": "firebase serve --only functions",
		"shell": "firebase functions:shell",
		"start": "npm run shell",
		"deploy": "firebase deploy --only functions",
		"logs": "firebase functions:log"
	},
	"dependencies": {
		"express": "^4.16.4",
		"firebase-admin": "~7.0.0",
		"firebase-functions": "^2.2.0",
		"googleapis": "^39.1.0"
	},
	"devDependencies": {
		"eslint": "^5.12.0",
		"eslint-plugin-promise": "^4.0.1"
	},
	"private": true
}
```

Node.js ではコールバック地獄に陥らないように頭を悩ませますね。



# 5. 関数をデプロイする
次のコマンドを実行してデプロイします。

```terminal
$ firebase deploy --only functions
```

実際に関数がデプロイされてるかどうか [Google Developer Console](https://console.cloud.google.com/) から Cloud Funcrions を選択して確認します。割り当てメモリはデフォルトで 256 MB になるので、あとから自分で 128 MB に変更しましたが `firebase` コマンドではデプロイ時に指定できないのでしょうか？？

![image03.png](https://qiita-image-store.s3.amazonaws.com/0/12023/8adab4d7-df5f-6b9b-e83d-97cc69221d93.png)

[Firebase Console](https://console.firebase.google.com/) の場合は次のように表示されます。

![image02.png](https://qiita-image-store.s3.amazonaws.com/0/12023/bd2b0d3f-7bad-fa37-48fc-20845387ee18.png)



# 6. 動作を検証する
意図したとおりに関数が動作するか検証します。まずはサービス アカウントを使ってアクセス トークンを入手するために、前項 2. でダウンロードした認証情報（JSON）を使ってアクセス トークンを取得します。今回は `curl` で動作検証するので環境変数にトークンを格納しておくと楽ちんです♫


## 6.1. アクセス トークンを環境変数に格納する

`gcloud` コマンドで取得します。

```terminal
$ export ACCESS_TOKEN=$(GOOGLE_APPLICATION_CREDENTIALS=./functions_service_account.json gcloud auth application-default print-access-token)
```

実際にアクセス トークンが環境変数にセットされているか確認します。

```terminal
$ echo $ACCESS_TOKEN
```

## 6.2. アクセス トークン無しでリクエストを投げる
まずは素のまま `curl` を実行してみます。期待どおりの結果（403）が返ってきます😀

```terminal
$ curl -w "\n" https://us-central1-{Project ID}.cloudfunctions.net/api/sample/

The request is forbidden.
```

## 6.3. アクセス トークン有りでリクエストを投げる
次にアクセス トークンを付与して `curl` を実行します。ちゃんと用意した JSON レスポンスが返ってきましたょ😃

```terminal
$ curl -w "\n" https://us-central1-{Project ID}.cloudfunctions.net/api/sample/ -H "Authorization: Bearer "$ACCESS_TOKEN

[{"id":1,"name":"ねこぽん"},{"id":2,"name":"うさぽん"},{"id":5,"name":"ぺんぺん"}]
```


# 7. アクセス トークンの有効期限
公式ドキュメントにも説明されているとおり、アクセス トークンには有効期限があります。本記事の方法でトークンを取得した場合は 1 時間で期限が切れます。ちなみに期限切れのトークンでリクエストを投げたところ 403 で弾かれ正しく挙動できています。

以下のコマンドでアクセス トークンに関する情報を取得することができます。

```terminal
$ curl -w "\n" https://oauth2.googleapis.com/tokeninfo?access_token=$ACCESS_TOKEN

{
  "azp": "106234950138847254616",
  "aud": "106234950138847254616",
  "scope": "https://www.googleapis.com/auth/cloud-platform",
  "exp": "1554774775",
  "expires_in": "3588",
  "access_type": "offline"
}
```

`expires_in` がトークンの有効期限を示します（単位は秒）。


# 〜あとがき〜
昭和、平成に続いて令和という時代の移り変わりと共にテクノロジーや開発手法も日々進化しています。今流行りの技術も、何年も経たないうちに廃れるぐらい目まぐるしく変化している時代ですが、これからも **学び** を忘れず成長していけたらと思います。。。