0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Teamsタブに Streamlit を Entra認証で載せる

0
Last updated at Posted at 2026-06-10

本記事記述および実装内容は、多くの部分をAIで処理しており、人間チェックは甘いです

はじめに

「Microsoft Teams のタブから、自前の Web アプリ(Azure App Service)を呼び出したい。しかも Teams のサインインユーザー(Entra ID)と連携したい」——よくある要望ですが、実際にやってみると App Service Easy Auth の落とし穴Teams manifest の地雷 で何度か足を取られます。

この記事では、Python Streamlit のダミーチャット(LLM なし)を Azure App Service にホストし、Easy Auth (Microsoft Entra ID) で保護した上で、Teams の Personal Tab から SSO 込みで呼び出すまでを、ハマりどころ中心にまとめます。

対象読者は次のような方です。

  • Teams タブに自前の Web アプリ(App Service)を載せたい
  • App Service Easy Auth (Entra) と Teams を連携させたい
  • Streamlit を iframe で Teams に埋め込めるのか知りたい
  • 「Teams 経由だけ許可、直リンクは禁止」ができるのか気になる

こんな画面
image.png

結論

  • ✅ Streamlit を App Service (Linux/B1) にホストし、Easy Auth (Entra) で保護 → Teams Personal Tab / 標準ブラウザの双方からログイン成功
  • ✅ アプリ側は認証コードを書かず、Easy Auth が注入する X-MS-CLIENT-PRINCIPAL-* ヘッダを読むだけでログインユーザーを表示できる。
  • ⚠️ 最大のハマり: ログイン後に /.auth/login/aad/callback401 になる。原因は Entra アプリ登録の 「IDトークン発行」が無効だったこと(Easy Auth は code+id_token ハイブリッドフローを使うため必須)。
  • ⚠️ Teams への zip アップロードで「マニフェスト解析エラー」。原因は manifest v1.17 で廃止済みの packageName が残っていたこと。
  • ℹ️ 「Teams 経由だけ許可、直アクセス禁止」は Easy Auth 単体では不可。やるなら Teams SSO トークン検証が必要(後述の3手法比較)。

アーキテクチャ

アプリ本体(Streamlit)

ポイントは、アプリ自身は認証ロジックを持たないことです。トークン検証もログイン画面も Easy Auth が前段で担当し、アプリは「認証済み前提でヘッダを読むだけ」。Streamlit 1.37+ の st.context.headers でリクエストヘッダにアクセスできます。

src/app.py
import base64
import json
import streamlit as st


def get_identity():
    """Easy Auth が注入するヘッダから Entra ユーザーを取得する。"""
    headers = dict(st.context.headers or {})
    name = headers.get("X-MS-CLIENT-PRINCIPAL-NAME")  # UPN

    claims = {}
    principal_b64 = headers.get("X-MS-CLIENT-PRINCIPAL")  # base64(JSON)
    if principal_b64:
        decoded = json.loads(base64.b64decode(principal_b64).decode("utf-8"))
        for c in decoded.get("claims", []):
            typ = c.get("typ", "")
            short = typ.rsplit("/", 1)[-1] if "/" in typ else typ
            claims[short] = c.get("val", "")
    return name, claims


def dummy_reply(text: str) -> str:
    """LLM を使わないダミー応答(エコー)。"""
    return f"受け取りました: 「{text}」({len(text)} 文字)"


user_name, claims = get_identity()
with st.sidebar:
    if user_name:
        st.success(f"ログイン中: {user_name}")
    st.json(claims or {"info": "claims なし"})

st.title("💬 ダミーチャット")
if "messages" not in st.session_state:
    st.session_state.messages = []
for m in st.session_state.messages:
    with st.chat_message(m["role"]):
        st.markdown(m["content"])
if prompt := st.chat_input("メッセージを入力..."):
    st.session_state.messages.append({"role": "user", "content": prompt})
    st.session_state.messages.append({"role": "assistant", "content": dummy_reply(prompt)})
    st.rerun()

App Service Linux では起動コマンドを指定します(既定ポートは 8000、iframe 用に CORS/XSRF を緩和、Streamlit の WebSocket を有効化)。

startup
python -m streamlit run app.py --server.port 8000 --server.address 0.0.0.0 \
  --server.headless true --server.enableCORS false \
  --server.enableXsrfProtection false --browser.gatherUsageStats false

インフラ(Bicep)の要点

App Service Plan(B1 Linux) + Web App + Easy Auth V2 を Bicep で構成します。Easy Auth は authsettingsV2 で Entra プロバイダを設定します。

infra/modules/appservice.bicep
resource authSettings 'Microsoft.Web/sites/config@2024-04-01' = {
  parent: site
  name: 'authsettingsV2'
  properties: {
    platform: { enabled: true }
    globalValidation: {
      requireAuthentication: true
      unauthenticatedClientAction: 'RedirectToLoginPage'
      redirectToProvider: 'azureactivedirectory'
    }
    identityProviders: {
      azureActiveDirectory: {
        enabled: true
        registration: {
          openIdIssuer: '${environment().authentication.loginEndpoint}<tenant-id>/v2.0'
          clientId: '<client-id>'
          clientSecretSettingName: 'MICROSOFT_PROVIDER_AUTHENTICATION_SECRET'
        }
        validation: {
          allowedAudiences: [ 'api://<client-id>', '<client-id>' ]
        }
      }
    }
    login: { tokenStore: { enabled: true } }
  }
}

Entra App Registration は Bicep では作れません。 azd の preprovision / postprovision フックで az ad app + Microsoft Graph PATCH を使って構成します(scope access_as_user、Teams クライアントの事前承認、リダイレクト URI など)。

ハマったポイントと解決策

1. ログイン後 callback が 401(最重要)

Entra ログイン画面までは出るのに、戻ってくると /.auth/login/aad/callback401標準ブラウザでも再現したので iframe の問題ではありませんでした。

原因: App Service Easy Auth の既定フローは response_type=code+id_token(OpenID Connect ハイブリッド)。IDトークンの発行が必須ですが、Entra アプリ登録の enableIdTokenIssuancefalse だったため、認可後のコールバックが拒否されていました。

解決は、アプリ登録で IDトークン発行を有効化するだけです。

fix-idtoken.sh
az rest --method PATCH \
  --uri "https://graph.microsoft.com/v1.0/applications/<app-object-id>" \
  --headers "Content-Type=application/json" \
  --body '{"web":{"implicitGrantSettings":{"enableIdTokenIssuance":true,"enableAccessTokenIssuance":false}}}'

Portal では Entra ID → アプリの登録 → 認証 → 「暗黙的な許可およびハイブリッド フロー」→ 「ID トークン」にチェック、の状態です。

Graph の正しいプロパティ名は implicitGrantSettings です。implicitGrant という名前は無効で PATCH が拒否されます(これも一度踏みました)。

2. Teams への zip アップロードで「マニフェスト解析エラー」

zip をアップロードすると「マニフェスト解析エラー メッセージが利用できません」とだけ表示され、原因が分かりません。

原因: Teams manifest schema v1.17 は additionalProperties: false。旧バージョンで使っていた packageName プロパティが余分として弾かれていました。公式スキーマで事前検証すると一発で分かります。

validate-manifest.py
import json
from jsonschema import Draft7Validator

schema = json.load(open("teams-1.17.schema.json"))   # 公式 v1.17 schema
inst = json.load(open("manifest.json"))
for e in Draft7Validator(schema).iter_errors(inst):
    print("NG:", list(e.path), "->", e.message)
# → Additional properties are not allowed ('packageName' was unexpected)

packageName を削除して解決しました。Teams タブ用の manifest 抜粋は以下です。

appPackage/manifest.json
{
  "manifestVersion": "1.17",
  "id": "<client-id>",
  "staticTabs": [
    {
      "entityId": "chat",
      "name": "Chat",
      "contentUrl": "https://<app-name>.azurewebsites.net/",
      "scopes": ["personal"]
    }
  ],
  "validDomains": ["<app-name>.azurewebsites.net"],
  "webApplicationInfo": {
    "id": "<client-id>",
    "resource": "api://<client-id>"
  }
}

3. その他の小さな罠

az / azd 周りで踏んだ細かいハマり
  • azd env get-value <key> はキー未存在時にエラー文を stdout に出すVAR=$(azd env get-value K || echo "") だとエラー文を変数に取り込むので、GUID 形式チェックで妥当性を見るのが安全。
  • Microsoft Graph の application PATCH は、scope と preAuthorizedApplications を同一 PATCH に含めると Permission Id cannot be found になる。scope を先に PATCH → preAuthorized を別 PATCH の2段階で解決。
  • az webapp auth update(V1系)は V2 構成済みアプリに対して Bad Request。V2 は authsettingsV2 の REST で操作する。
  • curl で / が 401: Accept: text/html の無い API 形式リクエストは Easy Auth が 401 を返す。ブラウザ(Accept: text/html)では正常に 302。仕様です。
  • B1 のクォータ不足: あるリージョンで B1 の VM クォータが 0 で provision 失敗。az appservice plan create --sku B1 の実投入で判定し、別リージョンにフォールバック。ARM の deployment validate はクォータを検出しません。

検証結果

シナリオ 結果
インフラ provision(B1 Linux + Easy Auth V2)
未認証アクセス → Entra ログインへ 302
Teams / 標準ブラウザでログイン&ユーザー表示
Streamlit ダミーチャット動作(LLM 不使用)
Entra App 登録(scope / 事前承認 / redirect / id_token)
Teams manifest パッケージ生成・サイドロード

懸念していた iframe のサードパーティ Cookie 制約(Teams Web タブで Easy Auth の Cookie がブロックされ得る問題)は、この環境では顕在化せず、Teams Web タブでもそのままログインできました。

おまけ: 「Teams 経由だけ許可、直アクセス禁止」はできる?

「ブラウザで直接 URL を開いても使えないようにしたい」という要望。結論、Easy Auth 単体では不可です(Teams 経由でも直アクセスでも同じ Entra ユーザーが同じようにログインするため区別できない)。X-Frame-Options / CSP frame-ancestors は「どこに埋め込みを許すか」の制御で、方向が逆なので効きません。

実現方法は強度別に3つあります。

観点 A: HTTPヘッダ判定 B: Teams JS SDK 判定 C: Teams SSO トークン検証
仕組み Sec-Fetch-Dest/Referer で推測 microsoftTeams.app.initialize() の成否 getAuthToken() のトークンの azp=Teams を検証
強度 弱(偽装可) 強(本物の境界)
実装コスト
Streamlit 相性 △(WS再実行で不安定) △(ラッパー要) ✕〜△(別フロント推奨)
Easy Auth との関係 併用可 併用可 要切替

確実に直アクセスを禁止したいなら C(Teams SSO トークン検証)一択です。getAuthToken() は Teams の中でしか取得できず、トークンの azp(呼び出し元クライアント)が Teams クライアント ID になります。これをサーバ検証すれば「Teams 経由」だけを通せます。ただし Easy Auth の Cookie 方式からトークン方式への切替が必要で、Streamlit では薄い認証フロント+検証 API を別に置く設計が現実的です。

まとめ

  • Streamlit を App Service に載せ、Easy Auth (Entra) で保護して Teams タブから呼ぶ、は 十分実用的。アプリは Easy Auth ヘッダを読むだけでユーザー連携できる。
  • 一番ハマるのは enableIdTokenIssuance。Easy Auth を使うなら Entra アプリ登録で IDトークン発行を ON にする、を覚えておくと丸ごと一日節約できます。
  • Teams manifest は 新しい schema の additionalProperties:false に注意。余分プロパティ(packageName 等)は削除し、公式スキーマで事前検証。
  • 「Teams 経由のみ許可」はセキュリティ要件なら Teams SSO トークン検証(方法C)まで踏み込む必要がある。

参考リンク

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?