3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Application Gateway x Entra ID x SPA を試す

Last updated at Posted at 2025-11-27

Ignite 2025 にて Application Gateway の JWT Validation がパブリックプレビューとして出てきました。これにより Application Gateway にて Microsoft Entra ID によって発行された JSON Web Token (JWT) を検証してバックエンドへのリクエストの転送を許可、拒否できるようになります。
curl による簡易な検証と静的アプリを使ったユーザー認証の両方を試して、ハマりどころや気になる点をまとめてみましたので、ご利用の際はご一読いただけると幸いです。

構成図

image.png

前提

  • Application Gateway v2 をデプロイ済み
  • バックエンド用の Web サーバー (VM) を構築済み
  • Application Gateway にてルールを構成済みでブラウザ経由で https にて Application Gateway にアクセス可能
  • 外部からアクセス可能 (ファイアウォール制限のない) な Blob Storage が作成済みであること

検証1 (curl で動作確認)

Entra アプリの作成

Entra の画面からアプリケーションの登録を行います。ここで作成するアプリ (API) により、適切な JWT を発行できるようになり、Application Gateway 側も “このクライアント ID” から発行されたトークンの場合は許可するといったことができるようになります。

Microsoft Entra ID の画面から "アプリの登録"-> "新規作成" をクリックします。
image.png

"名前" を入力し、"この組織ディレクトリのみに含まれるアカウント" を選択して、"登録" をクリックします。
image.png

作成されたアプリの "アプリケーション (クライアント) ID" と "ディレクトリ (テナント) ID" をコピーしておきます。
image.png

"API の公開" -> "アプリケーションの URI" の "追加" をクリックし、"api://<クライアント ID>" が表示されるので、そのまま保存します。
image.png

"API の公開" -> "Scope の追加" をクリックし、以下のような形で入力して保存します。

項目
スコープ名 access_as_user
同意できるのはだれですか? 管理者とユーザー
管理者の同意の表示名 Access MyWebAPI
管理者の同意の説明 Allow the app to access MyWebAPI

image.png

シークレットの作成 (オプション)

動作確認として、このアプリに対して curl コマンドでトークンを取得できるようにシークレットの設定も行います。ユーザー認証を使う場合はシークレットは不要です。

"証明書とシークレット" -> "新しいクライアントシークレット" をクリックし、"説明" を入力して "追加" をクリックします。
image.png

作成されたシークレットの値をコピーしておきます。
image.png

Application Gateway の設定

公開情報にある通り App Gateway JWT 構成ポータル のリンクから Azure Portal にログインします。

上記のリンクを経由することで左のペインに JWT validation configurations という項目が追加されます。
"Add JWT validation configuration" をクリックして、各項目を入力します。

項目
Name 適当な名前
Tenant ID 先ほどコピーしたテナント ID
Client ID 先ほどコピーしたアプリケーション (クライアント) ID
Audiences api://<上記の Client ID>

関連付けるルールを選択して、"Add" をクリックします。※現状、JWT validation はルール単位で有効となるため、パス単位では制御できません。
image.png

curl コマンドにて動作確認

Powershell & curl.exe を使ってトークンを取得します。Tenant ID と Client ID は先ほどの Application Gateway に設定したものと同じものを設定してください。シークレットは事前にコピーしておいたものを利用します。

$response = curl.exe -X POST https://login.microsoftonline.com/<Tenant ID>/oauth2/v2.0/token `
 -H "Content-Type: application/x-www-form-urlencoded" `
 -d "grant_type=client_credentials" `
 -d "client_id=<Client ID>" `
 -d "client_secret=Gmexxxxxxxxxxxxxxxxxxxxt0mahatS" `
 -d "scope=api://<Client ID>/.default"
 
$token = ($response | ConvertFrom-Json).access_token

まずはトークンのヘッダー無しで、Status 401 にて拒否されることを確認します。

curl.exe https://testxxx2.xxxnet2.org/
<html>
<head><title>401 Authorization Required</title></head>
<body>
<center><h1>401 Authorization Required</h1></center>
<hr><center>Microsoft-Azure-Application-Gateway/v2</center>
</body>
</html>

次にトークンを付けてアクセスしてみます。

curl.exe -v -H "Authorization: Bearer $token" https://testxxx2.xxxnet2.org/
~~省略~~
< HTTP/1.1 200 OK
< Date: Thu, 27 Nov 2025 07:57:11 GMT
< Content-Type: text/html
< Content-Length: 7
< Connection: keep-alive
< Server: Apache/2.4.58 (Ubuntu)
< Last-Modified: Tue, 25 Nov 2025 08:14:58 GMT
< ETag: "7-64466e0253999"
< Accept-Ranges: bytes
<
AppVM1
* Connection #0 to host testagv2.hiyamanet2.org left intact

Status 200 となり、正常にアクセスできることを確認できました。

気になる点

JWT validation が機能することはこの方法で確認できますが、実利用を考えるとユーザー認証+認可も行いたいところです。
Application Gateway には認可を行う機能はない (有効なトークンであればすべて許可してしまう) ため、他の方法にて実装する必要があります。
そこで Single Page Application (SPA) を Storage 上に用意して、ユーザー認証を行い、そこでトークンを取得してから Application Gateway にアクセスさせるということをやりたいと思います。
SPA の環境は何でもよいですが、用意が簡単だったのでストレージを使っています。

検証2 (SPA + ユーザー認証)

構成図

image.png

Azure Storage にて静的 Web サイトの設定

Blob Storage の "静的な Web サイト" の画面にて "静的な Web サイト" を有効化します。
インデックス ドキュメント名を "index.html" と入力して、保存します。
プライマリ エンドポイントをコピーしておきます。
image.png

SPA 用の Entra アプリの作成

Microsoft Entra ID の画面から "アプリの登録"-> "新規作成" をクリックします。
image.png

"名前" を入力し、"この組織ディレクトリのみに含まれるアカウント" を選択して、"シングルページアプリケーション" を選択後、先ほどコピーした静的な Web サイトの URL をコピペして、"登録" をクリックします。
image.png

クライアント ID をコピーしておきます。
image.png

"認証" のページにて "URI の追加" をクリックして、Application Gateway の URL も追加して、"保存" をクリックします。
image.png

Azure Storage に SPA のアップロード

適当なエディタにて以下の SPA アプリ用のコードを記載します。生成 AI で作った簡易なアプリです。
修正するところは <script> の下にある SPA_CLIENT_ID, APPGW_BASE_URL, TENANT_ID, API_APP_ID の 4 つです。
Web サーバーへは /api/hello でアクセスしてますので、Web サーバー側もアクセスできるようにしておきます。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <title>JWT → AppGW テスト</title>
  <script src="https://alcdn.msauth.net/browser/2.35.0/js/msal-browser.min.js"></script>
</head>
<body>
  <h1>JWT → AppGW テストページ</h1>

  <button id="login">① ログインしてトークン取得</button>
  <button id="call-api" disabled>② AppGW 経由で API 呼び出し</button>

  <pre id="output" style="white-space: pre-wrap; background: #f3f3f3; padding: 16px; margin-top: 20px;"></pre>

  <script>
    const SPA_CLIENT_ID = "<SPA 用の Entra アプリの Client ID>";
    const APPGW_BASE_URL = "https://testxxx2.xxxnet2.org/";  
    const TENANT_ID = "69817bd9-xxxx-xxxx-a096-a7018e27f302";
    const API_APP_ID = "最初に作った Entra アプリの Client ID";
    const API_SCOPES = ["api://" + API_APP_ID + "/access_as_user"];

    const msalConfig = {
      auth: {
        clientId: SPA_CLIENT_ID,
        authority: "https://login.microsoftonline.com/" + TENANT_ID,
        redirectUri: window.location.origin
      }
    };

    const msalInstance = new msal.PublicClientApplication(msalConfig);
    let account = null;
    let accessToken = null;

    function log(title, obj) {
      const out = document.getElementById("output");
      out.textContent += "\n\n=== " + title + " ===\n";
      if (obj !== undefined) {
        out.textContent += (typeof obj === "string" ? obj : JSON.stringify(obj, null, 2));
      }
    }

    document.getElementById("login").onclick = async () => {
      try {
        // ① ログイン
        const loginResp = await msalInstance.loginPopup({
          scopes: API_SCOPES
        });
        account = loginResp.account;
        log("ログイン成功", { username: account.username });

        // ② トークン取得
        const tokenResp = await msalInstance.acquireTokenSilent({
          account,
          scopes: API_SCOPES
        });
        accessToken = tokenResp.accessToken;
        log("Access Token(先頭40文字)", accessToken.substring(0, 40) + "...");

        document.getElementById("call-api").disabled = false;

      } catch (err) {
        console.error(err);
        log("ログイン/トークン取得エラー", err);
      }
    };

    document.getElementById("call-api").onclick = async () => {
      try {
        const res = await fetch(APPGW_BASE_URL + "/api/hello", {
          headers: {
            "Authorization": "Bearer " + accessToken
          }
        });

        const text = await res.text();
        log("API レスポンス", `status=${res.status}\nbody=${text}`);

      } catch (err) {
        console.error(err);
        log("API 呼び出しエラー", err);
      }
    };
  </script>
</body>
</html>

ファイル名を index.html として保存してコンテナーの $web 内にアップロードします。

image.png
image.png

この状態で静的 Web アプリのエンドポイントにブラウザでアクセスすると以下のような画面が出るはずです。
"ログインしてトークン取得" をクリックして、"ログイン成功" となれば動作しています。

image.png

この構成の問題点

この構成で "AppGW 経由で API 呼び出し" をすると Status 401 となります。
この構成では、SPA はストレージ、アクセスしたい Web サイトは Application Gateway という形で、別サイト (別オリジン) 扱いとなりますが、その場合、ブラウザが安全確認のために preflight(OPTIONS) を送ります。しかし、OPTIONS にはトークンを付けられないため、AppGW が JWT validation で拒否してしまいそのあとの処理に進めない状態になります。

image.png

Application Gateway 配下に SPA (BLOB Storage) も配置することで、同一オリジンとして扱うことはできますが、JWT validation はルール単位での関連付けとなり、パス単位で制御できないため、SPA にアクセスする時点でトークンを検証してしまい 401 で拒否されてしまいます。

検証3 (Front Door を置いて回避)

回避策の構成案

Azure Front Door を Storage と Application Gateway のフロントに配置し、パスベースで SPA or Application Gateway にアクセスするような構成を試してみます。
Front Door でパスベースのルーティングを行うことで同一オリジンかつ SPA だけは JWT validation を行わないといったことができます。

構成図

image.png

フロントドアの構成

Front Door リソースを作成して、パスによってアクセスする配信グループを変えておきます。

SPA (Blob Storage) 用のルール
image.png

Application Gateway 用のルール
image.png

Entra アプリ、SPA の修正

Front Door のエンドポイントの URL も SPA 用の Entra アプリのリダイレクト URI に追加しておきます。
image.png

BLOB ストレージに配置した index.html 内の以下の URL も Front Door のエンドポイントに変えておきます。

    const APPGW_BASE_URL = "https://jwt-ag-hiyama-xxxxxxxxx.b01.azurefd.net/";  

うまくいくと、以下のような形で API レスポンスに status=200 と表示されるはずです。
image.png

認可を行う方法

ここまででユーザー認証を行い JWT validation を通して Web サーバーにアクセスすることはできましたが、ユーザーの認可は行っていないため同じテナント内のユーザーであればだれでもアクセスできる状態となります。
Application Gateway には認可を行う機能はないため、Entra アプリの方の機能でトークンを取得できるユーザーを制御してみます。
トークンを取得できるユーザーを絞る簡易な認可の方法にはなるため、より細かい制御はバックエンドのアプリでやる必要があります。

Entra ID の画面からエンタープライズアプリケーションをクリックします。
image.png

最初に作った Entra アプリをクリックします。
image.png

"割り当てが必要ですか?" を "はい" に変更して、保存します。
image.png

この設定で "ユーザーとグループ" のところにいないユーザーはトークンの取得ができなくなります。
image.png

上記のユーザーとして割り当てられているユーザーの場合、以下のような同意を求められるので許可すると、Web サーバーにアクセスできました。

image.png

image.png

アプリに対して割り当てがないユーザーの場合、以下のようにトークンを取得できずに失敗しました。

image.png

WAF と併用した場合の挙動

WAF が優先される

トークン無しで WAF にて拒否されるリクエストを送ってみると Status 403 となりましたので、WAF が優先されていることがわかります。

image.png

SPA を少し修正して、トークンを付けた場合でも確認しましたが、期待通りに拒否されました。
image.png

まとめ

SPA の配置やパスベースのルーティング等、少し考慮は必要ですが、レガシーなアプリで VM にホストされているものやオンプレでホストしているアプリでもリバースプロキシ経由 (Application Gateway) 経由で Entra ID 認証を利用することができるようになると感じる機能でした。
アプリ側に認証ロジックを入れる必要がない点は使いどころがあるのではないかと思います。
パスベースで JWT validation を有効化できるようになるとなおよいなと思いました。

3
0
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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?