Teams タブでサインインさせたのに親タブへ戻らず、「Something went wrong」「Failed to complete authentication: The authentication window was closed.」と表示される――この現象は、実装の“数ミリ”のズレで起きがちです。本記事は Teams JavaScript SDK v2 を前提に、原因の切り分けと最短の修正手順、落とし穴と実運用のベストプラクティスを、コピペ可能なコードとチェックリストで詳解します。
症状と前提条件
Teams のタブ アプリで microsoftTeams.authentication.authenticate()(以下 authenticate)を呼び出したところ、認証ポップアップは開くものの、サインイン完了後に親タブへ制御が返らず、次のいずれかが表示される/失敗コールバックに渡されることがあります。
- Something went wrong
- Failed to complete authentication: The authentication window was closed.
- CancelledByUser / Timeout などのメッセージ
想定環境は、Teams タブ、Teams JavaScript SDK v2.0.0、Azure AD(Entra ID)アプリ登録済み、委任権限として Files.ReadWrite.All と Sites.ReadWrite.All を付与済み、という構成です。なお、これらの権限の有無は「ポップアップが親に戻らない」という物理現象の直接原因ではありません(後述)が、テナントの同意ポリシーとの組み合わせで間接的に失敗を引き起こすことがあります。
結論(最短の解決方針)
ほぼすべての「戻らない」問題は、次の3 点セットで収束します。まずはこの順に確認してください。
- Azure ポータルでリダイレクト URI を正確に登録する。(ポップアップで最後に表示される コールバック URL=
auth-end.htmlなどを登録) - ポップアップのコールバック ページで
notifySuccess/notifyFailureを必ず呼ぶ。(SDK 初期化後すぐに) - 親タブとコールバック ページの双方で SDK を最初に
initializeする。
この 3 つのどれか 1 つでも欠けると、ポップアップは閉じても親タブの successCallback は呼ばれず、最終的に 「閉じられた」 か 「タイムアウト」 として扱われます。
原因と対策の一覧
| 主な原因 | 対応策(※は必須) | 補足・ポイント |
|---|---|---|
| ① リダイレクト URI 未登録 | ※ Azure ポータル →「認証」→ リダイレクト URI に、ポップアップの最終遷移先(例 https://{ドメイン}/auth-end.html)を 正確に追加。 | Teams アプリ マニフェストの validDomains に同ドメインを入れるのも忘れずに。 |
| ② コールバックが親へ結果通知していない | ※ auth-end.html 内で SDK 初期化後に microsoftTeams.authentication.notifySuccess(result) または ...notifyFailure(errorMessage) を必ず呼ぶ。 | これを呼ばないと、ポップアップは閉じるが親に結果が返らずタイムアウト扱い。 |
| ③ SDK 初期化忘れ | ※ 親タブと auth-end.html の双方で最初に microsoftTeams.initialize()(または app.initialize())を実行。 | initialize 前に他の関数を呼ぶと不定動作や例外に。 |
| ④ ポップアップのブロック/早期クローズ | ユーザー操作(クリック等)の直後に authenticate を起動。認証ページの初期描画を軽量化し、ブラウザが「空のポップアップ」と誤判定しないように。 | 特に Teams デスクトップ(Edge/Chromium WebView)で顕著。 |
| ⑤ SSO/資格情報キャッシュの矛盾 | 同一アカウントで複数サインインの干渉を疑う。開発中は Teams キャッシュ削除やブラウザのシークレット モードで再現性を確認。 | 企業テナントのポリシーや管理者同意の要否とも関連。 |
最短チェックリスト(これだけで直ることが多い)
- マニフェストに
validDomainsとしてexample.com(あなたのタブ/認証ページのドメイン)を含める。 - Azure ポータルのアプリ登録 →「認証」→ リダイレクト URI に
https://example.com/auth-end.htmlを追加。 - 親タブのスクリプト先頭で
microsoftTeams.initialize()(またはapp.initialize())を呼ぶ。 - クリック直後に
authenticateを呼ぶ(自動起動にしない)。 - auth-end.html のスクリプト先頭でも
microsoftTeams.initialize()を呼び、その直後にnotifySuccess(またはnotifyFailure)を呼ぶ。 - 成功時は JSON 文字列で必要情報(
stateやcode)を返し、親タブ側で検証する。
実装例:親タブ(タブ本体)
以下は TypeScript/モジュール構成の例です。CDN スクリプトを使わずバンドルする想定ですが、グローバルの microsoftTeams オブジェクトでも同様です。
import { app, authentication } from "@microsoft/teams-js";
(async () => {
await app.initialize(); // SDK v2 の初期化
})().catch(console.error);
// クリックイベントから起動(ポップアップブロック対策)
document.getElementById("signin-btn")?.addEventListener("click", () => {
const state = crypto.randomUUID();
const authUrl = `/auth-start.html?state=${encodeURIComponent(state)}`;
authentication.authenticate({
url: authUrl,
width: 600,
height: 560,
successCallback: (result) => {
try {
const payload = JSON.parse(result);
if (payload.state !== state) {
throw new Error("State mismatch");
}
// ここでサーバーへ認可コードを渡してトークン交換(PKCE/OBO など)
console.log("Auth success", payload);
} catch (e) {
console.error("Invalid result", e);
// 必要に応じて UI へ反映
}
},
failureCallback: (reason) => {
console.error("Auth failed:", reason);
// UI にエラー表示・再試行ボタンなどを出す
},
});
});
ポイントは「クリック直後に authenticate を起動する」「initialize を先に呼ぶ」「state を自前で発行して往復で照合する」ことです。
実装例:認証フロー中間ページ(auth-start.html)
ここでは AAD(Entra ID)の認可エンドポイントにリダイレクトして、最後に auth-end.html に戻すだけの役割です。フレーム内で複雑な描画を行う必要はありません。
<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>Auth Start</title></head>
<body>
<script type="module">
// SDK の読み込みは不要(ここでは単純に AAD へ遷移するだけ)
const params = new URLSearchParams(window.location.search);
const state = params.get("state") ?? "";
// あなたのアプリ設定に合わせて置換
const tenant = "common";
const clientId = "YOUR_CLIENT_ID";
const redirectUri = encodeURIComponent("https://example.com/auth-end.html");
const scopes = encodeURIComponent("openid profile offline_access Files.ReadWrite.All Sites.ReadWrite.All");
// 認可コード + PKCE を推奨(ここでは簡略化)
const authorizeUrl =
`https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize` +
`?client_id=${clientId}` +
`&response_type=code` +
`&redirect_uri=${redirectUri}` +
`&response_mode=query` +
`&scope=${scopes}` +
`&state=${encodeURIComponent(state)}`;
window.location.replace(authorizeUrl);
実運用では PKCE とバックエンドでのトークン交換(OBO など)を実装してください。ここでは 「最終的に auth-end.html に戻ってくる」 ことだけが重要です。
実装例:コールバック ページ(auth-end.html)
最重要ポイントは「SDK を初期化してから notifySuccess / notifyFailure を呼ぶ」です。これを怠ると、親タブは永遠に結果を受け取れません。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Auth End</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self' https://teams.microsoft.com https://*.teams.microsoft.com;">
</head>
<body>
<script>
/* グローバルオブジェクトがある前提。モジュール構成の場合は import { app, authentication } from "@microsoft/teams-js" を使用 */
(function () {
try {
// v1 互換の初期化(v2 は app.initialize() でも可)
if (window.microsoftTeams && typeof window.microsoftTeams.initialize === "function") {
window.microsoftTeams.initialize();
}
// 認証結果をパース(query または hash のどちらか)
const query = new URLSearchParams(window.location.search);
const hash = new URLSearchParams((window.location.hash || "").replace(/^#/, ""));
const code = query.get("code") || hash.get("code") || "";
const state = query.get("state") || hash.get("state") || "";
const error = query.get("error") || hash.get("error") || "";
const errorDescription = query.get("error_description") || hash.get("error_description") || "";
if (error) {
microsoftTeams.authentication.notifyFailure(`${error}: ${errorDescription}`);
return;
}
if (!code) {
microsoftTeams.authentication.notifyFailure("Missing authorization code");
return;
}
// 親へ結果を戻す(JSON 文字列で返すのが安全)
const payload = JSON.stringify({ code, state, ts: Date.now() });
microsoftTeams.authentication.notifySuccess(payload);
// 以降の window.close() は基本不要(SDK が管理)
} catch (e) {
try {
microsoftTeams.authentication.notifyFailure(String(e));
} catch (_) {
// SDK 初期化前に落ちた場合の保険(親には届かないことがある)
}
}
})();
このページは Teams から独立したポップアップで動きます。X-Frame-Options を DENY にしたり、CSP の frame-ancestors で Teams を拒否していると描画できない場合があります。上のサンプルのように frame-ancestors を許可するか、CSP を環境に合わせて見直してください。
アプリ マニフェストと Azure 登録の最低設定
Teams アプリ マニフェスト(抜粋)
{
"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.16/MicrosoftTeams.schema.json",
"manifestVersion": "1.16",
"version": "1.0.0",
"id": "YOUR-APP-ID",
"developer": { "name": "Your Org", "websiteUrl": "https://example.com", "privacyUrl": "https://example.com/privacy", "termsOfUseUrl": "https://example.com/terms" },
"name": { "short": "Sample Tab", "full": "Sample Tab App" },
"description": { "short": "Auth sample", "full": "Authentication using microsoftTeams.authentication.authenticate()" },
"staticTabs": [
{
"entityId": "tab",
"name": "Home",
"contentUrl": "https://example.com/tab.html",
"websiteUrl": "https://example.com/",
"scopes": ["personal"]
}
],
"validDomains": ["example.com"] // ここに auth-start/end と同じドメインを必ず入れる
}
Azure(Entra ID)アプリ登録の要点
- プラットフォームは SPA または Web を選択(実装に合わせる)。リダイレクト URI に
https://example.com/auth-end.htmlを追加。 - 公開 API や リダイレクトの種類(
query/fragment/form_post)は、あなたのフロー(コード+PKCE/OBO 等)に合わせる。 - API のアクセス許可(例:
Files.ReadWrite.All、Sites.ReadWrite.All)は、ポップアップの生死には無関係。ただし管理者同意が必要な場合、最初の同意画面で エラー終了→ポップアップだけ閉じる と見えることがあるため、権限は最小から始めるのが安全。
トラブルシュート:症状別の判定早見表
| 見える症状 | 観測ポイント | 疑う箇所 | 対処 |
|---|---|---|---|
| サインイン直後に無言で閉じる | 親タブの failureCallback に The authentication window was closed | auth-end.html で notifySuccess/Failure 未実行 | initialize() → notify... の順で必ず呼ぶ |
| ポップアップが開かない | コンソールにポップアップブロックの警告 | 自動実行/非ユーザー操作 | クリック直後に authenticate を呼ぶ |
| サインイン後にエラー表示 | DevTools の Network で auth-end.html が 200 で返っているか | リダイレクト URI の不一致 | Azure の登録値と完全一致させる(末尾スラッシュやスキーム) |
| 特定ユーザーだけ失敗 | 複数アカウントでの同居、テナントの同意ポリシー | SSO/資格情報キャッシュの衝突 | シークレット モードで再現確認、不要なサインインを整理、キャッシュ削除 |
なぜ notifySuccess を呼ばないと「閉じた」扱いになるのか
Teams の認証ポップアップは、親タブとポップアップ間で SDK のメッセージ チャネルを用いて結果を返します。親は successCallback または failureCallback のどちらか一方を待ちます。auth-end.html 側が notifySuccess も notifyFailure も呼ばないままウィンドウが閉じると、親には「ユーザーが閉じた」以外の事実が分からず、Closed/Cancelled とみなされます。
講じるべき対策はとてもシンプルで、SDK 初期化→即 notify... 実行です。ポップアップ内で時間のかかる処理を行う場合は、一旦 notifySuccess で最小限のトークン(または認可コード)だけ返し、重い処理は親タブやバックエンドで続行すると UX が安定します。
実装のベストプラクティス
権限(スコープ)は最小から
管理者同意が必要な広域スコープを最初から要求すると、テナント設定によってはサインイン ダイアログがエラーで閉じることがあります。最初は OpenID(openid profile)とアプリに本当に必要な最小スコープだけで検証し、段階的に増やしましょう。
state の往復と検証
CSRF 防止のため、親タブで生成した state を auth-start.html に渡し、auth-end.html から notifySuccess で同じ値を返します。親は state の一致を確認できない場合は必ず失敗扱いにします。
Teams 開発者ポータルでの検証
SSO(Single Sign-On)をマニフェストで有効にしていると、getAuthToken() などの SSO API を使う道が開けます。今回のように authenticate を使うカスタム フローを選ぶなら、開発者ポータル上で SSO を無効(または併用時はフローの責務を明確化)にしておくと混乱が減ります。
ログの活用
- 親タブ:
failureCallbackのreasonをconsole.log。Cancelled/Timeout/Other の違いで原因が絞れます。 - ポップアップ:Network パネルで
auth-end.htmlが 200 か、URL にcode/stateが載っているかを確認。
ポップアップの安定化
- クリック直後に起動(ポップアップ ブロック対策)。
- 最初のペイントを軽量に(空白画面は早期クローズの誘因)。
- SameSite=None; Secure なクッキーを使う場合は HTTPS を徹底。
window.close()は原則不要(SDK が管理)。使うならnotify...後に。
「それでも戻らない」時の深掘りポイント
1. リダイレクト URI の完全一致
Azure 側の登録と実際の遷移先が 1 文字でも異なると失敗します。スキーム(http/https)、ホスト、ポート、パス、末尾スラッシュまで一致しているか確認してください。ローカル開発では http://localhost:ポート を SPA/Web のいずれかに正しく登録しておく必要があります。
2. マニフェストの validDomains
ポップアップで表示するドメインは validDomains に入っていないとブロックされます。サブドメインが分かれている場合(例:app.example.com と auth.example.com)は両方を列挙します。
3. CSP と X-Frame-Options
X-Frame-Options: DENY や厳しすぎる frame-ancestors は Teams 内表示やダイアログ遷移を阻害します。必要に応じて https://teams.microsoft.com、https://*.teams.microsoft.com を許可します。
4. アカウントの競合
Teams デスクトップでは MSA/職場/学校アカウントの複数同居により意図しないテナントへ飛ぶケースがあります。いったんすべての Teams/ブラウザ セッションからサインアウトし、シークレット モードで単一アカウントのみで再検証します。
5. タイムゾーン/時刻ずれ
極端な端末時計のずれは認可コードの有効期限に影響します。NTP が正常か確認します。
完全最小構成のサンプル(4 ファイル)
次の 4 ファイルを HTTPS 配下に配置し、Teams 開発者ポータルからタブを追加すれば、動作確認できます。
/tab.html(親タブ)/tab.js(親タブのロジック)/auth-start.html(認可開始)/auth-end.html(結果通知)
上記コード断片をそれぞれ貼り付け、Azure 側のリダイレクト URI に https://{ドメイン}/auth-end.html を登録、マニフェストの validDomains に {ドメイン} を入れれば、まず「戻らない」問題は再現しなくなるはずです。
よくある Q&A
Q. Files.ReadWrite.All や Sites.ReadWrite.All を付けたら急に失敗し始めた…
A. その権限自体はポップアップの往復に直接影響しませんが、「管理者同意が必要」なために同意ダイアログでエラー終了し、結果としてポップアップが閉じることがあります。最小権限でまず成功(親タブへ戻る)させてから、必要に応じて権限を拡張し、同意は管理者経由で事前に与えるのが安全です。
Q. dialog API ではだめ?
A. 一般のダイアログ表示には dialog API も使えますが、認証の「結果通知」フローが組み込まれているのは authentication.authenticate です。既存の実装を尊重するならまずは authenticate を安定させるのが近道です。
Q. notifySuccess の引数は必ず JSON?
A. 文字列であれば任意ですが、JSON 文字列が推奨です。state、code、タイムスタンプなど、後工程で検証しやすくなります。
Q. 親タブ側は Promise で書ける?
A. authenticate はコールバック型ですが、ラッパーで Promise 化すれば扱いやすくなります。
function authenticateAsync(opts) {
return new Promise((resolve, reject) => {
microsoftTeams.authentication.authenticate({
...opts,
successCallback: (result) => resolve(result),
failureCallback: (reason) => reject(new Error(reason)),
});
});
}
セキュリティと運用の注意点
- PKCE + 認可コードを基本にする(暗黙的フローは避ける)。
- OBO(On-Behalf-Of)フローで Graph などのリソース トークンをサーバー側で取得し、タブからはアクセストークンを扱わない設計が安全。
- ブラウザや Teams デスクトップのアップデートで挙動が変わることがあるため、回帰テストを自動化(E2E テストで
notify...の到達を検証)。 - 障害時のために、再試行ボタンと、失敗理由(
reason)の収集・可視化を実装。
「もう迷わない」ための実装チェックシート
| チェック | 内容 | OK の基準 |
|---|---|---|
| SDK 初期化(親) | app.initialize() または microsoftTeams.initialize() を最初に呼ぶ | 初期化前に SDK API を呼んでいない |
| SDK 初期化(子) | auth-end.html でも同様に初期化 | notify... 実行前に初期化済み |
| ユーザー操作 | クリック直後に authenticate | 自動起動・ページロード直後に呼んでいない |
| リダイレクト URI | Azure と実際の URL が完全一致 | スキーム/ポート/パス/末尾まで一致 |
| validDomains | 検証に用いる全ドメインを列挙 | 不足がない(サブドメイン含む) |
| state | 親で発行 → 子で返却 → 親で一致検証 | 不一致なら即失敗にできる |
| エラーハンドリング | failureCallback でユーザー向けメッセージと再試行導線 | 原因ごとにガイダンスを表示 |
まとめ
microsoftTeams.authentication.authenticate() の「サインイン後に戻らない」現象は、(1)リダイレクト URI の登録、(2)auth-end.html での notify... 呼び出し、(3)親子双方の SDK 初期化のどれかが欠けていることがほとんどです。まずは本記事のチェックリストと最小コードで動作を安定させ、次に権限や OBO など運用要件を段階的に組み込む――この順番が、最短で確実にプロダクション品質へ到達するコツです。

コメント