アプリとつなぐ

ローカル OAuth

Calendarpit のローカル OAuth 2.0 認可コードフロー、トークンエンドポイント、対応スコープについて説明します。

Calendarpit にはローカルの OAuth 2.0 認可サーバーが含まれています。Google Cloud プロジェクトや本物の Google アカウントがなくても、アプリをテストできます。

認可時には、ローカルの同意画面のドロップダウンからテストユーザーを選択します。パスワードの入力は不要です。

フローを開始する前に、アプリの OAuth クライアントと Google Calendar SDK の接続先を http://127.0.0.1:4173 に設定してください。エンドポイントの設定方法は、アプリを接続するを参照してください。

エンドポイント

メソッドパス用途
GET/oauth/authorize認可リクエストを検証し、ローカルの HTML 同意画面を表示します
POST/oauth/authorize同意の選択結果(approve または deny)を送信し、redirect_uri へ 302 でリダイレクトします
POST/oauth/token認可コードまたはリフレッシュトークンをアクセストークンと交換します
POST/oauth/revokeアクセストークンまたはリフレッシュトークンと、その接続(grant)を失効させます
GET/userinfo認可されたテストユーザーのプロフィールクレームを返します
POST/__calendarpit/connections/{grantId}/revoke管理 API から接続を失効させます(204)
  • POST /oauth/authorize は同一オリジンのリクエストのみ受け付けます。クロスサイトリクエストは 403 forbidden を返します。
  • POST /__calendarpit/connections/{grantId}/revoke には、管理 API のリクエスト規則が適用されます。詳細はテスト制御 APIを参照してください。

ルーティングされるのは上記のパスのみです。次のリクエストはステータス 501 を返します。

  • /oauth2/... 配下の Google エンドポイント(/oauth2/v4/token や /oauth2/v3/userinfo など)
  • /oauth/* に対する未対応の HTTP メソッド

501 のレスポンスボディは {"error":"unsupportedFeature","error_description":"Unsupported API endpoint or method."} です。

認可コードフロー

  1. Fixture に OAuth クライアントを登録します。 起動時 Fixture の clients 配列に、id、secret、redirectUris を持つエントリを追加します。各リダイレクト URI は、フラグメントや userinfo を含まない http: または https: の絶対 URL である必要があります。
    • 起動時 Fixture が適用されるのは、新規データベースまたは :memory: セッションのみです。
    • 既存のデータベースファイルを使う場合は、POST /__calendarpit/fixtures で Fixture を読み込むか、新しいデータベースで起動してください。Fixture を読み込むと、次のリセットまでワークスペースのデータが置き換わります。詳細はFixtureとストレージと再起動を参照してください。
    • デフォルトの Fixture には、ID が sample-app、シークレットが local-sample-secret、リダイレクト URI が http://127.0.0.1:4175/callback のクライアントが含まれています。デフォルトの Fixture は、--config も CALENDARPIT_CONFIG も指定しない場合に使用されます。
  2. ブラウザを GET /oauth/authorize へリダイレクトします。 http://127.0.0.1:4173/oauth/authorize に、以下のクエリパラメータを URL エンコードして付与します。
    • client_id(必須): 登録済みのクライアント ID です。未登録の ID は 400 invalid_client を返します。
    • redirect_uri(必須): コールバック URL です。クライアントの redirectUris のいずれかと、文字列として完全一致する必要があります。ループバックポートのワイルドカードや末尾スラッシュの正規化は行いません。一致しない場合は 400 invalid_request を返します。
    • response_type(必須): code を指定します。
    • scope(必須): 対応しているスコープをスペース区切りで指定します。スコープを参照してください。
    • state(任意): ユーザーのブラウザセッションに紐づくランダムな値です。指定した場合、リダイレクト時にそのまま返されます。
    • access_type(任意): online または offline。リフレッシュトークンを受け取るには access_type=offline を指定します。
    • code_challenge および code_challenge_method(任意): PKCE パラメータです。使用する場合、code_challenge_method は S256 にします。code_challenge は 43 文字の base64url ダイジェストにする必要があります。
    • prompt(任意): consent、select_account、consent select_account、select_account consent のいずれかを受け付けます。同意画面は常に表示されます。
    • include_granted_scopes(任意): 指定する場合は false でなければなりません。
  3. 同意画面で承認または拒否します。 同意画面には、ワークスペース内のすべてのテストユーザー(Name · email)と、要求されたスコープ文字列が表示されます。テストユーザーを選択し、Approve または Deny をクリックします。
    • Approve をクリックすると、新しい接続(grant)が作成され、<redirect_uri>?state=<state>&code=<code> へリダイレクト(302)されます。
    • Deny をクリックすると、<redirect_uri>?state=<state>&error=access_denied へリダイレクト(302)されます。
  4. コールバックハンドラで state を検証します。 クエリパラメータの state と、リダイレクト前に保存しておいたセッションの値を比較します。error=access_denied が含まれている場合は処理を中断し、/oauth/token は呼び出さないでください。
  5. POST /oauth/token で認可コードを交換します。 application/x-www-form-urlencoded 形式のボディで送信します。grant_type=authorization_code、code、redirect_uri(ステップ 2 と同一の値)、クライアント認証情報、PKCE を使用した場合は code_verifier を含めます。認証情報についてはクライアント認証を参照してください。エンドポイントは以下の JSON を返します。
    {
      "access_token": "...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "scope": "https://www.googleapis.com/auth/calendar email profile",
      "refresh_token": "..."
    }
    refresh_token は、認可リクエストに access_type=offline が含まれている場合のみ返されます。
  6. アクセストークンを使って API を呼び出します。 /calendar/v3/... や GET /userinfo へのリクエストに Authorization: Bearer <access_token> ヘッダーを付与します。詳細はSDK の使用例を参照してください。

HTTP リクエスト例

この例では、クライアントの値とトークンをシェル変数で渡します。

  • PIT_CLIENT_ID、PIT_CLIENT_SECRET、PIT_REDIRECT_URI: クライアントの値。
  • PIT_CODE: コールバックで受け取った認可コード。
  • PIT_REFRESH_TOKEN: トークン交換で返された refresh_token。
  • CALENDARPIT_ACCESS_TOKEN: トークン交換で返された access_token。

PKCE を使用した場合は、コード交換時に --data-urlencode "code_verifier=$PIT_VERIFIER" も渡してください。

Profile のリクエストには、openid、email、profile、userinfo.* のいずれかのスコープが必要です。

curl --fail-with-body http://127.0.0.1:4173/oauth/token \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode "client_id=$PIT_CLIENT_ID" \
  --data-urlencode "client_secret=$PIT_CLIENT_SECRET" \
  --data-urlencode "redirect_uri=$PIT_REDIRECT_URI" \
  --data-urlencode "code=$PIT_CODE"
curl --fail-with-body http://127.0.0.1:4173/oauth/token \
  --data-urlencode 'grant_type=refresh_token' \
  --data-urlencode "client_id=$PIT_CLIENT_ID" \
  --data-urlencode "client_secret=$PIT_CLIENT_SECRET" \
  --data-urlencode "refresh_token=$PIT_REFRESH_TOKEN"
curl --fail-with-body http://127.0.0.1:4173/userinfo \
  -H "Authorization: Bearer $CALENDARPIT_ACCESS_TOKEN"
curl --fail-with-body http://127.0.0.1:4173/oauth/revoke \
  --data-urlencode "token=$CALENDARPIT_ACCESS_TOKEN"

クライアント認証、有効期限、失効

クライアント認証

POST /oauth/token のすべての呼び出しで、クライアント認証が必須です。authorization_code と refresh_token のどちらも対象で、PKCE を使用している場合も同様です。シークレットを持たないパブリッククライアントには対応していません。

認証情報は次の 2 通りの方法で指定できます。

  • フォームフィールド(client_secret_post): URL エンコードされたボディに client_id と client_secret を含めます。
  • HTTP Basic(client_secret_basic): Authorization: Basic <base64(urlencode(client_id):urlencode(client_secret))> ヘッダーを送信します。

両方を送信した場合は、Authorization ヘッダーが優先されます。

POST /oauth/token は、Content-Type ヘッダーの値にかかわらず、常にボディを application/x-www-form-urlencoded としてパースします。JSON ボディは読み取られません。JSON を送信すると 400 invalid_client で失敗します。認証情報を HTTP Basic で渡している場合は 400 unsupported_grant_type で失敗します。

有効期限と永続化

対象有効期限1 回限り再起動後の保持(SQLite)
保留中の同意 nonce5 分はいいいえ
認可コード60 秒はいいいえ
アクセストークン1 時間(expires_in: 3600)いいえはい(元の有効期限を維持)
リフレッシュトークン有効期限なし(失効、テストユーザーの削除、リセット、または Fixture 読み込みまで有効)いいえはい
接続(grant)失効、テストユーザーの削除、リセット、または Fixture 読み込みまで有効いいえはい
  • 保留中の同意は最大 1,000 件です。超過すると 429 rateLimitExceeded を返します。
  • 認可コードは、その client_id と redirect_uri に紐づきます。
  • :memory: モードでは、プロセスが終了するとすべての OAuth の状態が失われます。詳細はストレージと再起動を参照してください。

リフレッシュ

grant_type=refresh_token は新しい access_token(expires_in: 3600)を発行します。

  • refresh_token の返却やローテーションは行いません。既存のリフレッシュトークンが引き続き有効です。
  • リフレッシュリクエストに scope を含める場合は、認可時のスペース区切りのスコープ文字列と完全一致する必要があります。一致しない場合は 400 invalid_scope で失敗します。

失効

トークンを失効させるには、POST /oauth/revoke を呼び出します。

  • token フォームフィールド、または ?token= クエリパラメータに、アクセストークンまたはリフレッシュトークンを渡します。
  • クライアント認証は不要です。
  • どちらのトークンを失効させても、接続(grant)全体が失効します。その接続のアクセストークン、リフレッシュトークン、認可コードがすべて削除され、watch チャネルが停止します。
  • 未知のトークンを渡した場合は、空のボディで 200 を返します。
  • token が未指定、空、または空白のみの場合は 400 invalidParameter を返します。

次の操作でもトークンが無効になります。

  • 接続の失効(管理画面、POST /__calendarpit/connections/{grantId}/revoke): その接続のトークンが無効化され、watch チャネルが停止します。
  • テストユーザーの削除(DELETE /__calendarpit/users/{id}): そのユーザーのトークンが無効化され、watch チャネルが停止します。
  • リセットまたは Fixture の読み込み(POST /__calendarpit/reset または POST /__calendarpit/fixtures): すべての接続、トークン、認可コード、保留中の同意が削除され、すべての watch チャネルが停止します。

詳細はテスト制御 APIを参照してください。

スコープ

すべての GET /oauth/authorize リクエストには、対応しているスコープを少なくとも 1 つ含める必要があります。それ以外のスコープを要求すると 400 invalid_scope を返します。

Calendar API のスコープ

スコープ許可される操作
https://www.googleapis.com/auth/calendar対応しているすべての Calendar API の読み書き(calendarList.*, calendars.get, events.*, channels.stop, freebusy.query)
https://www.googleapis.com/auth/calendar.readonlycalendarList.list, calendarList.get, calendarList.watch, calendars.get, events.list, events.get, events.instances, events.watch, channels.stop, freebusy.query
https://www.googleapis.com/auth/calendar.eventsevents.list, events.get, events.instances, events.insert, events.update, events.patch, events.delete, events.watch, channels.stop, freebusy.query
https://www.googleapis.com/auth/calendar.events.readonlyevents.list, events.get, events.instances, events.watch, channels.stop, freebusy.query
https://www.googleapis.com/auth/calendar.calendarlistcalendarList.list, calendarList.get, calendarList.insert, calendarList.patch, calendarList.update, calendarList.delete, calendarList.watch, channels.stop
https://www.googleapis.com/auth/calendar.calendarlist.readonlycalendarList.list, calendarList.get, calendarList.watch, channels.stop
https://www.googleapis.com/auth/calendar.calendars.readonlycalendars.get
https://www.googleapis.com/auth/calendar.freebusyfreebusy.query
https://www.googleapis.com/auth/calendar.events.freebusyfreebusy.query

プロフィールのスコープ

これらのスコープでは GET /userinfo を呼び出せます。レスポンスには常に { sub, id } が含まれ、どちらにもテストユーザー ID が設定されます。

スコープGET /userinfo のレスポンス
openid{ sub, id }
email, https://www.googleapis.com/auth/userinfo.emailemail、verified_email: true、email_verified: true が加わります
profile, https://www.googleapis.com/auth/userinfo.profilename が加わります

openid スコープは、{ sub, id } を返す GET /userinfo へのアクセスのみを許可します。Calendarpit は次のことを行いません。

  • /oauth/token から id_token を返す
  • /.well-known/openid-configuration や JWKS を配信する
  • /userinfo のレスポンスに picture、given_name、family_name、locale を含める

トークンとスコープのエラー

条件レスポンス
アクセストークンが未指定、期限切れ、または失効している401(authError)と WWW-Authenticate: Bearer ヘッダー
有効なトークンに、エンドポイントに必要なスコープがない403(insufficientPermissions)
別の接続のトークンで channels.stop を呼び出した404(notFound)

channels.stop には、チャネルを開いた接続(grant)のトークンが必要です。

/oauth/* エンドポイントとは異なり、GET /userinfo と Calendar API エンドポイントは Google API のエラーエンベロープ形式でエラーを返します。詳細はトラブルシューティングを参照してください。

未対応の OAuth 機能

GET /oauth/authorize のバリデーションエラーは、ブラウザに直接返されます。redirect_uri へはリダイレクトしません。レスポンスはステータス 400 の JSON({"error": "<reason>", "error_description": "<message>"})です。

  • 未登録の client_id: 400 invalid_client("Unknown client.")を返します。
  • 未登録の redirect_uri: 400 invalid_request("Unregistered redirect URI.")を返します。クライアントの redirectUris のいずれとも文字列として完全一致しない値が対象です。
  • 未知の認可パラメータ: 400 unsupported_feature を返します。nonce、login_hint、hd、request、request_uri、enable_granular_consent などが該当します。
  • 非対話型の同意: 400 unsupported_feature を返します。prompt=none、またはステップ 2 に記載した 4 つ以外の prompt 値が該当します。
  • 増分認可(Incremental authorization)(include_granted_scopes=true): 400 unsupported_feature を返します。
  • その他のレスポンスタイプ(response_type=code 以外): 400 unsupported_response_type を返します。
  • PKCE の plain メソッド: 400 invalid_request を返します。
  • 一覧にないスコープ: 400 invalid_scope を返します。calendar.events.owned、calendar.app.created、calendar.settings.readonly、calendar.acls、書き込みスコープの calendar.calendars などが該当します。

その他の OAuth の制限:

  • 未対応の grant type: POST /oauth/token は、クライアント認証の成功後に 400 unsupported_grant_type を返します。
  • API キー(key= クエリパラメータ): API リクエストのシステムパラメータとして受け付けますが、Bearer トークン認証の代わりにはなりません。

未対応の Google 機能の一覧については、対応範囲と制限を参照してください。

目次