🆔

[Teams][Entra ID] 2025 年版: Teams アプリに SSO を実装してみた話

に公開

概要

今回は以下の記事の内容を 2025 年版として更新しつつ、Teams カスタム アプリにおける SSO についてもう少し詳しく書いていこうと思います。
https://zenn.dev/yuta331/articles/6f8c26ee1e3208

今回は Docker と devtunnel を使用して完全ローカルで動かすことを目指します。
(前回は ngrok を使用していましたが、devtunnel に改宗しました。)

前回はバックエンドまで作成しましたが、今回は SSO にフォーカスするため、フロントの Teams カスタム アプリのみにとどめようと思います。

今回作成したものはこちら

https://github.com/Yuta-31/TeamsTabSSOAppSample

SSO について

そもそも SSO ってなんだ

SSO は "Single Sign-On" の略称で、一度の認証で複数のサービスを使用できるようにする という 概念 です。
Teams カスタム アプリにおいては Teams にサインインしているユーザーの ID を使用してカスタム アプリへのアクセスを提供する ことができます。
わざわざ同じユーザーで何回もサインインしなくて良いのでユーザー目線でらくちんになるってことですね。

https://learn.microsoft.com/ja-jp/microsoftteams/platform/concepts/authentication/authentication

Teams 内の SSO は、アプリ ユーザーの Teams ID を使用してアプリへのアクセスを提供する認証方法です。

Teams カスタム アプリではどうやってサインインしているユーザーにて認証を実施しているのか。

認証を実施する方法については以下の公開情報に記載されています。

https://learn.microsoft.com/ja-jp/microsoftteams/platform/tabs/how-to/authentication/tab-sso-overview#sso-in-teams-at-runtime

ざっくり説明すると、タブアプリ上で Teams クライアントに対して getAuthToken() を呼び出すと裏でいろいろやってくれてトークン (JWT; Json Web Token) を返却してくれるという感じですね。

もう少し詳しく

せっかくなのでもう少し詳しくどんな動作になっているのかを見てみましょう。

上記の公開情報にも記載されている以下の図で見てみます。

  1. タブアプリ上で getAuthToken() を実行し、 Teams へトークンを取得するよう指示します。
  2. Teams は Entra へアクセストークンを要求します。
  3. 必要に応じて Entra はユーザーへ同意や 2 段階認証を求める要求をします。
  4. Entra はアクセストークンを Teams へ返します。
  5. Teams はタブ アプリへトークンを渡します。

それでは実際に実装しましょう

実際にカスタムアプリに SSO を実装するためには大きく 3 のステップがあります。

  1. Entra ID でアプリケーションを構成する。
  2. カスタム アプリ内にてトークンの取得するコードを追記する。
  3. カスタム アプリの manifest.json を更新する。

今回は上記に沿って実装していきます。

ちなみに今回の環境は以下の通りです。

> docker --version
Docker version 28.3.2, build 578ccf6

0. カスタム アプリを作成する。

上記で 3 つといいましたが、そもそも SSO を実装するためのカスタム アプリが無いのでここから作成します。
とはいえ、ほぼ Microsoft 365 Agent Toolkit (旧: Teams Toolkit) でポチポチするだけの作業なので簡単です。(今回は docker 上で動作、 devtunnel で公開するため、少し手順が増えます。)

0-1. カスタム アプリのひな形を作成する。

  1. VSCode を開いて Microsoft 365 Agent Toolkit を選択します。
  2. [Create a New Agent/App] をクリックします。

  1. [Teams App] をクリックします。

  1. [Tab] をクリックします。

  1. 作成するタブ アプリの形式を選択します。
    ※ どれを選んでも OK です。
    ※ 私は今も変わらず React 信者なので [React with Fluent UI] を選びたいのですが、今回も簡単のために [Basic Tab] を選択します。

  1. 作成するタブ アプリで使用する言語を選択します。
    ※ どっちでもよいですが今回も [TypeScript] を選択します。

  1. 作成するアプリを保存する場所を選択します。
  2. 最後に作成するアプリの名前を入力します。
  3. アプリのひな形を作成してアプリのフォルダを VSCode で開いてくれます。

名前が変わっただけでやはり Microsoft 365 Agent Toolkit (旧 Teams Toolkit) はポチポチするだけでひな形作ってくれるの本当に楽ですね。
星 5 です。

動作テスト①

やっぱりちょこちょこ動作テストをするのは大事です。
とはいえ今回は docker 上で動作させるため、ホストマシン上には nodejs / npm が必要ない状況です。
もし、ローカルに nodejs / npm がある場合には以下のコマンドを入力してテストできます。

npm install
npm run build
npm run start

その後、 Express server listening on port 3333 が表示された後に http://localhost:3333 にアクセスして以下のページが表示されれば OK です。

0-2. Docker 上で動かせるようにしてみる。

ここではほんの少しだけ少しコードを書きます。

ルートに以下の docker-compose.yml を作成します。

services:
  app:
    image: node:22-alpine
    working_dir: /app
    ports:
      - "3333:3333"
    volumes:
      - .:/app
      - /app/node_modules
    command: sh -c "npm install && npm run build && npm start"

以上です。

動作テスト②

以下のコマンドで docker を起動します。

docker compose up

※ この時 Windows ユーザーの方は Docker Desktop を起動するのを忘れないでください (1 敗)

その後、 app-1 | Express server listening on port 3333 が表示された後に http://localhost:3333 にアクセスして以下のページが表示されれば OK です。

0-3. アプリケーション ID を決めておく

[前回](https://zenn.dev/yuta331/articles/6f8©26ee1e3) は Toolkit パワーでアプリケーション ID が決まっていましたが、今回は全部ローカルで動かすためまだアプリケーション ID が決まっていません。
そのため、自分でアプリケーション ID を決める必要があります。

今回はローカルで動かすところまでのため、適当に GUID を生成して使用しましょう。
PowerShell にて以下のように New-Guid コマンドレットで作成可能です。

New-Guid
# Guid
# ----
# a69023d1-e4ac-45fb-a793-18ef95f62ceb

この a69... をアプリケーション ID として env/.env.local に以下のように記載します。

# ...
TEAMS_APP_ID=a69023d1-e4ac-45fb-a793-18ef95f62ceb
# ...

0-4. devtunnel を起動しておく。

今回は全て devtunnel を使用して完全ローカルで完結させます。
そのため、あらかじめ使用するポートについては devtunnel を起動してポートを開放しておきます。

今回はカスタムアプリでポート 3333 を使用するため、以下の様なコマンドを実行し、ポートを開放します。(セキュリティ的にあまりよくないですが、簡単のために匿名アクセスを許可した状態で作成しています。)

# devtunnel にログインします。(github アカウントでログインする場合には `-g` オプションを付けてください。
devtunnel user login

# 3333 のポートを開放する devtunnel を作成します。
devtunnel create -a
devtunnel port create -p 3333

# devtunnel をホストして 3333 を開放します。
devtunnel host

以下の様な表示になれば OK です。

env/.env.local ファイルに以下の通りポート 3333 につながる URL とそのドメインを記載しておきましょう。

...
TAB_DOMAIN=***.asse.devtunnels.ms
TAB_ENDPOINT=https://***.asse.devtunnels.ms
...
動作テスト③

ngrok を起動した状態で以下のコマンドで docker を起動します。

docker compose up

この状態で http://localhost:3333 につながっている URL にアクセスし、[Continue] をクリックします。
※ スクリーンショットの場合、 Hosting port:3333 の下に記載されている https://... です。

以下の画面が表示されれば OK です。

1. Entra ID でアプリケーションを構成する。

Entra ID 上のアプリケーションを構成していきます。
ココは以前の記事と同じです。

1-1. アプリケーションの登録

まずは Entra ID へ、アプリケーションを登録します。
ここは UI 上でポチポチするだけです。

  1. Entra 管理センターにアクセスします。
  2. 左側の [Entra ID] より [アプリの登録] をクリックします。
  3. 左上の [新規登録] をクリックします。

  1. アプリケーションの名前を入力して [登録] をクリックします。
    ※ 今回も簡単のためにシングル テナントで使用するアプリとしています。

  1. [概要] に表示されている "アプリケーション (クライアント) ID" を env/.env.local に保存します。

...
ENTRA_APP_ID=********-****-****-****-************
...

1-2. アプリケーションの構成

次は登録したアプリケーションでカスタム アプリが SSO を使用できるように構成します。
ほぼポチポチ タイムです。

https://learn.microsoft.com/ja-jp/microsoftteams/platform/tabs/how-to/authentication/tab-sso-register-aad

  1. 左側の "管理" セクション内 [API の公開] をクリックします。
  2. "アプリケーション ID の URI" の右側の [追加] をクリックします。

  1. アプリケーション ID URI を以下の通りに変更押し、 [保存] をクリックします。
    ※ 事前に api://<アプリケーション ID> と入力されているので、 api://<アプリケーション ID> の間に 0-4 で env/.env.local に記載したドメインを入力してください。

  1. [Scope の追加] をクリックします。

  1. 構成の詳細を入力し、[スコープの追加] をクリックします。

  1. [クライアント アプリケーションの追加] をクリックします。

  1. クライアント ID に使用したいクライアントのクライアント ID を入力します。(両方使用したい場合は 6. ~ 9. を繰り返してください。)
  • 1fec8e78-bce4-4aaf-ab1b-5451cc387264: Teams デスクトップ クライアント、Teams モバイル
  • 5e3ce6c0-2b1f-4285-8d4b-75ee78787346: Teams web クライアント
  1. "承認済みのスコープ" にて 4. ~ 5. で作成したスコープを選択します。
  2. [アプリケーションの追加] をクリックします。

2. カスタム アプリ内にてトークンの取得をするコードを追加する。

カスタム アプリ内で getAuthToken() を実行することでトークン (JWT) を取得できます。
src/static/scripts/m365agents.ts を以下のように編集します。

import { app, authentication } from "@microsoft/teams-js";

(function () {
  "use strict";

  // Call the initialize API first
  app.initialize().then(() => {
    authentication
      .getAuthToken()
      .then((token: string) => {
        // Use the token for authentication or API calls
        console.log("Authentication token received:", token);
        updateHubState(token);
      })
      .catch((error: any) => {
        updateHubState("Error: Unable to get authentication token");
        console.error("Error getting authentication token:", error);
      });
  });

  function updateHubState(hubName: string) {
    if (hubName) {
      const hubStateElement = document.getElementById("hubState");
      if (hubStateElement) {
        hubStateElement.innerHTML = "in " + hubName;
      }
    }
  }

  // Notify success when the DOM content is fully loaded
  document.addEventListener("DOMContentLoaded", () => {
    app.notifySuccess();
  });
})();

3. カスタム アプリの manifest.json を更新する。

Teams のアプリとして使用した際に getAuthToken() が実行できるよう manifest.json の内容を更新、およびパッケージを作成します。

  1. appPackage/manifest.json に以下の webApplicationInfo を追記します。
{
    // 省略
    "webApplicationInfo": {
        "id": "${{ENTRA_APP_ID}}",
        "resource": "api://${{TAB_DOMAIN}}/${{ENTRA_APP_ID}}",
    }
}
  1. VScode 上の Microsoft 365 Agents Toolkit から、 "UTILITY" セクションの "Zip App Package" をクリックします。

  1. "manifest.json" をクリックします。

  1. "local" をクリックします。

これで appPackage/build 内に appPackage.local.zip というアプリ パッケージが作成されます。

実際に Teams 上で動作させてみる

作成した appPackage.local.zip を Teams 上で読み込むことで今回作成したアプリにアクセスすることが可能です。
今回はブラウザ上の Teams (Teams web クライアント) でアクセスしてみましょう。

  1. docker compose up でカスタム アプリを起動します。
  2. Teams web クライアント (https://teams.microsoft.com/) にアクセスします。
  3. 左側の [アプリ] より、下部の [アプリを管理] をクリックします。
  4. [アプリのアップロード] をクリックします。

  1. [カスタム アプリをアップロード] をクリックし、作成した appPackage.local.zip を選択します。

  1. [追加] をクリックします。

  1. 正常に追加されたら、 [開く] をクリックします。

  1. 以下のように表示されれば、正常にトークンが取得できています。
    ※ 今回は簡単のために画面にトークンを表示させていますが、実際に使う際には表示させないようにしてください。

このトークンは何なのか

このトークンはこちらに記載されている Entra ID の アクセス トークン というトークンです。

https://learn.microsoft.com/ja-jp/entra/identity-platform/access-tokens

このアクセス トークンはユーザーが認証済みのユーザーに代わって特定のリソースへのアクセスを許可する、認可用の JWT という形式で作成されたユーザーの情報などが詰まっています。
JWT は記載された内容を base64 という形式でエンコードされているため、 base64 でデコードすると内容を確認できます。
今回は以下のサイトを使用してデコードして中身を見てみます。

https://www.jwt.io/ja

デコードしてみると "デコードされたペイロード" から次のような情報を確認できます。

{
  ...
  "family_name": "<Teams にサインインしているユーザーの姓>",
  "given_name": "<Teams にサインインしているユーザーの名>",
  ...
  "name": "<Teams にサインインしているユーザーの表示名>",
  "oid": "<Teams にサインインしているユーザーの ID>",
  ...
  "tid": "<Teams にサインインしているユーザーのテナント ID>",
  ...
  "upn": "<Teams にサインインしているユーザーの UPN>",
}

この内容を見てわかる通り、 Teams にサインインしているユーザーの認証情報を Teams カスタム アプリに渡すことができています。

これで何ができるのか

取得したアクセス トークンを用いて、特定のリソースへのアクセスを許可するよう実装をすることで、 ユーザーは追加のサインインなしでアクセス可能になります。

getAuthToken() について少し詳しく

もう少し深く実際の動作についてみてみましょう。どうやってサインインせずに認証情報を取得しているのでしょうか。
実際の HTTP Archive (HAR) を見てみましょう。
ブラウザ上の Teams にアクセスした状態で devtools の network を開きます。
この状態で作成したカスタム アプリを開き、HAR を見てみましょう。
すると https://login.microsoftonline.com/<tenantId>/oauth2/v2.0/token の様な URL へ POST 要求をしていることが分かります。

Payload タブの中を見てみましょう。

grant_type を見てみると refresh_token となっており、リフレッシュトークンを使用してアクセス トークンを取得していることが分かります。

まとめ

今回は Teams カスタム アプリに SSO を実装し、少しだけ SSO について深堀しました。
最近になって Teams Toolkit の名称が Microsoft 365 Agents Toolkit に変わったので作り方も変わったのかもしれないしまた書こうかなという見切り発車で書き始めました。
が、そこまでやることは変わっていませんでした。

とはいえ、前回の記事では Envoy や Microsoft Graph 用のトークンとの交換まで実装していたので、今回はより小さい単位でまとめられた記事になっているかと思います。

Graph 用のトークンとの交換についてはこちらの記事にまとめました。
https://zenn.dev/microsoft/articles/da4b43e1373ab7

引用まとめ

https://zenn.dev/yuta331/articles/6f8c26ee1e3208
https://learn.microsoft.com/ja-jp/microsoftteams/platform/concepts/authentication/authentication
https://learn.microsoft.com/ja-jp/microsoftteams/platform/tabs/how-to/authentication/tab-sso-overview#sso-in-teams-at-runtime
https://learn.microsoft.com/en-us/microsoftteams/platform/toolkit/develop-single-sign-on-experience-in-teams?tabs=tab-app%2Cbot

Microsoft (有志)

Discussion