PythonでGoogleドライブを操作するパッケージPyDrive2の認証まわり

## はじめに

この記事では、PythonでGoogleドライブを操作するパッケージの中から`PyDrive2`（[GitHub](https://github.com/iterative/PyDrive2)）を取り上げます。
GoogleドライブはDrive APIを介して操作できますが、`PyDrive2`におけるDrive APIの認証部分について備忘録としてまとめます。
具体的には以下を扱います。

- 認証情報（OAuth 2.0 クライアント ID）をGCPコンソールで作る方法
- PyDrive2で認証する方法
- 認証情報をローカル端末に保存し、都度ブラウザの認証画面が立ち上がらないようにする方法

## PyDrive2について

PythonでGoogleドライブを操作するパッケージとして`PyDrive`（[GitHub](https://github.com/gsuitedevs/PyDrive)）は多少知られているかもしれません。
残念ながら、PyDriveは1年以上前から更新が止まっています。

PyDriveからPyDrive2がフォークされたのが最近（2020年1月）のようです。
PyDrive2は「Maintained fork of PyDrive.」と謳っています。

PyDrive2では、後方互換性のない変更はまだ取り込まれていないようです（2020年5月時点）。
PyDrive2のドキュメントとして、[PyDriveのドキュメント](https://gsuitedevs.github.io/PyDrive/docs/build/html/index.html)がリンクされているほどです。
また、PyDriveのIssueに蓄積された知見は、PyDrive2でもそのまま適用できる印象です。

## 動作環境

```console
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.6
BuildVersion:	18G3020
$ python -V
Python 3.8.1
$ pip list | grep PyDrive2
PyDrive2                 1.4.10
```

## PyDrive2 使い出し

PyDriveのドキュメントの [Quickstart](https://gsuitedevs.github.io/PyDrive/docs/build/html/quickstart.html) に沿って進めます。

1. Drive APIの操作に必要な認証情報をGCPコンソールで作成
2. 作成した認証情報を使った認証

の順で進めます。

### 1. GCPコンソールから認証情報を作る

GCP (Google Cloud Platform)のアカウントがあり、GCPに1つ以上のプロジェクトを持っている状態を前提とします。
Quickstartに沿って、「OAuthクライアントID」を作っていきます。

1. 有効化していない場合、GCPコンソールからDrive APIを有効化
  - https://console.cloud.google.com/apis/api/drive.googleapis.com/overview にて行う
  - この記事では「Drive-Template-Copy」プロジェクトでDrive APIを有効にし、OAuthクライアントIDを作成しています
2. OAuthクライアントIDを作成（ドキュメントの値を入力）
  - [APIとサービス ダッシュボード](https://console.cloud.google.com/apis/dashboard)からの操作をオススメ
  - 左側のナビゲーションの「認証情報」から作成していく
<img width="1024" alt="1-create_oauth_client_id.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82132/c41b358f-4a89-66bb-0c1d-1268e581f777.png">
→Quickstartにある設定値を入力
<img width="815" alt="2-fill_out_creation_form.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82132/afca2101-07cb-aa3d-a7cd-84c81ae80c14.png">
→作成できました（このポップアップは「OK」をクリックして閉じる）
<img width="509" alt="3-complete_creation.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82132/b88c0ef1-de5e-c21f-569c-a2b071a0583d.png">

3. 作成後、開発端末に認証情報をダウンロード
<img width="1033" alt="4-download_client_id.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82132/52319b57-ace4-3b8e-9900-922eca15e90e.png">
  - 認証情報は作業ディレクトリに移動し、`client_secrets.json`とrenameします

### 2. 動作確認

Quickstartを参考に、以下のコードを動かします。

```python:quickstart.py
from pydrive2.auth import GoogleAuth  # Quickstartからpydriveをpydrive2に読み替えます

gauth = GoogleAuth()
gauth.LocalWebserverAuth()
```

```:ファイル配置
.
├── client_secrets.json
└── quickstart.py
```

`quickstart.py`を実行するとブラウザが起動します。

```console
$ python quickstart.py
Your browser has been opened to visit:

    https://accounts.google.com/o/oauth2/auth?client_id=...
```

<img width="966" alt="5-login_google.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82132/71f1c798-95d8-4582-b2e1-56ef61471692.png">

Googleアカウントを選択してログインすると、
<img width="777" alt="6-authentication_completed.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82132/cbdce202-c632-5afa-dad5-41abf747c304.png">

コマンドラインにも認証成功の旨が表示されています！

```console
$ python quickstart.py
Your browser has been opened to visit:

  :

Authentication successful.
```

## 毎回ブラウザが起動しないようにしたい

PyDriveのドキュメント [OAuth made easy](https://gsuitedevs.github.io/PyDrive/docs/build/html/oauth.html) に進みます。

使い出しではわずか2行のコードでDrive APIを使うための認証が済みました。
とても簡単ですが、このスクリプトを実行するたびに毎回ブラウザからログインする必要があります。
これはスクリプトを使った自動化には向きません。
そこで**認証に使った情報を保存し、以降は自動で更新**するようにPyDrive2を設定していきます。
一度ログインしたあとは、保存した認証情報が更新して使われるので、ブラウザからのログインが不要になります。

上記を実現するために、`settings.yaml` を書きます。
認証情報をファイルに保存する設定は以下のようになります。

```yaml:settings.yaml
# OAuth 2.0 クライアントの認証情報を指定（renameしています）
client_config_file: my_client_secrets.json

# 認証情報をファイルに保存する設定
save_credentials: True
save_credentials_backend: file
save_credentials_file: saved_credentials.json

# 認証情報(credentials)を自動で更新する設定（ブラウザが立ち上がらなくなる）
get_refresh_token: True
```

```:ファイル配置
.
├── my_client_secrets.json
├── quickstart.py
└── settings.yaml
```

コマンドラインで `python quickstart.py` を実行すると、初回はブラウザからログインが必要になります。
<img width="908" alt="7-login_google-again.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82132/41753655-0f44-dda7-de6c-d4f00945ac99.png">

Drive APIを使った操作の範囲（スコープ ※後述）によって警告ページが表示されます。
「詳細を表示」して移動します。
<img width="850" alt="8-unverified_page.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82132/b4da6b0d-96ae-972b-6450-28c9790eb27a.png">

※初めてアクセスする場合は。「Googleドライブのすべてのファイルの表示、編集、作成、削除」を許可するか確認するポップアップが出ました。
ポップアップで許可したあと、以下の画面になります。

<img width="815" alt="9-allow_access_private_data.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82132/0af57872-fa20-8387-e6a9-f1e271ef0fa0.png">

許可するとブラウザに「The authentication flow has completed」と表示されるでしょう。

ブラウザを使った認証は初回のみです。
`get_refresh_token`を有効にしている[^1]ので、2回目以降は自動で認証情報を更新して認証するため、ブラウザは起動しません[^2]。

```:ファイル配置
.
├── my_client_secrets.json
├── quickstart.py
├── saved_credentials.json  # 作成された。以降は自動で更新される
└── settings.yaml
```

[^1]: `get_refresh_token`の値を変更するたびに、初回はブラウザでログインする必要があるようです。保存された認証情報を削除して再実行するのをおすすめします（過去に`saved_credentials.json`がある状態で、`get_refresh_token`を有効化したところ、反映されずにハマりました。「認証で返されるデータの項目が異なるのかな」と思っています）

[^2]: 期間があくと認証情報を自動で更新できないことがあるようです。久しぶりに触ったところ、エラーが発生しました。保存された認証情報（`saved_credentials.json`）を削除して再実行して解決しました。

## 補足事項

### Drive APIのスコープ

`settings.yaml`には`oauth_scope`という項目があります[^3]。
今回は指定していないので、デフォルト値の `[‘https://www.googleapis.com/auth/drive‘]` となります。
[Drive APIのスコープ一覧](https://developers.google.com/drive/api/v2/about-auth#OAuth2Authorizing)によると、これは`Restricted` なスコープです。
Restrictedなスコープを要求するアプリでGoogleの確認プロセスを通過していないアプリは「Unverified app（確認されていないアプリ）」として、先の「このアプリは確認されていません」というページが表示されます[^4]。

https://support.google.com/cloud/answer/7454865 によれば、「OAuth同意画面（OAuth consent screen）」に `https://www.googleapis.com/auth/drive` を追加する必要があります。
しかし、単に追加するだけでは不十分で、Googleのverification（確認）を申請する必要がありました。

PythonでGoogleドライブを操作して実現したいことは、今の段階では、個人的な自動化です。
第三者が使うアプリを開発するわけではないので、承認プロセスには進まず、このまま開発していく考えです[^5]。

[^3]: https://gsuitedevs.github.io/PyDrive/docs/build/html/oauth.html#automatic-and-custom-authentication-with-settings-yaml

[^4]: https://support.google.com/cloud/answer/7454865 より。「An unverified app is an app or Apps Script that requests a sensitive or restricted OAuth scope, but hasn't gone through the Google verification process」

[^5]: 「Apps in development: if your app is experimental or a test build, you don't need to go through verification unless you decide to launch it to the public.」https://support.google.com/cloud/answer/7454865 より

### `LocalWebserverAuth`と`CommandLineAuth`

初回のログインもブラウザが立ち上がらないようにするために、`LocalWebserverAuth`を`CommandLineAuth`に変えたところ、うまくいきませんでした。

ブラウザに表示された「承認エラー」から、OAuth クライアントIDを*ウェブアプリケーションとして作っている*ので、`CommandLineAuth`には対応していないようです。

<img width="961" alt="could_not_use_commandlineauth.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82132/dc0047a0-f79e-2474-8858-82da778a296e.png">

そのため、初回のブラウザでのログインは必須と考えています[^6]。

[^6]: ブラウザが起動できない実行環境では、保存した認証情報を複製して配置しています（ブラウザが立ち上がらないので問題なく実行できています）

### settings.yamlとclient_secrets.json

settings.yamlとclient_secrets.jsonが、quickstart.pyと同じディレクトリにある場合、settings.yamlが読み込まれるようです。
気になったので、`auth.py`の`GoogleAuth`の[実装](https://github.com/iterative/PyDrive2/blob/d2065b42a4c6cc39fb6666a278b361d2c59ab8c2/pydrive2/auth.py#L142)を見てみました。

- `GoogleAuth()`という呼び出しで、`"settings.yaml"`（デフォルト値）が指定されたことになる
	- settings.yamlをrenameした場合は、`GoogleAuth`の引数に渡せばよさそう
- `GoogleAuth()`を実行する中で、settings.yamlが読み込まれる
	- settings.yamlに指定された`client_config_file`が使われる
	- settings.yamlに指定がない場合は、デフォルト値として `"client_secrets.json"` が使われる

この記事では

- settings.yamlを作る前はclient_secrets.jsonが使われ
- settings.yamlを作ったあとは、そこに指定された`client_config_file`が使われて

いますね。