2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「Googleでログイン」を押したあと、Googleはサービスにパスワードを渡していない。じゃあ何を渡しているのか?

2
Posted at

はじめに

「Googleでログイン」は毎日押しているのに、押したあとに何が飛んでいるかを説明できる人は意外と少ない。この記事では、あの1クリックの裏側を、Googleのパスワードがサービスに渡らない理由から、サービスが受け取る「2種類の券」の違いまで順に整理します。

執筆時点は2026年9月です。OAuth 2.1 はまだIETFのドラフト段階(draft-ietf-oauth-v2-1)で、確定した仕様ではありません。本文ではその点を明記して扱います。

対象読者
「Googleでログイン」を使う側としては慣れているが、作る側の仕組みを説明できない人。これから実装する人。OAuthとOpenID Connectの違いを一度で腹落ちさせたい人。読了目安は10分ほどです。

この記事で追うのは、サーバーを持つWebサービスがGoogleをIDプロバイダとして使うOpenID Connectの認可コードフローです。Googleが提供している埋め込みボタン(Google Identity Services)の「認証だけ」のフローでは、IDトークンがブラウザやバックエンドに直接返り、アクセストークンは登場しません。ボタンを1つ置くだけの実装をしている人は、この記事の「認可コード」と「アクセストークン」の部分が省略されている、と読み替えてください。仕組みの骨格は同じです。

忙しい人向けに結論を先に置きます。

問い 答え
Googleはサービスにパスワードを渡す? 渡さない。パスワードやパスキーはGoogleの画面にだけ入力される
じゃあ何を渡す? まず使い捨ての「認可コード」。サービスが裏側でそれを「IDトークン」と「アクセストークン」に交換する
ログインの根拠になるのはどっち? IDトークン(OpenID Connect)。アクセストークンはGoogleのAPIを呼ぶための券で、ログインの証明には使えない
OAuthって認証の仕組みでしょ? 違う。OAuth 2.0は「権限を委譲する」仕組み。ログインに使えるようにしたのがOpenID Connect

参考文献

仕様と公式ドキュメントを先に置きます。本文の記述はここで確認したものです。

「Googleでログイン」で、Googleはパスワードを渡していない

まず一番大事な事実です。あなたがGoogleのパスワードを入力する画面は、サービスの画面ではなくGoogleの画面です。ボタンを押した瞬間にブラウザは accounts.google.com へ移動していて、サービス側はその入力を一切見ていません。

実際に見てみましょう。Googleが公開している OAuth 2.0 Playground でスコープに openid email profile を入れて「Authorize APIs」を押すと、ブラウザは次のURLへ飛びます。

https://accounts.google.com/o/oauth2/v2/auth
  ?client_id=407408718192.apps.googleusercontent.com
  &redirect_uri=https://developers.google.com/oauthplayground
  &response_type=code
  &scope=openid+email+profile
  &access_type=offline
  &prompt=consent

飛んだ先がこの画面です。ヘッダーに「Google でログイン」、本文に「Google OAuth 2.0 Playground に移動」と書かれています。ここはGoogleのドメインで、Playground(=サービス)はこの画面の中身を見ることができません。

なお、Playgroundが組み立てるこのURLには後述するPKCEの code_challenge が載っていません。自分で実装するときは code_challengecode_challenge_method=S256 を足すのが今の標準です。

google-signin-oauth-playground.png

URLに載っている項目が、この後の話の登場人物になります。

パラメータ 意味
client_id どのサービスからの依頼か。Google Cloud Consoleで発行したもの
redirect_uri ログイン後にGoogleがユーザーを戻す先。事前登録した値と一致しないと弾かれる
response_type=code 「トークンではなく認可コードを返してください」という指定
scope 何を求めているか。openid が入るとOpenID Connectのフローになる
access_type=offline 後述するリフレッシュトークンも欲しい、という指定

登場人物は3人だけ

仕様書の用語で言うと、あなたが「リソースオーナー」、サービスが「クライアント」、Googleが「認可サーバー」です。名前が堅いので、この記事では「あなた・サービス・Google」で通します。

大事なのは、サービスとGoogleの間に「あなたのパスワードを共有する」関係が存在しないことです。かつては「Gmailの連絡先を取り込むために、Gmailのパスワードをそのサービスに入力する」という設計が普通にありました。パスワードを渡した相手は、連絡先だけでなくメールの全削除もできてしまう。この「全部か、ゼロか」を解消するために生まれたのがOAuthで、渡すものをパスワードから「範囲を限定した券」に変えたのが本質です。

押した直後に何が飛んでいるか

流れを1枚にしました。赤い矢印だけがブラウザを通らないサーバー間通信です。

google-login-three-parties.png

順に追います。

なぜトークンを直接ブラウザに返さず、わざわざ認可コードを経由するのか。ブラウザに返すとURLの履歴、ページ内のスクリプト、拡張機能、オープンリダイレクトなどからトークンが漏れる経路が増えるからです。認可コードは短命で、しかも交換にはクライアント認証(client_secret)に加えて、後述するPKCEの検証値 code_verifier が必要なので、コードだけ盗んでもトークンにできません。

トークン交換のリクエストは、サービスのサーバーからこう投げます。

token-exchange.sh
curl -s -X POST https://oauth2.googleapis.com/token \
  -d code=4/0AX4XfWh...(認可コード) \
  -d client_id=YOUR_CLIENT_ID.apps.googleusercontent.com \
  -d client_secret=YOUR_CLIENT_SECRET \
  -d redirect_uri=https://example.com/auth/callback \
  -d code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk \
  -d grant_type=authorization_code

code_verifier は、最初のリクエストに載せた code_challenge の元になったランダム値です(PKCEの節で説明します)。

返ってくるのはこういうJSONです。

{
  "access_token": "ya29.a0AfB_byC...",
  "expires_in": 3599,
  "refresh_token": "1//0gA1b2C3d...",
  "scope": "openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile",
  "token_type": "Bearer",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6..."
}

id_tokenaccess_token が並んで入っています。ここからが、私がつまずいた本題です。

手に入るのは2種類の券

id-token-vs-access-token.png

IDトークンは「この人は誰か」を証明する券で、宛先はサービス自身です。中身は署名付きのJWTで、base64urlを外せば読めます。

decode_id_token.py
import base64, json, sys

id_token = sys.argv[1]
payload = id_token.split(".")[1]
payload += "=" * (-len(payload) % 4)
print(json.dumps(json.loads(base64.urlsafe_b64decode(payload)), indent=2, ensure_ascii=False))

中身はこんな形です(値は例です)。

{
  "iss": "https://accounts.google.com",
  "azp": "407408718192.apps.googleusercontent.com",
  "aud": "407408718192.apps.googleusercontent.com",
  "sub": "110169484474386276334",
  "email": "someone@gmail.com",
  "email_verified": true,
  "name": "Someone",
  "iat": 1789193200,
  "exp": 1789196800
}

一方のアクセストークンは「何をしてよいか」の券で、宛先はGoogleのAPIです。サービスから見ると中身は不透明な文字列で、Authorization: Bearer ヘッダーに付けてカレンダーやDriveのAPIを呼ぶために使います。

同じレスポンスに入っているので混ぜたくなるのですが、宛先が違う券です。IDトークンはサービスが読むもの、アクセストークンはGoogleが読むもの。この区別がないまま実装すると、次の節の誤用にまっすぐ突っ込みます。

「〇〇があなたのメールアドレスへのアクセスを求めています」の正体

同意画面に出てくる文言は、scope をそのまま人間語に翻訳したものです。openid email profile なら「メールアドレスと基本的なプロフィール」だけが表示され、https://www.googleapis.com/auth/calendar.readonly を足すと「カレンダーの予定の表示」が1行増えます。

つまりあの画面は、サービスが「これだけ欲しい」と申告した範囲を、Googleがあなたに見せて確認を取っている場面です。サービスが勝手に範囲を広げることはできず、広げたければもう一度同意画面を通す必要があります。「全部か、ゼロか」だったパスワード共有と比べて、ここが構造的に違います。

なぜOAuthは「ログインの仕組み」ではないのか

ここが私の最初の誤解でした。OAuth 2.0(RFC 6749)は認可、つまり「あなたの代わりにこの範囲の操作をしてよい」という権限委譲の枠組みで、「この人が誰か」を証明する仕組みは含まれていません。

それでも昔は、アクセストークンでGoogleの userinfo エンドポイントを叩き、返ってきたメールアドレスで「ログイン成功」とする実装が広く行われていました。問題は、アクセストークンの形式や、それを「自分のクライアント向けに発行されたものか」検証する方法が、OAuth 2.0では規定されていないことです。悪意ある別のアプリが正規に取得したアクセストークンを、あなたのサービスに横流しすれば、あなたのサービスはそれを見分けられず、そのユーザーとしてログインさせてしまう。これが「OAuthをログインに使ってはいけない」と言われる理由です。

この穴を埋めるために、OAuth 2.0 の上に薄く乗せられたのが OpenID Connect(OIDC)です。追加されたのは、乱暴に言えば次の3つだけです。

  • scopeopenid を含めると、トークン交換のレスポンスに id_token が加わる
  • IDトークンには aud(宛先のclient_id)と iss(発行者)が入り、署名で改ざんを検知できる
  • サービスは署名・issaudexp を検証してから、sub をキーにユーザーを引き当てる

aud があるので、他のアプリ向けに発行されたIDトークンを横流しされても、あなたのサービスは「宛先が自分ではない」と弾けます。Googleの公式ドキュメントも、ユーザーの識別子には email ではなく sub を使うよう明記しています。メールアドレスは変更できるが、sub はそのGoogleアカウントに対して不変だからです。

検証は自分で書かず、Googleの公式ライブラリに任せるのが正攻法です。google-authrequests を入れておきます。

python3 -m pip install --upgrade google-auth requests
verify_id_token.py
from google.oauth2 import id_token
from google.auth.transport import requests

CLIENT_ID = "YOUR_CLIENT_ID.apps.googleusercontent.com"

def verify(token: str) -> dict:
    # 署名・iss・aud・exp をまとめて検証する。失敗すると ValueError
    claims = id_token.verify_oauth2_token(token, requests.Request(), CLIENT_ID)
    return claims  # claims["sub"] をユーザーのキーにする

昔の危ないやり方と今の標準

OAuth 2.0 が2012年に出た頃は、シングルページアプリ向けに「インプリシットフロー」という、認可コードを経由せずトークンを直接ブラウザへ返す方式がありました。URLのフラグメントにアクセストークンが載るので、履歴・リファラ・拡張機能から漏れやすい。現在ドラフト中の OAuth 2.1 では、この方式は仕様から削除されています。

代わりに標準になったのが、認可コードフロー+PKCEです。PKCEは、サービスがフロー開始時にランダムな code_verifier を作り、そのハッシュ code_challenge を最初のリクエストに載せ、トークン交換時に元の code_verifier を送る仕組みです。認可コードを途中で盗まれても、code_verifier を知らない攻撃者は交換できません。client_secret を持てるサーバー型のサービスでも、PKCEは client_secret の代わりではなく上乗せです。OAuth 2.1 のドラフトでは、原則すべてのクライアントでPKCEが必須になっています(OIDCの nonce を正しく使う機密クライアントに限った例外はあります)。

もう1つ、state パラメータも忘れがちです。サービスがランダム値を作ってリクエストに載せ、戻ってきた値と一致することを確認する。これが無いと、攻撃者が用意した認可コードをあなたに踏ませて、攻撃者のGoogleアカウントであなたをログインさせるCSRFが成立しうる。PKCEやOIDCの nonce をブラウザのセッションに結び付けて同じ役割を持たせることもできますが、Googleのドキュメントでは state を使う手順が最初に書かれています。

パスキーとの関係

前にパスキーの記事を書いたとき、「サービス側がパスキー対応するのは大変では」というコメントをもらいました。実は「Googleでログイン」を使っているサービスは、何もしなくてもパスキーの恩恵を受けています。

先ほどのシーケンス図の③、Googleの画面でのログインは、Googleアカウント側の設定次第でパスワードでもパスキーでも行われます。サービスから見ると「認可コードが戻ってくる」までは同じで、あなたがGoogle側でパスキーを使った回は、その認証ステップがフィッシング耐性を持つことになります。ただし恩恵はそこまでです。Googleに既存のセッションがあればパスキーを使わずに通ることもあるし、サービス自身が発行したセッションCookieを守るのはサービスの仕事のままです。それでも、認証の難しい部分をGoogleに委ねる、という設計の効き目が一番わかりやすく出る場面だと思っています。

まとめ

  • 「Googleでログイン」で、パスワードやパスキーはGoogleの画面にだけ入力される。サービスには一度も渡らない
  • サービスが受け取るのは使い捨ての認可コードで、それを裏側でIDトークンとアクセストークンに交換する
  • IDトークンは「誰か」を証明する券でサービス宛て、アクセストークンは「何ができるか」の券でGoogleのAPI宛て。ログインの根拠にできるのはIDトークンだけ
  • OAuth 2.0 は権限委譲の枠組みで、ログインに使えるようにしたのがOpenID Connect。aud の検証と sub による識別がその中核
  • 認可コードフロー+PKCE+state が現在の標準。インプリシットフローは OAuth 2.1 ドラフトで削除された
  • この記事はサーバー型サービスの認可コードフローの話。Google Identity Servicesの認証専用フローではIDトークンだけが返る

「Googleでログイン」を押すたびに、裏で7往復くらいの会話が起きているのが見えるようになると、同意画面の1行1行が急に意味を持って読めるようになります。次に押すとき、URLバーを一瞬だけ見てみてください。accounts.google.com に居ることが、この記事の全部です。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?