Cloud Runのサービス間認証

# 概要
Cloud Runを例にアクセスに認証情報が必要な非公開サービスと通信するための手順をまとめる。
# 参照ドキュメント（Cloud Run）
⑴クイックスタート: ビルドとデプロイ
https://cloud.google.com/run/docs/quickstarts/build-and-deploy/ 
⑵認証の概要
https://cloud.google.com/run/docs/authenticating/overview
⑶サービス間認証
https://cloud.google.com/run/docs/authenticating/service-to-service

# 全体像
![Screen Shot 2021-08-16 at 14.34.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1553280/c7aa87d2-7c51-ff2c-3c8d-05958aeb7619.png)

# 手順
1. アプリケーションを Cloud Run にデプロイ
1. サービスアカウントの設定
1. IDトークンの取得
1. テスト

# 1.アプリケーションをデプロイ
まず、呼び出し側のアプリケーションを作成してコンテナ イメージにパッケージ化し、コンテナ イメージを Container Registry にアップロードして Cloud Run にデプロイする。[*⑴](https://cloud.google.com/run/docs/quickstarts/build-and-deploy/ )

# 2. サービスアカウントの設定
呼び出し側サービスからのリクエストを受け入れるようにするためには、呼び出し元のサービスアカウントを受信側サービスのメンバーにする必要がある。[*⑵](https://cloud.google.com/run/docs/authenticating/overview)

今回は、呼び出し元のサービスアカウントに Cloud Run 起動元（roles/run.invoker）のロールを付与する。[*⑶](https://cloud.google.com/run/docs/authenticating/service-to-service#acquire-token)

# 3. IDトークンの取得
Google 署名付きIDトークンを取得して、リクエストにセットする方法はドキュメントによると以下になる。
 >・受信側のサービスの URL に設定されたオーディエンスクレーム（aud）を使用して、Google 署名付き ID トークンを取得します。
（ 現在、カスタム ドメインは aud 値でサポートされていません。）
>・受信側サービスに対するリクエストの Authorization: Bearer ID_TOKEN ヘッダーに ID トークンを含めます。

オーディエンスクレーム（aud）ってなんぞや？が分からない。。。（分かる人コメントいただけると助かります）
受信側のサービスURLだということは分かる。

とりあえず受信側サービスのURLをもとにIDトークンを取得し、リクエストの Authorization: Bearer ID_TOKEN ヘッダーにセットすればいい。[*⑶](https://cloud.google.com/run/docs/authenticating/service-to-service#acquire-token)


以下のサンプルコードをもとに呼び出し側のサービスを実装。

* Javaのサンプルコード（GitHub）
https://github.com/GoogleCloudPlatform/java-docs-samples/tree/702077dcfe619cc3764a213f253beaefcdb04b43/run/authentication

```authentication/src/main/java/com/example/cloudrun/Authentication.java
package com.example.cloudrun;

import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.IdTokenCredentials;
import com.google.auth.oauth2.IdTokenProvider;
import java.io.IOException;

public class Authentication {

  // makeGetRequest makes a GET request to the specified Cloud Run or
  // Cloud Functions endpoint, serviceUrl (must be a complete URL), by
  // authenticating with an Id token retrieved from Application Default Credentials.
  public static HttpResponse makeGetRequest(String serviceUrl) throws IOException {
    GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
    if (!(credentials instanceof IdTokenProvider)) {
      throw new IllegalArgumentException("Credentials are not an instance of IdTokenProvider.");
    }
    IdTokenCredentials tokenCredential =
        IdTokenCredentials.newBuilder()
            .setIdTokenProvider((IdTokenProvider) credentials)
            .setTargetAudience(serviceUrl)
            .build();

    GenericUrl genericUrl = new GenericUrl(serviceUrl);
    HttpCredentialsAdapter adapter = new HttpCredentialsAdapter(tokenCredential);
    HttpTransport transport = new NetHttpTransport();
    HttpRequest request = transport.createRequestFactory(adapter).buildGetRequest(genericUrl);
    return request.execute();
  }
}
```

* Goのサンプルコード（GitHub）
https://github.com/GoogleCloudPlatform/golang-samples/tree/2a6361b94cd77803b9a581422bdd827814576975/functions/security

```security/idtoken.go
package security

import (
	"context"
	"fmt"
	"io"

	"google.golang.org/api/idtoken"
)

// makeGetRequest makes a request to the provided targetURL with an authenticated client.
func makeGetRequest(w io.Writer, targetURL string) error {
	// functionURL := "https://TARGET_URL"
	ctx := context.Background()

	// client is a http.Client that automatically adds an "Authorization" header
	// to any requests made.
	client, err := idtoken.NewClient(ctx, targetURL)
	if err != nil {
		return fmt.Errorf("idtoken.NewClient: %v", err)
	}

	resp, err := client.Get(targetURL)
	if err != nil {
		return fmt.Errorf("client.Get: %v", err)
	}
	defer resp.Body.Close()
	if _, err := io.Copy(w, resp.Body); err != nil {
		return fmt.Errorf("io.Copy: %v", err)
	}

	return nil
}
```

# 4. テスト
以下の手順にそってテスト開始
1. サービスアカウントの認証情報の取得（呼び出し側サービスがGoogle Cloudの外部である場合）
参照：https://cloud.google.com/docs/authentication/production
1. サービス間認証を行わなかった時に、アクセスができないことを確認
1. サービス間認証を行った時に、アクセスができることを確認

以下のコードはJavaでのテストコード
（受信側であるCloud RunのサービスURLをAuthentication.makeGetRequestの引数として渡して、レスポンスが返ってこれば、アクセスできたことが確認できる）

```authentication/src/test/java/com/example/cloudrun/AuthenticationTest.java
package com.example.cloudrun;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.core.StringContains.containsString;

import com.google.api.client.http.HttpResponse;
import java.io.IOException;
import org.junit.Test;

public class AuthenticationTest {

  @Test
  public void canMakeGetRequest() throws IOException {
    String url = "https://helloworld-gqnrt6tdna-an.a.run.app"; // Cloud RunのサービスURL
    HttpResponse response = Authentication.makeGetRequest(url);
    // "Hello World!"と返すだけのアプリケーションを受信側のサービスとした場合、
    assertThat(response.parseAsString(), containsString("Hello World!"));
    assertThat(response.getContentType(), containsString("text/html"));
    assertThat(response.getStatusCode(), equalTo(200));
  }

  @Test
  public void failsMakeGetRequestWithoutProtocol() throws IOException {
    String url = "helloworld-gqnrt6tdna-an.a.run.app/";
    try {
      Authentication.makeGetRequest(url);
    } catch (IllegalArgumentException e) {
      assertThat(e.getMessage(), containsString("no protocol"));
    }
  }
}
```