Google APIからGoogle Workspaceのユーザーデータにアクセスする最適な認証方法

## はじめに

本記事は[モチベーションクラウドシリーズ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/mcs)の21日目になります。

リンクアンドモチベーションでソフトウェアエンジニアをしているnakagam3です。
最近業務の中でGoogle APIを利用する機会があったのですが、知識不足が故にかGoogle APIの認証周りを調べるのになかなか骨が折れました。今回は備忘も兼ねて調べたことをまとめてみます。

## Google APIの認証方式が３種類ある

Google APIには現在３種類の認証方式が存在するそうです。

### 1. APIキー方式

事前に発行しておいたAPIキーという文字列を、APIリクエストに含めて送信することで認証する方式です。
手軽な方式ではありますが、今回利用したかったAPIは対応していませんでした。
これはAPIキー方式が他２つに比べてセキュアではなく、Google Maps Platform APIなど一般公開されているデータにアクセスする一部のAPIにしか適していないためだそうです。
ということで却下。

### 2. OAuthクライアントID方式

皆さんがお持ちのGoogleアカウントを使って認証します。その際にアカウント所有者本人に同意を求め、同意されたらアクセスを許可する、という方式です。
OAuth2.0で定義されている認可コードフローにあたります。

<img width=800 alt="認可コードフロー" src="https://camo.qiitausercontent.com/cbceb0f0e391aeeb9220c484838d0c13e730c75d/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3130363034342f64393131396632312d373336642d643565642d393634642d3330363861663066636465392e706e67">

[※OAuth 2.0 全フローの図解と動画 から引用](https://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f#1-%E8%AA%8D%E5%8F%AF%E3%82%B3%E3%83%BC%E3%83%89%E3%83%95%E3%83%AD%E3%83%BC)

ユーザーアカウントを使って認証できるため試しやすく、またユーザー認証が入るので非常にセキュアというメリットもあります。この方式で実現できなくもなかったのですが、今後の運用面を考えると以下2点がネックとなったため別の方式を検討することにしました。
1. 初回認証時にアクセス要求に同意する画面操作が必要になる
1. 認証後もaccess token/refresh tokenの管理が必要になり、有効期限が切れた場合に1のオペレーションが再度必要になる

### 3. サービスアカウント方式

サービスアカウントという専用のアカウントとして認証することで、ユーザーへの認証は行わずアプリが直接サーバーにアクセスを要求する方式です。
OAuth2.0で定義されている**クライアント・クレデンシャルズフロー**にあたります。
<img width=800 alt="クライアント・クレデンシャルズフロー" src="https://camo.qiitausercontent.com/db046530a661d77bf16e6202697cb655d3cd3d9d/68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f3130363034342f36303332343938302d313365612d323133382d326638652d3635653938306366636362332e706e67">

[※OAuth 2.0 全フローの図解と動画 から引用](https://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f#4-%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E3%82%AF%E3%83%AC%E3%83%87%E3%83%B3%E3%82%B7%E3%83%A3%E3%83%AB%E3%82%BA%E3%83%95%E3%83%AD%E3%83%BC)

ユーザーへの認証が無いため、バッチ処理のようにサーバー間の通信のみで完結する場合にのみ推奨されるようです。そのため認証情報はより厳密な管理が必要となりますが、今回のケースではこちらが適しているようですので、こちらを採用することにしました。

### 設定方法

公式の手順を参考に発行できました。
> [Google Workspace 管理者ヘルプ - サービス アカウントの作成](https://support.google.com/a/answer/7378726?hl=ja)

## Google Workspaceの制約

サービスアカウント方式を採用した場合、通常ならサービスアカウントに対して「ファイルを共有して」データ取得・更新をするのが標準的な使い方なのですが、今回のケースではGoogle Workspace内に存在するファイルを更新する必要がありました。

皆さんのGoogle Workspaceには「ドメイン外のユーザーへの共有制限」が設定されていることが多いかと思います。サービスアカウントのメールアドレスは`<サービスアカウント名>@<プロジェクト名>.iam.gserviceaccount.com`のようなドメインになるので、この制約によってファイルを共有することができません。

この問題を解決する手段として見つけたのが「ドメイン全体の委任」という設定でした。

### ドメイン全体の委任

この設定は、Google Workspace管理者から「ドメイン内のユーザーに代わって」ユーザーデータにアクセスすることを許可する、というものです。

既に管理者から許可されているため、ドメイン内のユーザーデータにアクセスする際ユーザーの同意などは不要になります。また、許可する際にスコープを指定できるため、許可された範囲外のデータにはアクセスできないようにもできます。

私はAWSで言うAssume Roleを許可する設定だと認識しました。Assume RoleはClassmethodさんの記事が分かりやすい。
> [IAM ロールの PassRole と AssumeRole をもう二度と忘れないために絵を描いてみた](https://dev.classmethod.jp/articles/iam-role-passrole-assumerole/)

### 設定方法

実際に設定するにはGoogle Workspaceの管理者しか実施できないため私はやっていませんが、公式ヘルプに手順が載っていました。
> [Google Workspace 管理者ヘルプ - API アクセスをドメイン全体の委任で制御する](https://support.google.com/a/answer/162106?hl=ja)

## Ruby on Railsからサービスアカウント方式で認証する
ここまでで認証アカウントの準備ができたので、実際に認証できるか試してみましょう！

gemはこちらの[google-api-ruby-client](https://github.com/googleapis/google-api-ruby-client)を利用しました。下記コマンドでインストールできます。

```bash
gem install google-api-client
```

認証に必要な情報は以下のコードで生成できます。
サービスアカウントを作った時にsecret.jsonがダウンロードできると思いますが、その中から必要な情報を環境変数にセットすることで読み込むこともできます。環境変数を使う方がシークレットをコードと分離できるため管理しやすいでしょう。
```ruby
# 以下の環境変数から認証情報を取得して認証する
# "GOOGLE_CLIENT_ID",
# "GOOGLE_CLIENT_EMAIL",
# "GOOGLE_ACCOUNT_TYPE",
# "GOOGLE_PRIVATE_KEY"
credentials = Google::Auth::ServiceAccountCredentials.make_creds(scope: [
    Google::Apis::DriveV3::AUTH_DRIVE,
])
```

また、subフィールドに特定のアカウントを指定することで、指定したアカウントとしてAPI実行ができます。ここでGoogle Workspaceのユーザーを指定する時に「ドメイン全体の委任」が効いてくるんですね。
```ruby
delegated_account = "hoge@hoge.com"
credentials.update!(sub: delegated_account)
```

生成したcredentialsは各サービスのauthorizationにセットすれば通信の際に認証されます。
```ruby
drives_service = Google::Apis::DriveV3::DriveService.new
drives_service.authorization = credentials
```

下記メソッドを実行すると、マイドライブの情報が取得できる＝認証通った！ということなので完成です。
```ruby
drives_service.get_about()
```

## おわりに
公式のドキュメントなどあるのはあるのですが、この結論に辿り着くまでに時間がかかってしまいました。
ですが認証関係は安全にデータを扱うためにとても重要な知識です。
今後も良いプロダクトを作っていくために正しく理解できるようにしていきたいですね。

## 参考
- [OAuth 2.0 全フローの図解と動画](https://qiita.com/TakahikoKawasaki/items/200951e5b5929f840a1f)
- [IAM ロールの PassRole と AssumeRole をもう二度と忘れないために絵を描いてみた](https://dev.classmethod.jp/articles/iam-role-passrole-assumerole/)
- [Google Workspace 管理者ヘルプ - サービス アカウントの作成](https://support.google.com/a/answer/7378726?hl=ja)
- [Google Workspace 管理者ヘルプ - API アクセスをドメイン全体の委任で制御する](https://support.google.com/a/answer/162106?hl=ja)