同一GCPプロジェクト内でGAEサービス間をIAPで認証してHTTP通信する

# はじめに
GAE、便利ですよね。
ちゃちゃっとデプロイして簡単に公開できる。
アプリのデプロイバージョン管理や、トラフィック管理、ABテストなんかもできちゃう。

1つのGCPプロジェクト内のGAEで、 `サービス` を複数作成することで、マイクロサービスのような環境設定も可能です。

今回は、そんな感じで複数サービスにまたがるGAEサービス間での認証の話です。

# やりたかったこと
- 複数のGAEサービス間で、サービスAはオープンなWebアプリ、サービスBはクローズドなAPIとし、サービスBはサービスAからしかアクセスされない。
- セキュリティ面を考慮して、サービスBはサービスAのみからアクセスを可能としたい。
- できればアプリ内での認証ではなく、より上位層（GCP）で認証したい

# どうやったか
## GAEにデフォルトで設定されるサービスアカウントで認証できないか
- GAEに1つでもサービスをデプロイすると、自動的に `[PROJECT-ID]@appspot.gserviceaccount.com` というサービスアカウントが作成されます。
- そのアカウントを使って認証できたら楽だなぁと思い、調べてみました。

## Identity-Aware Proxy(IAP) を使えばいいのでは？
- いろいろ調べていると、[Identity-Aware Proxy（IAP)](https://cloud.google.com/iap/docs/concepts-overview?hl=ja) という機能により、GAEのアプリよりも上位層で認証ステップを提供してくれるとのこと。
- 早速やってみよう！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167177/1f2be713-3bd2-690c-f072-64a435babaf5.png)
    - IAPを有効にしたらサービスAもサービスBも両方に適用されて、GAE全体が非公開になってしまった…

## やりたいことは、サービスAはパブリックに公開、サービスBだけにIAPでアクセス制限をしたい
- どうしよう。
    - もしかして、サービス単位に権限を制御できるのでは？
    - サービス単位で権限が割り当てられるなら、片方のサービスだけを公開に出来るはず。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167177/ab0ae3c2-1f68-f1fd-a2e4-bd2b6d9bf1bb.png)
    - できた！
- この設定行うと、少し反映完了までラグがありますが、サービスAにアクセスする際は認証リダイレクトが行われずに、無事にサービスBだけを非公開にできます。

## WebブラウザからのアクセスはGoogleの認証画面にリダイレクトされるけど、サーバーサイドでAPIアクセスするときにどう認証しよう
- さて、これでアクセス制限周りのGCP設定はできましたが、次はサービスAからサービスBのAPIを叩く際に認証が必要になります。
- このままの状態で普通にリクエストを投げると403エラーで認証が失敗すると思います。
- そこで、GAEのサービスアカウントの出番です。
    - サービスアカウントをサービスBの認証済みユーザーとして登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/167177/c9989495-6e2c-ed32-7993-294818ba70b5.png)
    - これで、このサービスアカウントを使うとサービスBに対してアクセス権が与えられます。

## プログラム側でTokenの取得
- GCP上でのアクセス権限を設定したので、実際にHTTPリクエストを送りましょう。
- HTTPリクエスト送信の際、GAEのサービスアカウントのそのリクエストに対してOICDトークンを付加してリクエストするとサービスアカウントでの認証が可能です。
- [こちらのページ](https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_service_account)を参考に、以下のような処理を行います。
    - ここではPythonで記述していますが、[公式ドキュメント](https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_service_account)に他言語でのサンプルもありますので問題ないかと思います。


```python
import requests
from google.auth.transport.requests import Request
from google.oauth2 import id_token

IAP_CLIENT_ID = "IAPの認証情報ページから取得したクライアントID"

def make_open_id_connect_token(client_id=""):
    if not client_id:
        client_id = IAP_CLIENT_ID

    open_id_connect_token = id_token.fetch_id_token(Request(), client_id)
    return open_id_connect_token


token = make_open_id_connect_token()
headers = {
    "Authorization": f"Bearer {token}",
}

res = requests.get(url="https://サービスB/api/", headers=headers)
print(res.content)
```

- `make_open_id_connect_token()` でOICDトークンを取得し、それをリクエストヘッダーに含めてアクセスすることで認証されます。

## さいごに
- これで当初やりたかった、GAEのサービス間でパブリックなWebサービスとクローズドなAPIとの通信が認証付きでできるようになりました。
- ただ、デメリットとして以下が挙げられます。
    - 既に公開・運用されているサービスがある場合、IAPを有効にする際に一時的に全サービスに対してIAPでの認証が追加されてしまうので、ダウンタイムが発生する。
    - allUsersを設定して全ユーザーがIAPをスルーしてアクセスできるようにしても、（おそらく）何かしらの認証ステップは有ると思うので、軽微だと思いますがレスポンスなどに影響がありそう。
    - そのプロジェクトに新たにGAEでパブリックサービスを追加するたびに、allUsersの権限を追加してあげる必要がある。
- これらのデメリットを考慮して、うまく使い分けていくのが良いのかなと思います。

# 参考にさせていただいた記事
- [GAE 2nd-gen でのサービス間認証](https://medium.com/google-cloud-jp/gae-2nd-gen-%E3%81%A7%E3%81%AE%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E9%96%93%E8%AA%8D%E8%A8%BC-1ed1d8b1abce)
    - こちら非常に参考になりました。ありがとうございます。
- [GoogleAppEngineのサービス間通信の認証などについて調べたこと（未解決）](https://qiita.com/aki3061/items/efde95e8a6559cd29a67)