ローカル 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."} です。
認可コードフロー
- 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も指定しない場合に使用されます。
- 起動時 Fixture が適用されるのは、新規データベースまたは
- ブラウザを
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でなければなりません。
- 同意画面で承認または拒否します。 同意画面には、ワークスペース内のすべてのテストユーザー(
Name · email)と、要求されたスコープ文字列が表示されます。テストユーザーを選択し、Approve または Deny をクリックします。- Approve をクリックすると、新しい接続(grant)が作成され、
<redirect_uri>?state=<state>&code=<code>へリダイレクト(302)されます。 - Deny をクリックすると、
<redirect_uri>?state=<state>&error=access_deniedへリダイレクト(302)されます。
- Approve をクリックすると、新しい接続(grant)が作成され、
- コールバックハンドラで
stateを検証します。 クエリパラメータのstateと、リダイレクト前に保存しておいたセッションの値を比較します。error=access_deniedが含まれている場合は処理を中断し、/oauth/tokenは呼び出さないでください。 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が含まれている場合のみ返されます。- アクセストークンを使って 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) |
|---|---|---|---|
| 保留中の同意 nonce | 5 分 | はい | いいえ |
| 認可コード | 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.readonly | calendarList.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.events | events.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.readonly | events.list, events.get, events.instances, events.watch, channels.stop, freebusy.query |
https://www.googleapis.com/auth/calendar.calendarlist | calendarList.list, calendarList.get, calendarList.insert, calendarList.patch, calendarList.update, calendarList.delete, calendarList.watch, channels.stop |
https://www.googleapis.com/auth/calendar.calendarlist.readonly | calendarList.list, calendarList.get, calendarList.watch, channels.stop |
https://www.googleapis.com/auth/calendar.calendars.readonly | calendars.get |
https://www.googleapis.com/auth/calendar.freebusy | freebusy.query |
https://www.googleapis.com/auth/calendar.events.freebusy | freebusy.query |
プロフィールのスコープ
これらのスコープでは GET /userinfo を呼び出せます。レスポンスには常に { sub, id } が含まれ、どちらにもテストユーザー ID が設定されます。
| スコープ | GET /userinfo のレスポンス |
|---|---|
openid | { sub, id } |
email, https://www.googleapis.com/auth/userinfo.email | email、verified_email: true、email_verified: true が加わります |
profile, https://www.googleapis.com/auth/userinfo.profile | name が加わります |
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 機能の一覧については、対応範囲と制限を参照してください。