Teams タブの認証ポップアップが戻らない原因と解決策|microsoftTeams.authentication.authenticate() 失敗を確実に直す手順【Teams JS SDK v2】

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.AllSites.ReadWrite.All を付与済み、という構成です。なお、これらの権限の有無は「ポップアップが親に戻らない」という物理現象の直接原因ではありません(後述)が、テナントの同意ポリシーとの組み合わせで間接的に失敗を引き起こすことがあります。

結論(最短の解決方針)

ほぼすべての「戻らない」問題は、次の3 点セットで収束します。まずはこの順に確認してください。

  1. Azure ポータルでリダイレクト URI を正確に登録する。(ポップアップで最後に表示される コールバック URLauth-end.html などを登録)
  2. ポップアップのコールバック ページで notifySuccess / notifyFailure を必ず呼ぶ。(SDK 初期化後すぐに)
  3. 親タブとコールバック ページの双方で 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 キャッシュ削除やブラウザのシークレット モードで再現性を確認。企業テナントのポリシーや管理者同意の要否とも関連。

最短チェックリスト(これだけで直ることが多い)

  1. マニフェストvalidDomains として example.com(あなたのタブ/認証ページのドメイン)を含める。
  2. Azure ポータルのアプリ登録 →「認証」→ リダイレクト URIhttps://example.com/auth-end.html を追加。
  3. 親タブのスクリプト先頭で microsoftTeams.initialize()(または app.initialize())を呼ぶ。
  4. クリック直後authenticate を呼ぶ(自動起動にしない)。
  5. auth-end.html のスクリプト先頭でも microsoftTeams.initialize() を呼び、その直後に notifySuccess(または notifyFailure)を呼ぶ。
  6. 成功時は JSON 文字列で必要情報(statecode)を返し、親タブ側で検証する。

実装例:親タブ(タブ本体)

以下は 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-OptionsDENY にしたり、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 を選択(実装に合わせる)。リダイレクト URIhttps://example.com/auth-end.html を追加。
  • 公開 APIリダイレクトの種類query/fragment/form_post)は、あなたのフロー(コード+PKCE/OBO 等)に合わせる。
  • API のアクセス許可(例:Files.ReadWrite.AllSites.ReadWrite.All)は、ポップアップの生死には無関係。ただし管理者同意が必要な場合、最初の同意画面で エラー終了→ポップアップだけ閉じる と見えることがあるため、権限は最小から始めるのが安全。

トラブルシュート:症状別の判定早見表

見える症状観測ポイント疑う箇所対処
サインイン直後に無言で閉じる親タブの failureCallbackThe authentication window was closedauth-end.htmlnotifySuccess/Failure 未実行initialize()notify... の順で必ず呼ぶ
ポップアップが開かないコンソールにポップアップブロックの警告自動実行/非ユーザー操作クリック直後に authenticate を呼ぶ
サインイン後にエラー表示DevTools の Network で auth-end.html が 200 で返っているかリダイレクト URI の不一致Azure の登録値と完全一致させる(末尾スラッシュやスキーム)
特定ユーザーだけ失敗複数アカウントでの同居、テナントの同意ポリシーSSO/資格情報キャッシュの衝突シークレット モードで再現確認、不要なサインインを整理、キャッシュ削除

なぜ notifySuccess を呼ばないと「閉じた」扱いになるのか

Teams の認証ポップアップは、親タブとポップアップ間で SDK のメッセージ チャネルを用いて結果を返します。親は successCallback または failureCallback のどちらか一方を待ちます。auth-end.html 側が notifySuccessnotifyFailure も呼ばないままウィンドウが閉じると、親には「ユーザーが閉じた」以外の事実が分からず、Closed/Cancelled とみなされます。

講じるべき対策はとてもシンプルで、SDK 初期化→即 notify... 実行です。ポップアップ内で時間のかかる処理を行う場合は、一旦 notifySuccess で最小限のトークン(または認可コード)だけ返し、重い処理は親タブやバックエンドで続行すると UX が安定します。

実装のベストプラクティス

権限(スコープ)は最小から

管理者同意が必要な広域スコープを最初から要求すると、テナント設定によってはサインイン ダイアログがエラーで閉じることがあります。最初は OpenID(openid profile)とアプリに本当に必要な最小スコープだけで検証し、段階的に増やしましょう。

state の往復と検証

CSRF 防止のため、親タブで生成した stateauth-start.html に渡し、auth-end.html から notifySuccess で同じ値を返します。親は state の一致を確認できない場合は必ず失敗扱いにします。

Teams 開発者ポータルでの検証

SSO(Single Sign-On)をマニフェストで有効にしていると、getAuthToken() などの SSO API を使う道が開けます。今回のように authenticate を使うカスタム フローを選ぶなら、開発者ポータル上で SSO を無効(または併用時はフローの責務を明確化)にしておくと混乱が減ります。

ログの活用

  • 親タブ:failureCallbackreasonconsole.log。Cancelled/Timeout/Other の違いで原因が絞れます。
  • ポップアップ:Network パネルで auth-end.html200 か、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.comauth.example.com)は両方を列挙します。

3. CSP と X-Frame-Options

X-Frame-Options: DENY や厳しすぎる frame-ancestors は Teams 内表示やダイアログ遷移を阻害します。必要に応じて https://teams.microsoft.comhttps://*.teams.microsoft.com を許可します。

4. アカウントの競合

Teams デスクトップでは MSA/職場/学校アカウントの複数同居により意図しないテナントへ飛ぶケースがあります。いったんすべての Teams/ブラウザ セッションからサインアウトし、シークレット モードで単一アカウントのみで再検証します。

5. タイムゾーン/時刻ずれ

極端な端末時計のずれは認可コードの有効期限に影響します。NTP が正常か確認します。

完全最小構成のサンプル(4 ファイル)

次の 4 ファイルを HTTPS 配下に配置し、Teams 開発者ポータルからタブを追加すれば、動作確認できます。

  1. /tab.html(親タブ)
  2. /tab.js(親タブのロジック)
  3. /auth-start.html(認可開始)
  4. /auth-end.html(結果通知)

上記コード断片をそれぞれ貼り付け、Azure 側のリダイレクト URI に https://{ドメイン}/auth-end.html を登録、マニフェストの validDomains{ドメイン} を入れれば、まず「戻らない」問題は再現しなくなるはずです。

よくある Q&A

Q. Files.ReadWrite.AllSites.ReadWrite.All を付けたら急に失敗し始めた…

A. その権限自体はポップアップの往復に直接影響しませんが、「管理者同意が必要」なために同意ダイアログでエラー終了し、結果としてポップアップが閉じることがあります。最小権限でまず成功(親タブへ戻る)させてから、必要に応じて権限を拡張し、同意は管理者経由で事前に与えるのが安全です。

Q. dialog API ではだめ?

A. 一般のダイアログ表示には dialog API も使えますが、認証の「結果通知」フローが組み込まれているのは authentication.authenticate です。既存の実装を尊重するならまずは authenticate を安定させるのが近道です。

Q. notifySuccess の引数は必ず JSON?

A. 文字列であれば任意ですが、JSON 文字列が推奨です。statecode、タイムスタンプなど、後工程で検証しやすくなります。

Q. 親タブ側は Promise で書ける?

A. authenticate はコールバック型ですが、ラッパーで Promise 化すれば扱いやすくなります。

function authenticateAsync(opts) {
  return new Promise((resolve, reject) =&gt; {
    microsoftTeams.authentication.authenticate({
      ...opts,
      successCallback: (result) =&gt; resolve(result),
      failureCallback: (reason) =&gt; 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自動起動・ページロード直後に呼んでいない
リダイレクト URIAzure と実際の URL が完全一致スキーム/ポート/パス/末尾まで一致
validDomains検証に用いる全ドメインを列挙不足がない(サブドメイン含む)
state親で発行 → 子で返却 → 親で一致検証不一致なら即失敗にできる
エラーハンドリングfailureCallback でユーザー向けメッセージと再試行導線原因ごとにガイダンスを表示

まとめ

microsoftTeams.authentication.authenticate() の「サインイン後に戻らない」現象は、(1)リダイレクト URI の登録、(2)auth-end.html での notify... 呼び出し、(3)親子双方の SDK 初期化のどれかが欠けていることがほとんどです。まずは本記事のチェックリストと最小コードで動作を安定させ、次に権限や OBO など運用要件を段階的に組み込む――この順番が、最短で確実にプロダクション品質へ到達するコツです。

コメント

コメントする

目次