SPA -> Azure Functions にアクセスするときに、Azure Active Directory B2C を使って認証をしたいときにどのように設計するべきか?と言うのを考えて実装してみました。

SPA と Azure Functions 間の認証方式
SPA には様々な制約があります。例えば環境変数を受け取れないなどの制約です。SPAに埋め込んだエンドポイントのURLは全てユーザ側にわかってしまいます。と言うことは、どうやって、認証・認可を行えばいいでしょう?一つの方法は、Azure Active Directory B2C を使って認証します。イメージ図は次の通りです。

大まかな方式としては、SPA の JavaScriptのADの認証ライブラリを使って認証し、アクセストークンを取得します。そのアクセストークンをつけて、Azure Functions にリクエストを送信し、Azure Functions 側では、そのアクセストークンが妥当かをAzure Active Directory B2C に問い合わせて認証すると言うものです。この方式であれば、エンドポイントがわかっても、認証しなければ、API にアクセスすることはできません。
SPA を実装する。
SPA のログイン実装をするためのサンプルが存在します。
これの設定をすれば良い感じです。じゃあ、Azure Active Directory B2C の設定からみていきましょう。
AD B2C を設定する
Functions アプリケーション の設定
Application ID
これは、CLIENT_ID とか言われたりするものです。アプリケーションのIDです。
Reply URL
一般的にAD で認証された後、リダイレクトされるURLです。このシナリオでは、Azure Functions 側からリダイレクトすることはないのですがAzure Functions のURlを書いておきます。
App ID URL
なにそれ?と言う感じですが、Azure Functions のURLの最初のサブディレクトリを書いておきます。ここでは、api
になっています。これが、Functions Application の識別子になっているようです。

Scope
公開するスコープです。ここでは、新しいスコープとして、func.read
と言うスコープを設定してみました。

SPA アプリケーションの設定
SPA 側のアプリケーションの登録です。App ID URI は不要です。Reply URL は、認証後、サンプルアプリはローカルで動くので、このURL にしています。こちらにリダイレクトされます。

また、SPA 側のアプリケーションでは、API access
の設定が必要です。これは、先ほど Function アプリケーションと、そちらで設定したスコープが見えるようになりますので、それを選択してセーブします。

Sign up / Sign in policy
ここでは、赤枠のところを保存しておきましょう。メタ情報です。

この後は、個別の設定を載せておきます。これらは、サインインに必要な属性の設定だったり、認証済みのリソースに渡される内容の設定だったりしています。
さて、準備環境
SPA Application の設定
先ほどのサンプルをClone したら次の箇所を変更します。
CLIENT_ID SPA アプリケーションの Application ID
authority "https://login.microsoftonline.com/tfp/{tenant}.onmicrosoft.com/{Sign UP/ Sign in Policy Name}"
b2cScopes "{Function App ID URL}/{Function App Policy name}"
webApi "認証後、アクセスする Azure Functions の URL"
var applicationConfig = {
clientID: '84270d3d-1c67-4d18-8436-f3a121b1ebef',
authority: "https://login.microsoftonline.com/tfp/someorganication.onmicrosoft.com/B2C_1_SPA_Local",
b2cScopes: ["https://someorganication.onmicrosoft.com/api/func.read"],
webApi: 'https://removebackend.azurewebsites.net/api/HttpTriggerCSharp1?name=Azure',
};
ここでポイントは、b2cScopes
です。CLIENT_ID
がSPA のIDであるため、ここも、SPA側の設定をしそうなものですが、ここは、Functions アプリケーション側の設定です。SPA からみて、どのリソースにアクセスするか?と言うリストです。(これをみて、AD B2C の方で認証して、アクセスできるリソースを取ってきます。
私は当初、ここを間違えていて、ずっと悩んでいました。なぜかと言うと、ここで、間違えるとバックエンドにアクセス時ではなく、ログイン時にエラーになります。(だから、SPAの設定が悪いと思った。)実際は、ログインすると、アクセスできるリソースの情報と権限が渡されるので、ログイン時にすでに査定されているので、間違えているとログイン時に落ちます。そのケースだと、下記のエラーになります。

このケースは、b2cScopes
が間違っていると思われます。これで SPAは準備おっけー
Azure Functions 側の認証
前回のポストで、Azure Function に直接 B2C の認証をかける方式は試しましたが、トークンの確認だけのパターンはまだなので、やってみます。前回と同じく Authentication /Authorization > Active Directory Settings を設定します。

CLIENT_ID: Function アプリケーションの Application ID
Issuer Url: Sign up / Sign in policy で出てきたメタ情報
ALLOWED TOKEN AUDIENCE: SPA アプリケーションの Application ID
と言う感じで設定します。
この設定をしてから、Azure Functions の画面からテストをすると、401 になります。準備完了。

おまけ
これですでに実行していいのですが、サンプルのアプリケーションは、ログイン時にポップアップが出てきて、認証します。しかし、ポップアップじゃなくしたい場合は、次のようにします。
var clientApplication = new Msal.UserAgentApplication(applicationConfig.clientID, applicationConfig.authority, function (errorDesc, token, error, tokenType) {
});
logMessage("here");
window.onload = function() {
if (!clientApplication.isCallback(window.location.hash) && window.parent == window && !window.opener) {
clientApplication.acquireTokenSilent(applicationConfig.b2cScopes).then(function(accessToken) {
logMessage("accessToken" + accessToken);
updateUI();
});
var user = clientApplication.getUser();
}
}
function login() {
logMessage("step1");
clientApplication.loginRedirect(applicationConfig.b2cScopes).then(function (idToken) {
login の中身を、loginPopup ではなく、loginRedirectにして、リダイレクトで戻ってきたときに、clientApplication オブジェクトが生成されたあとに、 acquireTokenSilent
を取ってきて、下の方で、トークンにつけて渡しています。
全ソースコードを上げておきました。
実行
クライアントアプリケーションを起動し、操作すると、無事ログインして、バックエンドのAzure Functions が呼ばれたことが確認できました。簡単!

Resource
- Microsoft Authentication Library Preview for JavaScript (MSAL.js)
- A single page application (SPA) calling a Web API. Authentication is done with Azure AD B2C by leveraging MSAL.js
-
Azure AD B2C: Single-page app sign-in by using OAuth 2.0 implicit flow
*Azure active directory - Allow token audiences - Node.js Web API with Azure AD B2C