🆔

[Teams][Envoy][Entra ID] Teams カスタム アプリで SSO を実装してみた

2024/11/09に公開

概要

以下の様な構成で SSO を実装した Teams のカスタム アプリを作ってみるお話です。
Envoy を使った JWT の検証も行います。
また、今回はバックエンド側については Docker と ngrok を使用して、全てローカル上で動作させることを目指します。

今回使用する環境

  • VSCode (Visual Studio Code)
  • Teams Toolkit (VSCode の拡張機能)
  • Docker

構成内容

1. Teams カスタム アプリ

https://learn.microsoft.com/ja-jp/microsoftteams/teams-custom-app-policies-and-settings

Teams アプリは、重要な情報、共通のツール、信頼できるプロセスを提供することで、コラボレーション ワークスペースの生産性を向上させ、ユーザーが集まり、学習し、仕事をする場を増やします。
アプリは、要件に合わせて Teams プラットフォームの機能を拡張する方法です。
新しいアプリを作成するか、既存のアプリを統合して、特定のビジネス ニーズに合わせて Teams プラットフォームの利点を活用します。

Teams アプリは Teams 内で動作するアプリケーションです。
今回は、この Teams アプリ内で SSO を実施し、組織内のリソースにアクセスすることを目指します。

2. Envoy

https://www.envoyproxy.io/

Envoy is an open source edge and service proxy, designed for cloud-native applications

Envoy はクラウド ネイティブなアプリケーションのために設計されたエッジおよびサービス プロキシです。(直訳)

今回はこの Envoy の機能の一つである HTTP Filter を使用して JWT を検証します。
HTTP Filter の設定の JWT Authentication を使用します。

Envoy を使用して JWT の検証を行うとサーバー側で書くべきコードをカットできます。
今回は実装するエンドポイントが少ないため、恩恵が少ないですが、各エンドポイントに JWT の検証コードを書くことになるともう腱鞘炎モノです。(コピペ連打)
また、 書くべき検証部分を書き漏らしたりするとそのエンドポイントだけセキュリティ強度が下がってしまう場合もあります。

JWT が検証済みである前提で API の実装が出来たりと、コードを書く際のセキュリティ面での心配事を減らすことが可能です。

3. Entra ID

https://learn.microsoft.com/ja-jp/entra/fundamentals/whatis

Microsoft Entra ID は、従業員が外部リソースへのアクセスに使用できる、クラウドベースの ID およびアクセス管理サービスです。

Entra ID は Microsoft が提供するクラウド ベースのアイデンティティ (ID) およびアクセスの管理サービスです。
元々は Azure Active Directory という名称だったサービスですね。
名称変更の詳細についてはこちらを参照してみてください。

Entra を使用することで、認証や認可の処理実装の手間を省くことができます。

4. Microsoft Graph

https://learn.microsoft.com/ja-jp/graph/overview

Microsoft Graph は、Microsoft 365 のデータとインテリジェンスへの入り口です。

Microsoft Graph を使用することで Microsoft 365 に保存されているデータ等にアクセスすることができます。
例えば、ユーザーの情報であったり、 Teams のチームの情報等ですね。
Teams アプリのバックエンド サーバーから任意のデータにアクセスする構成でもよいのですが、今回は Microsoft Graph でユーザーの情報を持ってくる構成にします。

今回実装する構成

上記の技術を用いて今回は以下の様な構成で実装します。

  1. Teams アプリから Entra ID に認証のリクエストを行います。
  2. Entra ID からアクセストークン (JWT) が返却されます。
  3. アクセストークンを付けてバックエンドへリクエストを送信します。
  4. Envoy と Entra ID にてトークンの検証を行います。
  5. トークンの検証に成功すればリクエストをバックエンド サーバーに転送します。
  6. バックエンド サーバーから Microsoft Graph を介して Microsoft 365 のデータを取得します。
  7. バックエンド サーバーから Teams アプリに返答を返します。

実装していきます

きっと皆さんも実装したくてウズウズしていると思うので、今回実装するものの説明はここまでにします。
実際に実装していきましょう。

1. Teams カスタム アプリを作成する

Teams カスタム アプリを作成する場合は、 Teams Toolkit を使用するとすごい簡単にひな形が作成できます。

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

  1. 作成するアプリを選択します。
    ※ どれを選んでも OK ですが、今回は [Tab] を選択してタブ アプリを作成します。

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

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

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

UI 上の操作のみでひな形の作成まで実施してくれるのはとても楽でいいですね🤩
星 5 です。

動作テスト ①

ちょこちょこ動作テストするのはとても大事です。
この状態で以下のコマンドを入力します。

npm install
npm run build
npm run start

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

2. 作成した Teams カスタム アプリをデプロイする

作成した Teams アプリを実際に Teams 上で使用するために Azure App Service 等にデプロイします。
これも Teams Toolkit のおかげで VSCode 上で UI ポチポチするだけで OK です。

  1. "ACCOUNTS" 内の Microsoft 365 account と Azure アカウントの両方にサインインします。


※ Azure アカウントに課金される場合があるのでご注意ください。

  1. "LIFECYCLE" 内の [Provision] をクリックします。

  2. 使用する環境として dev を選択します。

  3. サブスクリプション、リソース グループを選択します。
    ※ 必要に応じてリソースグループ等を作成してください。

  4. 課金されるかもだけど OK? という趣旨のメッセージが出るので [Provision] をクリックします。

  5. 右下の表示が完了になるまで待ちます。

  1. "LIFECYCLE" 内の [Deploy] をクリックします。

これでもうデプロイが終了しています。本当に Teams Toolkit 様様ですね。

動作テスト ②

こんな簡単な操作で本当にデプロイできているか確認しておきましょう。
appPackage/build/manifest.dev.jsonstaticTabs[0].websiteUrl にアクセスしてみてください。
動作テスト ① で表示されたページと同じページが表示されれば OK です。

3. Microsoft Entra ID の構成

Entra ID を使用して SSO をする場合には Entra ID にアプリケーションを事前に登録して、構成する必要があります。

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

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

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

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

3.2. アプリケーションの構成

では、登録したアプリケーションを使用して Teams カスタム アプリが SSO 出来るようにアプリケーションを構成しましょう。
若干手順が増えますが、また UI 上でポチポチします。

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

3.2.1. API の公開

  1. Entra 管理センターにて先程作成したアプリケーションを開きます。
  2. 左側の "管理" セクション内 [API の公開] をクリックします。
  3. "アプリケーション ID の URI" の右側の [追加] をクリックします。

  1. アプリケーション ID URI を以下の通りに変更し、[保存] をクリックします。
    ※ 事前に api://<アプリケーション ID> で入力されているので、api://<アプリケーション ID> の間に動作テスト ② でアクセスした URL のドメインを入力してください。
    (appPackage/build/manifest.dev.jsonstaticTabs[0].websiteUrl のドメインです。)

api://<staticTabs[0].websiteUrl ② のドメイン>/<アプリケーションID>

3.2.2. API のスコープを構成する

  1. [API の公開] 内にて、[Scope の追加] をクリックします。

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

3.2.3. 承認されたクライアント アプリケーションを構成する

以下の 2 つのクライアント ID について手順を実行します。

  • 1fec8e78-bce4-4aaf-ab1b-5451cc387264: Teams デスクトップ クライアント、Teams モバイル
  • 5e3ce6c0-2b1f-4285-8d4b-75ee78787346: Teams web クライアント
  1. [API の公開] 内にて、[クライアント アプリケーションの追加] をクリックします。

  1. クライアント ID に上記のクライアント ID を入力します。
  2. "承認済みのスコープ" にて 3.2.2 で作成したスコープを選択します。
  3. [アプリケーションの追加] をクリックします。

3.2.4. API のアクセス許可を構成する

今回は Microsoft Graph の /me にアクセスするだけなので、最初から入っている User.Read のアクセス許可だけで OK です。
[<組織名> に管理者の同意を与えます] をクリックするだけ OK です。

4. Teams カスタム アプリで SSO を有効にする

それでは、Teams カスタム アプリ内に SSO を実施するコードを記載していきましょう。

4.1. カスタム アプリ内に SSO を有効にするコードを追加する

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

Teams カスタム アプリ内で SSO を有効にするコードを追加します。
といっても追加するコードはほんの少しで OK です。
SSO を有効にするだけであれば、 microsoftTeams.app.initialize() 後に microsoftTeams.authentication.getAuthToken() を実行するだけで OK です。
これで、Teams カスタム アプリ内で SSO を行い、アクセス トークン (JWT) を受け取ることができます。

/src/static/scripts/teamsapp.js を以下のように変更します。

(() => {
  "use strict";

  // Call the initialize API first
  microsoftTeams.app.initialize().then(() => {
    microsoftTeams.authentication
      .getAuthToken()
      .then((token) => {
        // Do something with the token.
        console.log(token);
      })
      .catch((e) => {
        console.error(e);
      });
  });
})();

4.2. アプリ マニフェストを更新する

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

Teams カスタム アプリではアプリに設定等をアプリ マニフェストファイルにまとめています。
今回もアプリケーションの ID 等々アプリ マニフェスト ファイルに記入する必要があります。
しかし、 Teams Toolkit 君のおかげで環境変数ファイル (env/.env.dev 等) に ID 等を書くことで zip ファイルを生成するときによしなに読み込んでくれます。

  1. 以下の通りに .env.dev に以下を追記します。
# 省略
ENTRA_APP_ID=<アプリケーション ID>
  1. webApplicationInfo を追加します。
// 省略
    "webApplicationInfo": {
        "id": "${{ENTRA_APP_ID}}",
        "resource": "api://${{TAB_DOMAIN}}/${{ENTRA_APP_ID}}"
    }
}

4.3. Teams カスタム アプリを動かしてみる。

さて、ここまでで一旦 Teams カスタム アプリ側で SSO が有効になっており、アクセス トークンを取得できるようになっているはずです。
それを確かめてみましょう。

4.3.1. appPackage.zip を作成する。

  1. Teams Toolkit で "UTILITY" > [Zip Teams App Package] をクリックします。
  2. manifest.json を選択します。

  1. dev を選択します。
  2. Temas package is successfully built at local address が表示されれば OK です。
    appPackage/build 内に appPackage.dev.zip が生成されているはずです。

4.3.2. Teams から appPackage.zip を読み込む

先程作成した appPacakage.dev.zip を Teams から読み込んで Teams カスタム アプリを起動してみましょう。
今回はブラウザで開きます。

  1. Teams web クライアント (https://teams.microsoft.com/) にアクセスします。
  2. 左側の [アプリ] より、下部の [アプリを管理] をクリックします。

  1. 上部の [アプリをアップロード] をクリックします。

  1. [カスタム アプリをアップロード] をクリックし、appPacakage.dev.zip をアップロードします。

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

  1. 以下の様なページが表示されればカスタム アプリの追加は OK です!

動作テスト ③

ちゃんとアクセス トークンが取れているか (SSO が有効になっているか) を確認します。

  1. 追加したカスタム アプリを開きます。
  2. コンソールを開きます。(Microsoft Edge なら Ctrl + Shift + I で開きます。)
  3. コンソールにアクセス トークンが表示されていれば OK です!

5. バックエンド側を作成する

ではでは、ここからバックエンド側を作成します。
バックエンド側では以下の手順にて Teams カスタム アプリからもらった request を処理します。

  1. Envoy にて JWT の検証を行う
  2. バックエンド サーバーにてアクセス トークンをサーバー側のトークンに交換します。
  3. サーバー側のトークンを用いて Microsoft Graph でユーザーの情報を持ってきます。
  4. 取得した情報を Teams カスタム アプリに返します。

5.1. Envoy で JWT の検証を行う

Envoy はコンテナ上で動作しますので、docker-compose.yaml を作成し、編集する感じで実装できます。

5.1.1. Envoy のコンテナを作成する

docker-compose.yaml を作成し、以下のように記載します。

version: "3"
services:
  envoy:
    image: envoyproxy/envoy:v1.32.0
    volumes:
      - ./envoy.yaml:/etc/envoy.yaml
    ports:
      - "8080:8080"
      - "9901:9901"
    command: ["-c", "/etc/envoy.yaml", "--service-cluster", "front-proxy"]

以上です。(簡単すぎる。。。)

5.1.2. Envoy の設定を行う

Envoy では yaml ファイルにその設定を記載することができます。
コンテナを用意しただけだとダメなんですね。ちゃんと設定しておきましょう。
envoy.yaml に以下のように記載します。
テナント IDアプリケーション ID は Entra ID のアプリケーションより確認可能です。

admin:
  address:
    socket_address:
      address: 0.0.0.0
      port_value: 9901

static_resources:
  listeners:
    - address:
        socket_address:
          address: 0.0.0.0
          port_value: 8080
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                codec_type: AUTO
                stat_prefix: ingress_http
                route_config:
                  name: backend_server
                  virtual_hosts:
                    - name: backend_server_hosts
                      domains:
                        - "*"
                      routes:
                        - match:
                            prefix: /
                          route:
                            cluster: backend-server-service
                            timeout: 15s
                            cors:
                              allow_origin_string_match:
                                - safe_regex:
                                    google_re2: {}
                                    regex: \*
                              allow_methods: "GET"
                        - match:
                            prefix: /getProfile
                          route:
                            cluster: backend-server-service
                            timeout: 15s
                            cors:
                              allow_origin_string_match:
                                - contains: <staticTabs[0].websiteUrl のドメイン>
                              allow_methods: "POST"
                http_filters:
                  - name: envoy.filters.http.jwt_authn
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
                      providers:
                        entra:
                          issuer: "https://login.microsoftonline.com/<テナント ID>/v2.0/"
                          audiences:
                            - "api://<staticTabs[0].websiteUrl のドメイン>/<アプリケーション ID>"
                          remote_jwks:
                            http_uri:
                              uri: "https://login.microsoftonline.com/<テナント ID>/discovery/v2.0/keys"
                              cluster: entra
                              timeout: 5s
                      rules:
                        - match:
                            prefix: /getProfile
                            headers:
                              - name: ":method"
                                string_match: { exact: "POST" }
                          requires:
                            provider_name: entra
                  - name: envoy.filters.http.router
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

  clusters:
    - name: backend-server-service
      type: LOGICAL_DNS
      dns_lookup_family: V4_ONLY
      load_assignment:
        cluster_name: backend-server-service
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: backend
                      port_value: 5000
    - name: entra
      type: LOGICAL_DNS
      dns_lookup_family: V4_ONLY
      load_assignment:
        cluster_name: entra
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: login.microsoftonline.com
                      port_value: 443
      transport_socket:
        name: envoy.transport_sockets.tls
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext

以上です。(内容はちょっと難しい。)

動作テスト ④

envoy が正常に動作しているかテストします。
以下のコマンドを実行します。

docker compose up

all dependencies initialized. starting workers と表示されれば OK です。

5.2. バックエンド サーバーを実装します

バックエンド サーバーは、Envoy 経由で検証済みのアクセス トークンを受け取り、サーバー用のトークンと交換し、 Microsoft Graph でユーザーの情報を持ってきます。
今回は簡単に express で実装しようと思います。

  1. まずは必要なパッケージをインストールしておきましょう。
npm install express @azure/msal-node
npm install -D @types/express

5.2.1. 必要な API の設計

今回のバックエンド サーバーは以下 2 つの API を実装します。

  • 接続テスト用の GET /: Hello world を返します。
  • 実際に使用する POST /getProfile: ユーザーのプロファイルを返します。

GET / はおいておいて POST /getProfile の使い方を考えましょう。
POST /getProfile では以下の処理を行います。

  1. アクセス トークンを受け取る
  2. アクセス トークンをサーバー側トークンと交換する
  3. サーバー側トークンを使用して Microsoft Graph でユーザーのプロファイルを取得する
  4. 取得したユーザーのプロファイルを Teams カスタム アプリに返す。

この内、2. のアクセス トークンとサーバー側トークンとの交換は @azure/msal-nodeaquireTokenOnBehalfOf() を使用します。
この関数はざっくり言うと、1. アクセス トークン、2. テナント ID、3. スコープを渡すと Microsoft Graph に使用できるトークンを返してくれます。

以上より、POST /getProfile では、 Teams カスタム アプリから 1. アクセス トークンを Teams カスタム アプリからもらえばよさそうです。
※ スコープは取得するトークンに付与するスコープなので、バックエンド サーバー側で定義して OK です。
※ テナント ID は実はアクセス トークン内に入っているのでそれを使用すれば OK です。

5.2.2. クライアント シークレットの発行

トークンの交換のためにクライアント シークレットが必要なので Entra ID のアプリケーションにて新しいクライアント シークレットを発行します。

  1. 左側の [証明書とシークレット] をクリックします。

  2. [新しいクライアント シークレット] をクリックします。

  3. "説明" と "有効期限" を入力し、 [追加] をクリックします。

  1. "値" をコピーし、env/.env.dev に以下のように追記する。
# 省略
ENTRA_CLIENT_SECRET=<コピーした値>

5.2.3. API の実装

server/index.ts に以下のように記述します。

import express, { Express, NextFunction, Request, Response } from "express";
import msal from "@azure/msal-node";

const app: Express = express();
const port: number = 5000;

app.use(express.json());
app.use((req: Request, res: Response, next: NextFunction) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE");
  res.header(
    "Access-Control-Allow-Headers",
    "Content-Type, Authorization, access_token"
  );
  if ("OPTIONS" === req.method) {
    res.sendStatus(200);
  } else {
    next();
  }
});

app.get("/", (req: Request, res: Response) => {
  res.send("Hello World");
});

app.post("/getProfile", (req: Request, res: Response) => {
  const token = req.body.token;
  const decodedJWT = decodeJWT(token);
  const tenantId = decodedJWT.tid;
  const msalClinet = new msal.ConfidentialClientApplication({
    auth: {
      clientId: process.env.ENTRA_APP_ID,
      authority: `https://login.microsoftonline.com/${tenantId}`,
      clientSecret: process.env.ENTRA_CLIENT_SECRET,
    },
  });
  const scopes = ["https://graph.microsoft.com/User.Read"];
  (async () => {
    const result = await msalClinet.acquireTokenOnBehalfOf({
      authority: `https://login.microsoftonline.com/${tenantId}`,
      oboAssertion: token,
      scopes: scopes,
      skipCache: false,
    });
    const graphResult = await fetch("https://graph.microsoft.com/v1.0/me", {
      method: "GET",
      headers: {
        accept: "application/json",
        authorization: `Bearer ${result.accessToken}`,
      },
      cache: "default",
    });
    const profile = await graphResult.json();
    res.status(200).json(profile);
  })();
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

const decodeJWT = (token: string) => {
  const [, payload] = token.split(".");
  const buff = Buffer.from(payload, "base64");
  const payloadDecoded = buff.toString("utf-8");
  return JSON.parse(payloadDecoded);
};

また、実行コマンドを package.json に登録しておきましょう。

// 省略
"scripts": {
    // 省略
    "start:backend": "ts-node server/index.ts"
  },
// 省略

5.2.4. コンテナの設定

コンテナの設定は docker-compose.yaml に以下を追記すれば OK です。
(本当にコンテナを使って開発するのは楽でいいですね...)

# 省略
  backend:
    image: node:18.16
    volumes:
      - .:/api
    working_dir: /api
    command: >
      bash -c "npm install && 
      npm run start:backend"
    ports:
      - "5000:5000"
    tty: true
    env_file:
      - ./env/.env.dev

ここまでで、バックエンド側は完成です!
最後に docker compose up して起動しておきましょう。

5.2.5 ngrok を使用してローカルで動かしてみる。

今回は ngrok を使用してバックエンド側はローカルで動かします。
ここで一旦 ngrok を使用してローカルで動作するか確認しておきましょう。

どんな方法で実施しても OK ですが、私が実施している方法を以下に記載しておきます。

  1. ローカルで ngrok を起動します。
  2. ngrok にアクセスしてログインします。
  3. ダッシュボードに記載されているコマンドを参考に以下のコマンドを ngrok 内で実行します。
ngrok config add-authtoken <ngrok の認証トークン>
ngrok http http://localhost:8080
  1. この状態で ngrok で生成された URL にアクセスします。

  1. [Visit Site] をクリックします。
  2. Hello world と表示されれば OK です!

ココで生成された ngrok の URL に対して Teams カスタム アプリからアクセスします。

5.3. Teams カスタム アプリからバックエンド側にアクセスする

最後にカスタム アプリ側からバックエンドにアクセスするためのコードを追加しましょう。
カスタム アプリから見た場合、単純にバックエンド側に /getProfile 宛てに POST 要求を送れば OK です。
5.2.1 にも記載した通り、 header と body にアクセス トークンを含めて POST 要求を送る感じですね。
※ テナント ID はアクセス トークンをデコードすると tid に記載されているのでそれを使います。

src\static\scripts\teamsapp.js を以下のように書き換えましょう。

(() => {
  "use strict";

  // Call the initialize API first
  microsoftTeams.app.initialize().then(() => {
    microsoftTeams.authentication
      .getAuthToken()
      .then((token) => {
        // Do something with the token.
        console.log(token);
        fetch("https://6f2b-126-19-120-213.ngrok-free.app/getProfile", {
          method: "POST",
          mode: "cors",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${token}`,
          },
          body: JSON.stringify({
            token: token,
          }),
        })
          .then((response) => {
            console.log(response);
            response
              .json()
              .then((data) => {
                document.getElementById("displayName").innerText =
                  data.displayName + "'s";
              })
              .catch((e) => {
                console.error(e);
              });
          })
          .catch((e) => {
            console.error(e);
          });
      })
      .catch((e) => {
        console.error(e);
      });
  });
})();

せっかくなので取得した Display Name を表示させるように src\static\views\hello.html を変更しましょう。

<!-- 省略 -->
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div>
      <h1>Hello, World</h1>
      <span>
        <p><span id="displayName">Your</span> app is running</p>
      </span>
    </div>
    <script type="text/javascript">
      microsoftTeams.appInitialization.notifySuccess();
    </script>
  </body>
</html>

この状態で再度デプロイします。
※ "LIFECYCLE" 内の [Deploy] をクリックします。

これで全て完成です!

動作検証

それでは完成したので動作を見ていきましょう。
今回はせっかく JWT の検証まで行っているので通常通りアクセスした場合と不正な JWT を使用した場合の 2 通りでアクセスしてみたいと思います。

1. 通常通りアクセスした場合

まずは通常通りアクセスした場合についてみてみましょう。
docker compose up でバックエンド側を起動し、作成した Teams カスタム アプリにアクセスします。
すると、最初は Your app is running という表示だったのが...

少しするとユーザーの情報を持ってくることができ、 <ユーザーの表示名>'s app is runnig に変化します!

2. 不正な JWT を使用してアクセスした場合

では、JWT を改ざんしてバックエンド側にアクセスするとどうなるでしょうか。
まず、通常通りアクセスすると JWT がコンソールに出てくるはずです。
これを jwt.io 等でデコードしてみましょう。

デコードした結果を一部変更して再度エンコードして不正な JWT を作成します。
※ 再度エンコードする際には Private Key が必要になるため、ssh-keygen 等で適宜準備してください。

作成した不正な JWT を使って curl でバックエンド側にアクセスしてみます。

$headers = @{}
$headers["Authorization"] = "Bearer <作成した不正な JWT>"
curl -H $headers -Uri  <ngrok で生成したバックエンドの URL>/getProfile -Method POST

すると、Jwt verification fails と JWT の検証でこけていることが分かります。

また、 ngrok のログを見ても 401 Unauthorized ではじかれていることが分かります。

Envoy のログも見てみる

docker-compose.yaml 内の services.envoy.command を以下の様に書き換えると envoy のログが見れます。

# 省略
    command:
      [
        "-c",
        "/etc/envoy.yaml",
        "--service-cluster",
        "front-proxy",
        "--log-level",
        "debug",
      ]

この状態で不正な JWT を用いてアクセスすると...

entra: JWT token verification completed with: Jwt verification fails

こんな感じでしっかり JWT の検証に失敗していることが記載されています。

今回のリポジトリ

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

まとめ

今回は、Envoy を使用して JWT の検証を行う SSO を実装した Teams のカスタム アプリを作成しました。
アプリケーション側で書くコードが減るのは素晴らしいですね。。。
実は Envoy を使用して CORS の設定もできたりしちゃったりするんですよね。(今回もすこし入れてたりします。)
すごいコードの削減になります。

参考文献

https://zenn.dev/hebo4096/articles/3848e1d1eebaba
https://learn.microsoft.com/ja-jp/microsoftteams/platform/toolkit/add-single-sign-on
https://learn.microsoft.com/ja-jp/microsoftteams/platform/tabs/how-to/authentication/tab-sso-register-aad
https://qiita.com/raiich/items/6822e5087d226e2eb8ea

Discussion