INFRASTRUCTURE NOTE
「API を有効にしたら、既存のログインにも影響が出るのでは」——
この不安は、プロジェクト・API・クライアント ID・スコープ・同意画面が、それぞれ何を担当しているのかが分かれば消える。
Gmail 監視を nzw.jp に足すときに整理した内容を残す。
| 対象プロジェクト | 番号 331995618405(AI ワークスペース/Gmail 監視で共用) |
|---|---|
| 書いた理由 | Gmail 監視の追加時に「既存の認証が壊れないか」を判断する必要があったため |
| 最終更新 | 2026-10-03 |
| 関連ノート | Google OAuth 同意画面を本番公開する(公開ステータスと審査の実務) |
API の有効化は「蛇口を付ける」だけで、誰にも権限を渡さない。権限を渡すのは OAuth クライアントとスコープの組み合わせであって、そこは用途ごとに完全に独立している。
そして何を許可してもらうかを決めているのは、コンソールの設定ではなくコードに書いたスコープの 1 行。書かない権限は付かない。
ただし 同意画面だけはプロジェクトに 1 枚しかなく、全クライアントで共有される。プロジェクト全体に影響が出るのはここだけ。この一点さえ押さえれば、何を触ると何が動くかが判断できる。
Google Cloud の「プロジェクト」は、課金と権限をまとめる入れ物でしかない。中身は大きく 3 種類に分かれていて、この 3 つが混同されやすい。
プロジェクトには ID(nzw-ai-agent のような文字列)と 番号(331995618405 のような数字)の 2 つの呼び名がある。同じものを指すが、出てくる場所が違う。
331995618405-no02rqsd….apps.googleusercontent.com の前半がそれ…@nzw-ai-agent.iam.gserviceaccount.com や、コンソールの URL に出るつまりクライアント ID を見れば、それがどのプロジェクトのものか一目で分かる。逆にサービスアカウントの鍵ファイルには番号が入っていないので、両者が同じプロジェクトかどうかは鍵だけでは判断できない。
ここが最初に引っかかったところ。API の有効化は、そのプロジェクトからその API を呼べる状態にするだけで、誰かがアクセスできるようになるわけではない。蛇口を付けただけで、水を出す権利は別に配る必要がある。
そのため Gmail API を有効にしても、AI ワークスペースのログインには何の影響もない。ワークスペースのログインは Gmail API を呼んでいないので、蛇口が増えたことにすら気づかない。
クライアント ID はアプリ 1 つにつき 1 枚の身分証で、いくつでも発行できる。重要なのは種別によって使える認証の流れが違うこと。同じプロジェクトの中に用途別に何枚あっても構わないし、互いに干渉しない。
| 種別 | 使う場面 | リダイレクト先 | nzw.jp での用途 |
|---|---|---|---|
| ウェブ アプリケーション | ブラウザから。事前登録したドメインにだけ戻せる | 登録済み URL のみ | AI ワークスペースのログイン |
| デスクトップ アプリ | 手元の CLI から。localhost に戻せる | http://localhost | Gmail 監視のトークン取得 |
| サービスアカウント | サーバー同士。人の同意が要らない | 該当なし | Drive の共有フォルダ読み書き |
Gmail のトークンを取るのにウェブ用ではなくデスクトップ用を新規に作ったのは、この 「戻り先」の違いが理由。
トークン取得スクリプトは手元でローカルサーバーを立てて http://localhost で認可コードを受け取るので、ウェブ用クライアントでは受け取れない。
ここまでの 3 種類はいずれも「クライアント ID + シークレット(またはリダイレクト先)」で身元を証明する。モバイルアプリ向けのクライアントはここに入らない。スマホアプリにシークレットを埋め込んでも抜き出されてしまうため、まったく別の仕組みで身元を確認する。次の 2 節で、その仕組みと、nzw.jp のアプリ(MyDictly)で実際に踏んだ事故を書く。
Firebase は Google が提供するモバイル/Web アプリ向けの開発プラットフォームで、認証(Firebase Auth)・DB(Firestore)・ストレージ・プッシュ通知などをひとまとめに使える。
実体は GCP プロジェクトそのもの。 Firebase でプロジェクトを作ると、同じ番号・同じ ID を持つ GCP プロジェクトが裏で自動的に作られる(逆に、既存の GCP プロジェクトへ Firebase を後から足すこともできる)。Firebase Console は、GCP の設定をモバイル開発者向けに並べ替えた「別の入り口」というのが実態に近い。
Firebase Console で何かを設定すると、裏側では GCP の「API とサービス」側の設定が一緒に書き換わっていることがある。次節の SHA-1 登録はその典型で、Firebase Console の操作だけで完結して見えるが、実際には GCP の「認証情報」ページに Android 向け OAuth クライアントが生成・更新されている。
google-services.json、iOS は GoogleService-Info.plist)ここまでは「アプリとプロジェクトを紐づける」準備で、Android の Google ログインだけはもう 1 段ある。それが次の SHA-1 登録で、ここを飛ばすとビルドは通り、見た目には何も壊れないまま、ログインだけが動かない。
ウェブ・デスクトップ・サービスアカウントは「クライアント ID + シークレット」の組で身元を証明すると書いた。Android 向けクライアントにはシークレットが無い。 代わりにパッケージ名 + 署名証明書の SHA-1 フィンガープリントの組が、そのアプリの身分証になる。
| プラットフォーム | 身元の証明方法 | 署名鍵を変えると |
|---|---|---|
| Android | パッケージ名 + 署名証明書の SHA-1 | 別アプリ扱いになる(新しい SHA-1 の登録が要る) |
| iOS | Bundle ID + URL スキーム(REVERSED_CLIENT_ID) | 無関係(署名証明書は見ていない) |
| Web | Firebase Authentication の「承認済みドメイン」 | 無関係(配信ドメインで識別する) |
同じパッケージ名のアプリでも、署名鍵を変えると SHA-1 が変わり、Google からは「別のアプリ」として扱われる。 debug 鍵でビルドしていたアプリをリリース鍵に切り替えたとき、新しい鍵の SHA-1 を Firebase/GCP に登録し忘れると、Google Sign-In はそこで失敗する。
厄介なのは失敗の出方で、「このアプリは認証できません」という直接のエラーにはならず、ユーザーが何もしていないのに canceled(キャンセルされた)という結果だけが返ってくる。 署名の不一致が原因だとは気づきにくい。
MyDictly(langbridge)で実際に踏んだ。 v1.3.7(2026-07-12)で debug 鍵から共通リリース鍵へ署名を移行した際、Firebase Console への新しい鍵の SHA-1 登録を行っておらず、2026-10-02 まで 3 ヶ月近く Android 版の Google ログインがこの状態のまま気づかれなかった。詳しい経緯は MyDictly の開発ノート の意思決定ログを参照。
keytool -list -v -keystore xxx.jks -alias yyygoogle-services.json を再ダウンロードしてアプリに差し替える(新しい鍵用の oauth_client エントリが増えている)Play Store 経由で配布するアプリは、Play App Signing が使うアップロード鍵とは別に、Google 管理のアプリ署名鍵が実際の配布に使われる。その SHA-1 も忘れずに登録する(Play Console の「アプリの署名」ページで確認できる)。
「何を許可するのか」を決めているのは API でもクライアントでもなくスコープ。そして同意画面に表示されるのは、そのとき要求されたスコープだけ。プロジェクトに登録されている他のスコープは出てこない。
| 区分 | 例 | 審査 | 同意画面 |
|---|---|---|---|
| 基本 | openid / email / profile | 不要 | 警告は出ない |
| 機微 | カレンダー・連絡先など | 必要 | 未確認なら警告 |
| 制限付き | gmail.readonly など | 必要(重い) | 未確認なら警告 |
AI ワークスペースのログインは google.accounts.id という ID トークン専用の仕組みを使っていて、そもそもスコープを一切要求しない。
返ってくるのは「この人は誰か」を示す署名付きの JWT だけ。そのため制限付きスコープを別のクライアントが要求しても、ワークスペース側の同意画面は変わりようがない。
ここが一番の勘所。「何を許可してもらうか」を決めているのはコンソールの設定ではなく、コード側で書いたスコープの文字列。コンソールでの登録は審査のための申告であって、実際の要求内容はアプリが認可のたびに送っている。
コンソールでスコープを登録しても、コードが要求しなければ何も起きない。逆にコードが要求すれば、同意画面にはその分だけ出る。主導権はコードにある。
Gmail 監視では、この 1 行がすべての起点になっている。
SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]
gmail.readonly は読み取り専用を意味する。送信したいなら gmail.send、削除まで許すなら gmail.modify と、別の文字列を書く。
つまり権限の強さは URL の末尾で決まる。ここを gmail.modify に書き換えれば、それだけでメールを消せる権限を要求するアプリになってしまう。
逆に言えば、書かない権限は絶対に付かない。
スクリプトはこの SCOPES を渡してブラウザを開く。ユーザーが見る同意画面の中身は、ここで渡した配列そのもの。
flow = InstalledAppFlow.from_client_config(
{"installed": {"client_id": client_id, "client_secret": client_secret, ...}},
SCOPES, # ← 同意画面に出るのはこれ
)
creds = flow.run_local_server(
port=0,
access_type="offline", # refresh token を貰う(長期間使うため)
prompt="consent select_account",
)
access_type="offline" が「アプリを閉じた後も使い続けたい」という意思表示で、これがあると refresh token が返る。付けないとその場限りのアクセストークンしか貰えず、定期実行できない。
ここから先はブラウザも同意画面も出てこない。サーバーが持っている refresh token だけで動く。
credentials = Credentials(
token=None,
refresh_token=refresh_token, # 手順 2 で貰った長期トークン
token_uri="https://oauth2.googleapis.com/token",
client_id=client_id,
client_secret=client_secret,
scopes=GMAIL_SCOPES,
)
注意したいのは、ここに書いた scopes が権限を増やすわけではないということ。権限が確定したのは手順 2 の同意の瞬間で、この行は「そのとき何を貰ったか」を控えているだけ。
ここを gmail.modify に書き換えても、実際に許可されていなければ API 呼び出しが弾かれる。
同じ理屈で読むと、AI ワークスペース側が Gmail の許可を求めない理由がはっきりする。
google.accounts.id.initialize({
client_id: CLIENT_ID,
callback: handleCredentialResponse,
auto_select: false,
});
scope という項目がどこにもない。書き忘れではなく、google.accounts.id は本人確認専用の API で、スコープを指定するオプションが存在しない。
API アクセスを求めたいときは google.accounts.oauth2 という別の名前空間を使う必要があり、こちらはサイト内で一度も使っていない。
そのためワークスペースのログインで許可画面が出ることは、コードを書き換えない限り起こらない。Gmail API を有効化しようが、別のクライアントが制限付きスコープを要求しようが、この initialize の中身は変わらない。
サーバーは送られてきた ID トークンを Google の公開鍵で検証する。audience は「このトークンは本当にうちのクライアント宛か」の確認。
id_info = google.oauth2.id_token.verify_oauth2_token(
id_token, request_adapter, audience=settings.GOOGLE_CLIENT_ID
)
ここで使う GOOGLE_CLIENT_ID はウェブ用クライアントの ID。Gmail 監視で使うデスクトップ用クライアントとは別物なので、設定も GMAIL_OAUTH_CLIENT_ID という別の変数に分けてある。
1 つの変数に 2 つの役目を持たせると、Gmail 側を直したときにログインの検証対象がずれて壊れる。
ここまで「独立している」と繰り返してきたが、1 つだけプロジェクト全体で共有されるものがある。それが OAuth 同意画面で、クライアントを何枚作っても同意画面は 1 枚しかない。
プロジェクトの設定を触って全クライアントに影響が出るのは、実質ここだけ。逆に言えば、同意画面に手を入れないなら、新しいクライアントを追加してもスコープを増やしても既存のログインは無風で済む。
同意画面が持っている設定のうち、影響が大きいのは公開ステータス(テスト中/本番)。これはプロジェクト単位なので、片方のクライアントのために切り替えると、もう片方も同じ状態になる。
公開ステータスごとの上限・トークン寿命・警告画面の有無、および審査を通すまでの実務は、別ノートに詳しく書いた。
とくに「テスト中」のままだと認可が 7 日で失効する点は、常時動かすバックグラウンド処理には致命的なので、そちらを読んでおくとよい。
DECISION 既存のウェブクライアントを使い回さず、デスクトップ種別を 1 枚追加した。設定も GOOGLE_CLIENT_ID とは別の GMAIL_OAUTH_CLIENT_ID に分離した。
WHY 技術的には http://localhost に戻せないという制約が理由。
ただしそれ以上に、ウェブクライアントの ID はログインの本人確認(audience 検証)に使っている値で、ここを Gmail 用に書き換えると認証の検証対象がずれる。
1 つの値に 2 つの役目を持たせると、片方を直すともう片方が壊れる。
RESULT 用途ごとに独立した。Gmail 側のクライアントを作り直しても、ログインには影響しない。
DECISION Gmail 監視専用の新規プロジェクトは作らず、AI ワークスペースと同じ 331995618405 に同居させた。
WHY 当初は「認証まわりを触りたくないから分けるべきか」と考えた。
だが影響範囲を洗った結果、共有物は同意画面 1 枚だけで、ワークスペース側は基本スコープしか使っておらず、公開ステータスを変えても実際に使える人は変わらない(アプリ側の許可リストが効く)ことが確認できた。
分ける理由が消えた。
RESULT 管理対象のプロジェクトが増えずに済んだ。ただしこの判断は「ワークスペースが基本スコープしか使っていない」という前提に依存している。
将来ワークスペースが機微・制限付きスコープを要求するようになったら、前提が崩れるので分離を再検討する。
| やろうとしていること | 他への影響 |
|---|---|
| API を有効化する | なし。呼べる API が増えるだけ |
| OAuth クライアントを追加する | なし。身分証が 1 枚増えるだけ |
| クライアントのシークレットを再発行する | そのクライアントのみ。他は無関係 |
| サービスアカウントを作る | なし。同意画面を通らない |
| テストユーザーを追加する | プロジェクト全体(ただし増える方向のみ) |
| 公開ステータスを変える | プロジェクト全体。ここだけ要注意 |
| 同意画面のスコープ構成を変える | プロジェクト全体。審査要件が変わる |
結論としては、同意画面に触るとき以外は、過度に警戒しなくていい。逆に同意画面を触るときは、そのプロジェクトにぶら下がっている全アプリを思い出してから手を動かす。