1. 記事一覧 >
  2. ブログ記事
Azure
category logo

Azure Static Web AppsのアプリにAzure ADカスタム認証機能を追加

(更新) (公開)

はじめに

Azure Static Web Apps アプリに Azure AD(Azure Active Directory)を使った認証機能を追加しました。
別記事「MSAL React を使用してサインインする SPA を Azure Static Web Apps にデプロイ」でも認証機能を実現していますが、この時は、MSAL を使って自力で実装したパターンです。
今回は、それと異なり、Azure Static Web Apps の機能を利用しています。


https://learn.microsoft.com/ja-jp/azure/static-web-apps/authentication-custom?tabs=aad

Azure Static Web Apps が提供するマネージド認証では、Azure が管理するプロバイダー登録が使用されます。
登録の柔軟性を高めるため、既定値をカスタム登録でオーバーライドできます。
とあり、最初、??...何をおっしゃっておられるのかな?
と思いましたが、
いろいろやっている内に、


マネージド認証: ほとんど何も実装しなくても認証機能を追加できる。しかも、Azure AD、GitHub、Twitter のアカウントを使える。Azure AD テナントは絞り込めない。
カスタム認証: Azure AD 側で仕込みが必要。認証対象を一つのテナントに絞り込める。Azure AD に限らず、OpenID Connect (OIDC)プロバイダー等も利用可。有料の Standard プラン必要。


ということが分かってきました。


今回、両方ともの手順と検証結果を書きたいと思います。


検証用プログラム

本記事で使用するソースコードサンプルは、GitHub にアップしました。
https://github.com/itc-lab/azure-devops-custom-auth-example


フロントエンドReact TypeScript
バックエンド APIPython (Azure Static Function用)


フロントエンドの実装については、
別記事「React TypeScript ESLint Prettier VSCode のプロジェクト作成
create-react-app を使って作成したプロジェクトをベースにしています。(記事の通り、ESLint、Prettier に対応しています。)
肉付けした実装は、
7. Web アプリに簡単な認証を追加する」(https://learn.microsoft.com/ja-jp/azure/developer/javascript/how-to/with-web-app/static-web-app-with-swa-cli/add-authentication
ほぼそのままです。


Python の Azure Static Function 用 API 作成 については、別記事「【swa】Azure Static Web Apps をローカル環境でデバッグ」で説明しています。
これを /api/hello とし、{ "message": "メッセージ内容" } の JSON を返すようにしました。
メッセージ内容は、
X-MS-CLIENT-PRINCIPAL-ID
X-MS-CLIENT-PRINCIPAL-NAME
X-MS-CLIENT-PRINCIPAL-IDP
X-MS-CLIENT-PRINCIPAL
X-MS-TOKEN-AAD-ID-TOKEN
X-MS-TOKEN-AAD-ACCESS-TOKEN
X-MS-TOKEN-AAD-REFRESH-TOKEN
ヘッダの内容です。



以降、前提として、API(https://**********.azurestaticapps.net/api/hello)は、認証された人からしか呼び出せないようにするものとします。


トークンについて
後になって、知ると困る事になりそうなので、話を進める前に書いておきますが、この記事の方法で認証を実現してもトークンは渡されないようです。
トークンを使って、本人の代わりに Microsoft サービスを呼び出すようなことはできません。
https://stackoverflow.com/questions/71728993/how-to-get-azure-ad-access-token-in-static-web-app

つまり、以下のようなことはできないということです。

Microsoft Graph APIを直接呼び出し


APIからMicrosoft Graph API

2番目の On-Behalf-Of (OBO) フローについては、チュートリアルをやってみた記事を
MSAL React を使用してサインインする SPA を Azure Static Web Apps にデプロイ
に書きました。Microsoft Authentication Library(MSAL)を使って実現する必要があります。


デプロイについて
別記事「Azure DevOps から React Python アプリを Azure Static Web Apps にデプロイ」に従って、sample-project が Azure Static Web Apps にデプロイ済みとします。


マネージド認証

承認プロバイダーログイン ルート
Azure Active Directory/.auth/login/aad
GitHub/.auth/login/github
Twitter/.auth/login/twitter

とあり、何もしなくても

https://[デプロイしたAzure Static Web Apps URL]/.auth/login/aad
で Azure AD 認証
https://[デプロイしたAzure Static Web Apps URL]/.auth/login/github
で GitHub 認証、
https://[デプロイしたAzure Static Web Apps URL]/.auth/login/twitter
で Twitter 認証

となります。


かなりありがたい機能です。


以上です。


...で終わると、重大な欠陥を持ったアプリになります!


https://**********.azurestaticapps.net/api/hello
と直接 API を呼び出すことが可能で、何もブロックしていません。

そこで、
staticwebapp.config.json をリポジトリに置いて、routes を設定します。


一部、本物のAzureで検証すると辛いため、swa コマンドを使ってローカルで検証しています。

画面キャプチャは、ローカルで検証中の画面ですが、本物でも結果は同じです。

swa は、ダミーの認証画面が表示されて、今回のような認証動作も検証できます。

swa コマンドの詳細は、別記事「【swa】Azure Static Web Appsをローカル環境でデバッグ」を参照してください。 ←この記事の時は、承認と認証のエミュレーションの検証はスキップしています。


認証をかけるには、

{
  "route": "/*", //保護したいパス
  "allowedRoles": ["authenticated"]
}

のように、"allowedRoles": ["authenticated"] を設定します。
"authenticated" の部分は、ロール(全員じゃなくてあらかじめ選ばれた人々に限定)にすることもできますが、ロールの話は、今回スルーします。

staticwebapp.config.json
{
  "routes": [
    {
      "route": "/*",
      "allowedRoles": ["authenticated"]
    }
  ]
}

を設定して、 https://**********.azurestaticapps.net/ にアクセスしてみます。

単純にアクセス拒否されました。あらゆるアクセスが拒否されて、AD 認証画面にたどり着けないので、何もできません。


URL 欄に直接
https://**********.azurestaticapps.net/.auth/login/aad
を入力して、ログインできます。


これで、アクセス可能になって、ログイン後の画面になりましたが、この操作方法を知らないと、使えません。

ログアウトすると、また同じ状況になります。


今度は、401: Unauthorized の時、/.auth/login/aad にリダイレクトされるようにします。

staticwebapp.config.json
{
  "routes": [
    {
      "route": "/*",
      "allowedRoles": ["authenticated"]
    }
  ],
  "responseOverrides": {
    "401": {
      "statusCode": 302,
      "redirect": "/.auth/login/aad"
    }
  }
}

とします。


今度はアクセスした途端、AD 認証画面になります。


これはこれで、良いのですが、最初の画面(twitter, github, aad 選択画面)を無認証で表示したいです。
無認証は、"allowedRoles": ["anonymous"] を設定すれば良いので、

staticwebapp.config.json
{
  "routes": [
    {
      "route": "/",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/index.html",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/*",
      "allowedRoles": ["authenticated"]
    }
  ],
  "responseOverrides": {
    "401": {
      "statusCode": 302,
      "redirect": "/.auth/login/aad"
    }
  }
}

とします。


アクセスすると、画面が真っ白になりました。


これは、なぜかというと、
/static/js/*/static/css/*/static/media/*/manifest.json/favicon.ico にアクセスが行って、リダイレクトされているからです。


/static/js/*/static/css/*/static/media/*/manifest.json/favicon.ico へのアクセスで 401 が返らないようにします。

staticwebapp.config.json
{
  "routes": [
    {
      "route": "/",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/index.html",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/manifest.json",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/favicon.ico",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/static/js/*",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/static/css/*",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/static/media/*",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/*",
      "allowedRoles": ["authenticated"]
    }
  ],
  "responseOverrides": {
    "401": {
      "statusCode": 302,
      "redirect": "/.auth/login/aad"
    }
  }
}

とします。



OK!


未認証で、/api/hello にアクセスすると、/.auth/login/aad にリダイレクトされます。


ログイン用のリンクがあるのだから、リダイレクトではなく、普通に拒否で良いやということで、リダイレクトをやめてみます。

staticwebapp.config.json
{
  "routes": [
    {
      "route": "/",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/index.html",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/manifest.json",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/favicon.ico",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/static/js/*",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/static/css/*",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/static/media/*",
      "allowedRoles": ["anonymous"]
    },
    {
      "route": "/*",
      "allowedRoles": ["authenticated"]
    }
  ]
}

未認証で、/api/hello にアクセスすると、401: Unauthorized になりました。


目的は達成です。


ちなみに、ここまでやっておいてなんですが、結果、API が保護されれば良いので、以下の設定でも目的は達成されます。

staticwebapp.config.json
{
  "routes": [
    {
      "route": "/api/*",
      "allowedRoles": ["authenticated"]
    }
  ]
}

続いて、twitter と github を無効にして、Azure AD 認証だけにします。

staticwebapp.config.json
{
  "routes": [
    {
      "route": "/api/*",
      "allowedRoles": ["authenticated"]
    },
    {
      "route": "/.auth/login/twitter",
      "statusCode": 404
    },
    {
      "route": "/.auth/login/github",
      "statusCode": 404
    }
  ]
}

成功です。


しかし、この状態では、Azure AD のどのテナントでもログインできてしまいます。
どういうことかと言うと、A 社、B 社、C 社、謎の組織...と Azure AD のアカウントであれば、サービスを提供する予定の無い会社でも謎の組織でも、誰でもログインできるということです。

正確には、ロールで絞り込めますが、25アカウントまでという情報があります。

https://learn.microsoft.com/ja-jp/azure/static-web-apps/faq

それで問題無い場合は、ここまでの対応で良いですが、A 社だけログインできるようにしたい場合、カスタム認証の対応が必要になります。
次節でカスタム認証の対応をやっていきます。


カスタム認証

カスタム認証を行うには、3つの対応が必要になります。
・Azure AD のテナントでアプリの登録
staticwebapp.config.json"auth": 設定追加
・Azure Static Web Apps のアプリケーション設定で環境変数設定


Azure Static Web Apps のアプリのホスティング プランを有料のStandardプランに切り替える必要があります。

Freeプランのままの場合、ここで行う作業はことごとく無視されます。


アプリの登録

・Azure AD のテナントでアプリの登録
からやっていきます。


Azure ポータル(https://portal.azure.com/)にアクセスして、Azure Active Directory をクリックします。


アプリの登録新規登録 をクリックします。


アプリ情報を入力します。
名前sample-project(任意)
サポートされているアカウントの種類この組織ディレクトリのみに含まれるアカウント (<テナント名> のみ - シングル テナント)
リダイレクト URIWeb https://**********.azurestaticapps.net/.auth/login/aad/callback

アプリは、SPAですが、Webが正解です。

シングルページアプリケーション (SPA)で登録すると、

AADSTS9002325: Proof Key for Code Exchange is required for cross-origin authorization code redemption.

エラーになります。

OAuth 2.0 authorization code flow で動作するのに、Web の選択が必要です。

https://stackoverflow.com/questions/64692600/aadsts9002325-proof-key-for-code-exchange-is-required-for-cross-origin-authoriz

https://**********.azurestaticapps.net はアプリのURLです。

/.auth/login/aad/callback は決め打ちです。


登録 をクリックします。


アプリケーション (クライアント) IDディレクトリ (テナント) ID が表示されます。これは、後で設定するところで必要になります。この アプリの概要 画面で何度でも確認できます。


アプリの概要 画面のまま、左側にある 証明書とシークレット をクリックします。


クライアントシークレット を選択して、新しいクライアントシークレット をクリックします。

説明sample-project secret
有効期限12 ヵ月(任意)
とし、追加 をクリックします。

追加し終わると、シークレット ID が表示されます。これは、他の画面に移ると、二度と表示されませんので、どこかへコピーペーストしておく必要があります。

後で必要になるのは、値 の方です。


左側にある 認証 をクリックします。


フロントチャネルのログアウト URL
https://**********.azurestaticapps.net/.auth/logout/aad/callback
を入力します。

**********.azurestaticapps.net はアプリのURLです。

/.auth/logout/aad/callback は決め打ちです。

承認エンドポイントによって発行してほしいトークンを選択してください。 のところの
ID トークン (暗黙的およびハイブリッド フローに使用) にチェックを入れます。

チェックを入れずに登録すると、

AADSTS700054: response_type 'id_token' is not enabled for the application.

エラーになります。

カスタム認証は、ID トークンを使用するからです。

保存 をクリックします。


staticwebapp.config.json 設定追加

staticwebapp.config.json"auth": 設定追加 を行います。

staticwebapp.config.json
{
  "auth": {
    "identityProviders": {
      "azureActiveDirectory": {
        "registration": {
          "openIdIssuer": "https://login.microsoftonline.com/<テナントID>/v2.0",
          "clientIdSettingName": "AZURE_CLIENT_ID",
          "clientSecretSettingName": "AZURE_CLIENT_SECRET"
        }
      }
    }
  },
  "routes": [
    {
      "route": "/api/*",
      "allowedRoles": ["authenticated"]
    },
  ]
}

と書き換えてデプロイしておきます。


<テナントID> 部分は、先ほどアプリの登録で確認した ディレクトリ (テナント) ID です。


AZURE_CLIENT_IDAZURE_CLIENT_SECRET は、置き換えず、本当にこのまま記述しておきます。


なお、

    {
      "route": "/.auth/login/twitter",
      "statusCode": 404
    },
    {
      "route": "/.auth/login/github",
      "statusCode": 404
    }

部分は無くなりましたが、カスタム認証設定により、twitter, github は自動的に 404: Not Found になり、404 設定は、不要になるからです。


アプリケーション設定

・Azure Static Web Apps のアプリケーション設定で環境変数設定
を行います。


Azure ポータル(https://portal.azure.com/)にアクセスして、静的 Web アプリ をクリックします。


作成したアプリ sample-project をクリックします。


左側から 構成 をクリックして、アプリケーション設定 → 追加 にて、以下の環境変数を追加します。

AZURE_CLIENT_IDa04dbd00-****-****-****-************
アプリの登録 → sample-project → 概要の アプリケーション (クライアント) ID です。

AZURE_CLIENT_SECRET1n78Q~V*********************************
クライアントシークレットで表示された です。(シークレット ID ではありません。

追加したら、保存 をクリックします。

保存を押さないと反映されません。保存をクリックし忘れて動作確認したとき、404: Not Found になりました。


動作確認







OK!


参考

Azure Static Web Apps の認証とアクセス制御まわりで気になったところ
https://ayuina.github.io/ainaba-csa-blog/auth-static-webapp/


Azure Static Web Apps で事前構成済みのプロバイダーで認証設定をしてみた
https://dev.classmethod.jp/articles/azure-static-web-apps-auth-pre-configured-providers/

loading...