Bubble.io で Outlook(Microsoft 365 / 個人 Microsoft アカウント)と連携しようとすると、テスト用の自分のアカウントでは動くのに、別の Outlook アカウントでは AADSTS70000 / invalid_grant が発生してトークンが取得できない――という相談はとても多いです。本記事では、実際の事例をベースに、原因の切り分けから具体的な設定例、再発防止のコツまで詳しく解説します。
Bubble.io×Outlook 連携で発生した AADSTS70000 / invalid_grant エラーの状況
まずは今回の典型的な状況を整理します。
- Bubble.io で作った Web アプリから、Outlook(Microsoft 365 / 個人 Microsoft アカウント)に接続したい
- アプリ登録者(開発者本人)の Outlook アカウントではトークン取得が成功している
- 別のユーザーの Outlook(個人 Microsoft アカウント)でサインインすると、認可コードまでは返ってくる
- しかし、認可コードをトークンに交換しようとすると HTTP 400 /
invalid_grant - エラーメッセージは
AADSTS70000: The provided value for the 'code' parameter is not valid. The code has expired.
見た目は「コードが期限切れ」と書かれていますが、実際には 認可コードの有効期限だけが原因とは限らないのが厄介なポイントです。今回のケースでは最終的に、スコープ(API 権限)の不足とアプリ登録設定の不整合を解消することで、別アカウントでも正常にトークンを発行できるようになりました。
AADSTS70000 / invalid_grant とは?エラーの意味を整理する
Azure AD / Microsoft ID プラットフォームで invalid_grant が返るのは、ざっくり言うと「渡された認可コード・リフレッシュトークン・リダイレクト URI などに問題がある」ときです。特に認可コードをトークンに交換する処理では、次のような原因が代表的です。
| 想定される原因 | 典型的なメッセージ例 | 主なチェックポイント |
|---|---|---|
| 認可コードが期限切れ | The code has expired. | 取得から数分以上経っていないか、同じコードを再利用していないか |
| リダイレクト URI 不一致 | redirect_uri ... does not match 等 | 認可リクエストとトークンリクエストで URI が完全一致しているか |
| PKCE の不整合 | invalid_grant のみで詳細が出ないことも | code_challenge と code_verifier を正しく対応させているか |
| スコープ不足/同意不足 | 裏側で弾かれ、結果として invalid_grant | アプリ登録に必要スコープがあり、ユーザーが同意しているか |
| アカウント種別のミスマッチ | 個人アカウントで利用できない設定 | 「任意の組織+個人アカウント」が許可されているか |
つまり AADSTS70000 が出た瞬間に「コードが古いだけ」と決めつけるのは危険で、Bubble.io と Azure AD(Microsoft Entra ID)の両方の設定を丁寧に確認する必要があります。
なぜ「アプリ所有者のアカウントだけ」正常で、他の Outlook アカウントで失敗するのか
今回の事例で特徴的なのは、アプリ登録を行った所有者アカウントでは成功するのに、別の Outlook アカウントでは失敗するという点です。この現象が起きる典型的な理由は次のとおりです。
- アプリ登録に十分な Microsoft Graph のアクセス許可が設定されているのは「自分のテナント側だけ」
- 別テナント・個人アカウントから見ると、そのアプリは必要なスコープに対して同意が済んでいない
- 結果として、認可コード発行はされるものの、トークン交換時に内部的な整合性エラーとなり
invalid_grantを返す
このように、テスト環境では開発者自身のアカウントで問題なく動いているために、設定不備に気づきにくいのが AADSTS70000 の厄介なところです。
よくある原因を一つずつ Bubble.io 目線で検証する
認可コードの有効期限と「1回しか使えない」ルール
まず真っ先に疑うべきは、エラーメッセージどおりの「認可コードの期限切れ/使い回し」です。
- 認可コードは数分程度の短い寿命しかありません
- さらに、1 回トークンに交換した時点で無効になります(二重使用不可)
- コピーして別のツールに貼り付けるような運用をしていると、手作業の間に失効しがちです
Bubble.io の本番運用では、ブラウザでサインイン→自動リダイレクト→バックエンドで即座にトークン交換、という流れにすることで、認可コードの寿命を気にせずに済みます。デバッグのために一度だけ手作業でコードをコピーするのは構いませんが、運用フローとして手動コピーに依存しない設計にしておくのがベストです。
リダイレクト URI の完全一致チェック
次に頻出するのが redirect_uri の不一致です。Azure 側で登録したリダイレクト URI と、Bubble.io が実際に送っている URI が 1 文字でも違うと、認可コードは返ってもトークン交換でエラーになることがあります。
| 項目 | チェック内容 |
|---|---|
| 末尾のスラッシュ | /oauth_redirect と /oauth_redirect/ は別物として扱われる |
| プロトコル | http:// と https:// の違いを確認 |
| サブドメイン | version-test 付きと本番 URL を取り違えていないか |
| URL エンコード | 認可リクエストで redirect_uri を URL エンコードしているか |
Bubble.io のデフォルトのリダイレクト URI は次のような形式です(テスト環境の例)。
https://<アプリ名>.bubbleapps.io/version-test/api/1.1/oauth_redirect
本番環境では version-test が外れるので、Azure ポータル側にはテスト用と本番用をそれぞれ追加登録しておくと安心です。
スコープ不足・同意不足が招く「見かけ上の code 期限切れ」
今回の事例で最も重要だったのが、この スコープ不足/同意不足です。
Outlook のメール送信や予定表操作を行う場合、Microsoft Graph の代表的な委任された権限(Delegated permissions)は以下のとおりです。
| スコープ名 | 用途 | 備考 |
|---|---|---|
User.Read | サインインしたユーザーのプロフィール取得 | ほぼ必須スコープ |
Mail.Send | ユーザーとしてメール送信 | Outlook 連携の中心 |
Calendars.Read | 予定表の読み取り | 閲覧のみでよければこれ |
Calendars.ReadWrite | 予定表の読み書き | 予定の追加・更新・削除を行う場合 |
offline_access | リフレッシュトークンの取得 | ユーザー不在時の再認証なし更新に必須 |
これらを アプリ登録の「API のアクセス許可」画面に追加しておくこと、さらに ユーザー本人が同意画面で許可していることの両方が必要です。
今回のように「開発者自身のアカウントだけ成功する」場合、多くは次のような構図になっています。
- 開発者はテナント管理者/アプリ所有者として、すでに広範な権限を持っている
- そのため、多少スコープ設定が甘くても内部的にカバーされてしまうことがある
- 別の個人アカウントで試すと、要求スコープとアプリ登録、実際の同意状態に矛盾が生まれる
- 結果として認可コードは出るが、トークン交換時に
invalid_grantで弾かれる
このギャップを埋めるには、後述するようにアプリ登録に必要スコープをすべて明示追加し、改めてユーザーに同意を取ることが最重要です。
サポートするアカウント種別(組織のみ/個人も可)の設定ミス
Outlook 連携では、個人の Microsoft アカウント(@outlook.com / @hotmail.com など)を使うケースがよくあります。このとき、Azure ポータルのアプリ登録で次の項目を確認してください。
- この組織ディレクトリ内のアカウントのみ
- 任意の組織ディレクトリ内のアカウント(マルチテナント)
- 任意の組織ディレクトリ内のアカウントと個人の Microsoft アカウント
個人アカウント(MSA)でサインインさせたいなら、必ず三つ目の「任意の組織ディレクトリ+個人の Microsoft アカウント」を選ぶ必要があります。これを誤って「この組織のみ」にしていると、Bubble.io からは一見サインインできたように見えても、裏側でトークン発行に失敗し、invalid_grant につながることがあります。
v1 / v2 エンドポイント・PKCE の不整合
Microsoft の OAuth エンドポイントには v1 と v2 があり、さらに PKCE を使うかどうかでパラメータが変わります。特に Bubble.io のような SaaS から Outlook 連携する場合、v2.0 エンドポイント+認可コードフロー+必要に応じて PKCEという構成が基本です。
| 用途 | 推奨 URL | 備考 |
|---|---|---|
| 認可リクエスト | https://login.microsoftonline.com/common/oauth2/v2.0/authorize | /common で組織+個人をまとめて扱う |
| トークンリクエスト | https://login.microsoftonline.com/common/oauth2/v2.0/token | Content-Type は application/x-www-form-urlencoded |
もし PKCE を使う場合は、
- 認可リクエストに
code_challengeとcode_challenge_method=S256 - トークンリクエストに
code_verifier
を正しく含める必要があります。これらの値の対応関係が崩れても invalid_grant が返ってくるため、ログに両方の値を一時的に出力して検証するとよいでしょう。
HTTP リクエスト形式の誤り(JSON 送信など)
最後に見逃されがちなのが、トークンエンドポイントへのリクエスト形式です。Microsoft のトークンエンドポイントは、必ず application/x-www-form-urlencoded 形式の POST ボディで送る必要があります。
Content-Type: application/jsonで JSON を送るとエラーになります- Bubble.io の API Connector では「Body type: Form-data」等の設定で送るのが安全です
Bubble 側の設定で誤って JSON を選んでいると、ヘッダーだけ OAuth っぽく見えて実際には invalid_grant となることがあります。
実際に有効だった解決策:スコープ追加と設定の見直し
今回のケースでは、次の対応を行うことで、別の Outlook アカウントでも正常にアクセストークンとリフレッシュトークンを取得できるようになりました。
- Azure ポータルのアプリ登録に、必要な Microsoft Graph の委任された権限を追加
- サポートするアカウント種別を「任意の組織+個人アカウント」に変更
- Bubble.io で v2.0 認可コードフロー(必要に応じて PKCE)を正しく設定
- ユーザーに再度サインイン・同意を行ってもらい、認可コードを即座にトークンに交換
特に重要なのは、次のスコープを漏れなく指定することです。
User.ReadMail.SendCalendars.ReadまたはCalendars.ReadWriteoffline_access
offline_access を入れ忘れると、リフレッシュトークンが返ってこないため、ユーザーが画面を閉じたあとにトークンを更新できなくなります。Bubble アプリで「一度接続したらしばらくは再ログイン不要」にしたい場合は、必須と考えてください。
Bubble.io 側の実装ポイント(OAuth2 設定例)
続いて Bubble.io の設定側の要点を整理します。ここでは、Bubble の API Connector あるいは OAuth2 プラグインを利用して Microsoft アカウントと連携する一般的なパターンを想定します。
リダイレクト URI を Azure に登録する
まず、Bubble が表示する OAuth 用のリダイレクト URL を Azure のアプリ登録に登録します。
- テスト環境の例:
https://<アプリ名>.bubbleapps.io/version-test/api/1.1/oauth_redirect - 本番環境の例:
https://<アプリ名>.bubbleapps.io/api/1.1/oauth_redirect
Azure ポータルの「認証」画面から、上記の URL を 正確にコピペして追加します。末尾のスラッシュ有無なども含めて、Bubble 側の表示と完全一致していることを必ず確認してください。
認可エンドポイントの設定例
Bubble の OAuth 設定画面で、認可エンドポイントを次のように指定します(改行は見やすさのため)。
https://login.microsoftonline.com/common/oauth2/v2.0/authorize
?client_id=<CLIENT_ID>
&response_type=code
&redirect_uri=<URL エンコード済みのリダイレクト URI>
&response_mode=query
&scope=User.Read%20Mail.Send%20Calendars.Read%20Calendars.ReadWrite%20offline_access
[&code_challenge=<PKCE 用>&code_challenge_method=S256]
Bubble の画面では、ベース URL とパラメータを別々に指定する形になることが多いので、
- ベース URL:
https://login.microsoftonline.com/common/oauth2/v2.0/authorize - Query パラメータとして
client_id,response_type,redirect_uri,scopeなどを設定
という形で登録していきます。
トークンエンドポイントの設定例
トークンエンドポイントは次のようなリクエストになります。
POST https://login.microsoftonline.com/common/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
client_id=<CLIENT_ID>
[client_secret=<CLIENT_SECRET>] ← 機密クライアントなら必須
grant_type=authorization_code
code=<認可コード>
redirect_uri=<認可時と完全一致の URI>
scope=User.Read Mail.Send Calendars.Read Calendars.ReadWrite offline_access
[code_verifier=<PKCE 用>] ← 公開クライアントなら必須
Bubble の API Connector では、
- Method:POST
- Body type:Form-data(または x-www-form-urlencoded 相当)
- 各パラメータをキー・値として登録
という形で設定します。Body type を JSON にしないことが非常に重要です。
Bubble プラグイン利用時にハマりやすいポイント
Bubble 純正の OAuth2 プラグインやサードパーティプラグインを使う場合、内部で PKCE やリダイレクト処理をよしなにやってくれる一方で、次のような点でつまずきがちです。
- プラグイン側が想定しているスコープが最小限で、Outlook 用の
Mail.SendやCalendars.ReadWriteが足りない - テスト環境と本番環境でリダイレクト URI が変わるのに、Azure 登録が片方しかない
- クライアントシークレットが必須なプラグインなのに、Azure 側を「パブリッククライアント」として作っている
プラグインの挙動に悩んだら、一度 API Connector で素の OAuth 2.0 フローを組み立ててみると、どこで invalid_grant が出ているのかを切り分けやすくなります。
PKCE を使う場合の注意点と確認方法
最近のブラウザベースアプリでは、セキュリティの観点から PKCE(Proof Key for Code Exchange) の利用が推奨されています。PKCE を使う場合、実装側のミスで invalid_grant を引き起こしやすいポイントがあるので整理しておきます。
code_verifierは認可リクエストごとにランダム生成し、トークン交換まで保持しておく必要があるcode_challengeはcode_verifierからS256(SHA256 + Base64URL)で計算する- 認可リクエストとトークンリクエストで、別々の
code_verifierを使ってはいけない
動作確認のコツとしては、
- 一時的にログに
code_verifierとcode_challengeを出力する - オンラインツールやローカルスクリプトで、
code_verifierからcode_challengeを再計算して一致するか検証 - 認可リクエストとトークンリクエストで同一の
code_verifierを使っているか確認
という手順で、PKCE 周りの不整合を徹底的に潰していくとよいでしょう。
すぐに確認したいチェックリスト(Bubble / Azure 共通)
ここまでの内容を踏まえ、AADSTS70000 / invalid_grant が出たときに最初に確認したいポイントをチェックリスト形式でまとめます。
| 観点 | チェック内容 | OK 条件 |
|---|---|---|
| リダイレクト URI | Bubble が使っている URI と Azure の登録が完全一致しているか | 末尾スラッシュ・プロトコル・サブドメインまで一致 |
| アカウント種別 | 個人 Microsoft アカウントを許可する設定になっているか | 「任意の組織ディレクトリ+個人アカウント」を選択 |
| スコープ | User.Read, Mail.Send, Calendars.Read(Write), offline_access を設定しているか | アプリ登録の API 権限にも、Bubble の scope パラメータにも含まれている |
| エンドポイント | v2.0 の /authorize と /token を使っているか | .../oauth2/v2.0/authorize / .../oauth2/v2.0/token になっている |
| リクエスト形式 | トークンリクエストが x-www-form-urlencoded で送信されているか | ヘッダーに Content-Type: application/x-www-form-urlencoded |
| PKCE | 利用する場合、code_challenge / code_verifier が正しく対応しているか | 同じ code_verifier から計算した code_challenge を利用 |
| 認可コードの寿命 | 取得から数分以内にトークン交換しているか | 自動リダイレクトで即座にトークン API を叩いている |
再発防止のための設計と運用のベストプラクティス
一度 AADSTS70000 を解消しても、設定変更や環境移行のタイミングで再発することがあります。ここでは、Bubble.io×Outlook 連携で安定運用するためのベストプラクティスをまとめます。
アカウント種別とスコープ設計を最初に固める
- 「社内テナントだけで使うのか」「一般ユーザー(個人アカウント)にも開放するのか」を最初に決める
- それに応じて「この組織のみ」か「任意の組織+個人」を選択する
- 利用シナリオ(メール送信だけ・予定表も操作・連絡先も扱う等)を洗い出して、必要スコープを一覧化しておく
Bubble の環境(開発/本番)ごとにリダイレクト URL を明示管理
- テスト用と本番用のリダイレクト URI を別々に Azure に登録
- 環境ごとにどの CLIENT_ID / CLIENT_SECRET を使うかを表にしておく
| 環境 | リダイレクト URI | Client ID | 備考 |
|---|---|---|---|
| 開発(version-test) | https://<app>.bubbleapps.io/version-test/api/1.1/oauth_redirect | DEV_CLIENT_ID | 開発者向けテスト用 |
| 本番 | https://<app>.bubbleapps.io/api/1.1/oauth_redirect | PROD_CLIENT_ID | 一般ユーザー向け |
認可コードを人間が触らないフローにする
本番に近づくほど、認可コードを人間がコピー&ペーストする場面は廃止しましょう。
- ユーザーがサインインしたら、Bubble が自動でリダイレクト URI を受け取る
- 認可コードはデータベースには保存せず、すぐにトークン交換に使う
- 保存するのはアクセストークンとリフレッシュトークン(必要に応じて暗号化)だけ
こうすることで、「コードが期限切れ」「コードを二重に使ってしまった」というヒューマンエラーを根本的に防げます。
ログ・HAR ファイルで差分を定期的に確認する
特に Bubble のようなノーコード/ローコード環境では、画面操作中にうっかり設定を変えてしまうことがあります。定期的に、
- ブラウザの開発者ツールでネットワークログ(HAR)を保存し、成功時と失敗時の差を比較する
scope,redirect_uri,grant_type,code_verifierなどの違いを見る
という習慣をつけておくと、AADSTS70000 が再発したときにも素早く原因にたどり着けます。
まとめ:AADSTS70000 / invalid_grant を「設定のシグナル」として活用する
Bubble.io から Outlook(Microsoft 365 / 個人 Microsoft アカウント)に連携する際に発生する AADSTS70000 / invalid_grant エラーは、一見すると「認可コードが期限切れ」としか読み取れません。しかし実際には、
- 認可コードの短い寿命・1 回限りの仕様
- リダイレクト URI の 1 文字の違い
- スコープ不足・同意不足
- アカウント種別のミスマッチ
- v1 / v2 エンドポイントや PKCE の不整合
- HTTP リクエスト形式の誤り
といった さまざまな設定ミスの「シグナル」として現れることが多いエラーです。
今回紹介したように、
- 必要な Graph スコープ(
User.Read,Mail.Send,Calendars.Read(Write),offline_access)をアプリ登録に追加する - 「任意の組織+個人アカウント」をサポートする設定にする
- Bubble で v2.0 認可コードフロー(必要に応じて PKCE)を正しく実装する
- 認可コードを即座にトークンに交換する自動フローにする
といった対策を取ることで、アプリ所有者以外の Outlook アカウントでも安定してトークンを取得できるようになります。
もし今まさに AADSTS70000 / invalid_grant に悩まされている場合は、本記事のチェックリストと設定例をそのまま写経してみてください。それだけで、Bubble.io と Outlook の OAuth2 連携が一気に安定し、ユーザーにとっても開発者にとってもストレスの少ないアプリに近づくはずです。

コメント