今回は前回の記事の続きです。前回は、SPA と Active Directory B2C を使って、認証、トークン発行して、Azure Functions の方では、トークンの認証だけを行う時のやり方を書いて見ましたが、今回は、Azure Functions と SPA の間に、API Management をかまして見ます。

API Managementは、柔軟にバックエンドのAPIを公開するために、URLの付け替え、認証、Development Portalなどの機能が柔軟に揃っています。
こんな感じの API 公開用のページも作ってくれて、なかなかいい感じです。サービスはいいのですが、ドキュメントがいまいちわかりにくいという問題があります。今回は、これを、Active Directory B2C で認証を行って、API マネジメントでトークンの確認を行い、APIマネジメントの部分で、JWT トークンをパースして、中にあるクレームをHTTP Header に入れて、バックエンドの Azure Functions に渡すということをして見ます。
Azure Active Directory B2C の設定
設定の意味合い、方法は、前回の記事とほぼ同じになりますので、スクリーンショットのみ載せます。SPA は前回のと同じです。(URL のみ API Management に向けています)
Application の設定 (API Management 用)
Reply URL に関しては、API Management 側では、トークンの確認しかしませんので、Reply URL は本来不要ですが、設定しておきます。今回はbackend
というサブディレクトリにしています。

スコープ名はなんでもいいですが、今回は api.read
という名前にして見ました。

SPA の設定
これは前回と同じです。今回のSPAはローカルで動かしているので、Reply URL もローカルのアドレスになっています。

ポイントとしては、新たに追加した、API Management の Application をしっかり足すことです。

Sign up/ Sign In policy
これも前回と同じです。
この赤で括った部分が、openid-configuration のメタデータです。あとで使います。

いくつか設定できますが、今回特にポイントとなる「Claim」を説明します。これは、Azure B2C で認証したのち、Authorization: Bearer ....
という形態でヘッダにJWT トークンが格納されるのですが、クレームは、このトークンの中にどの認証情報を含むかという設定です。ここでは、あとで使う emails
に注目しましょう。

API Management
API Management を起動して、Publisher Portal のリンクをクリックすると、次のような画面になります。
設定していきます。
Products
最初にプロダクトを設定します。今後、API を公開していきますが、そのグループとして、プロダクトという単位があります。この単位で、APIを公開するかどうかというのを設定します。最初は、APIを登録しても、API Management に接続しても、バックエンドのAPI (Azure Functions) にアクセスできない問題がありま下が、これは、Products を設定すればオッケーです。
Product / Settings
ここでは、Require subscription のチェックを外します。デフォルトではこのチェックが付いています。JWT のチェックをする場合は、後ほど出てくる Policies の設定で行うので、ここでは、チェックを外しておきます。
APIs
Settings での設定を見ていきます。
Settings
- Web service URL
Azure Functions の function のURLを入力します。例えば、https://apimgbackend.azurewebsites.net/api/HttpTrigger?code=xasdfasdfasdgasdgasdgagalllacad923gdaga&name=Azure
みたいな感じだとしても、最初のホスト名までで結構です。
- Web API URL suffix
API Management 経由でこの バックエンド API を呼ぶ時のURLになります。これを設定すると、下に、どんなURLになるのかが表示されています。(This is what the Web API URL is going to look like:のところ)
- Web API URL Schema
今はHTTP を設定しています。なぜかというと、SSL証明書を設定しないと、HTTPs を選べないからです。WebApps/Functions とかだと、デフォルトであるので不要なのですが、プロダクションで使うものなので、どうせ証明書がいるので、HTTPS を使いたければ、設定する必要があります。ここでは、テストなので HTTP にしています。
Operations
ここに、API をちゃんと登録しないと、公開されませんし、開発者ドキュメントに反映されません。必要な文を登録しましょう。
ここでは、Api Management の提供するホスト名に対して、http://xxxx/backend?name=Azure
というリクエストを送ると、バックエンド Function に対して https://yyyyy/api/HttpTriggerCSharp1?code=xxxxxxxxx&name=Azure
といったようなルーティングをしようとしています。{name} の箇所がバックエンド側の設定 (Rewrite URL template) に引き継がれます。
URL template
/backend?name={name}
Rewrite URL template
api/HttpTriggerCSharp1?code=h8o7xxxxxxxxxPqafQ==&name={name}
Policies
今回最大のポイントとなるところです。
次の画面になります。Product / API / Operation を選択して、それに対して、ポリシーを適用していきます。最後に全体を載せますが、個別に設定しておきます。 XML で設定できるようです。
Validate JWT
SPA 側で認証したAccess Token を検証します。
ポイントとしては、open-config
のところで、Sign up/ Sign In policy
のところで、獲得した、open config のメタデータをここに貼っておきます。また、audience
のところは、Azure Active Directory B2C で設定したAPI Management の Application ID になります。
また、ここで、required-claim
というのもありますが、ここにクレームを書いておくと、そのクレームがJWTトークンに含まれいていることが必須になります。ですのでここでは、特に設定していません。
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
<openid-config url="https://login.microsoftonline.com/someorganication.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_SPA-APIManagement" />
<audiences>
<!-- API Management Application ID -->
<audience>782a51e6-4186-4258-95cc-7c6720ec4e72</audience>
</audiences>
<!--<required-claims>
<claim name="id" match="all">
<value>xxx</value>
</claim>
</required-claims>-->
</validate-jwt>
CORS
SPA の場合 CORS が必要なので設定しておきます。見ての通りですね。origin
のところに、SPAのURLを書いておきます。
<cors>
<allowed-origins>
<!--<origin>*</origin>-->
<!-- allow any -->
<!-- OR a list of one or more specific URIs (case-sensitive) -->
<origin>http://localhost:6420</origin>
<!-- URI must include scheme, host, and port. If port is omitted, 80 is assumed for http and 443 is assumed for https. -->
</allowed-origins>
<allowed-methods>
<!-- allow any -->
<method>*</method>
</allowed-methods>
<allowed-headers>
<!-- allow any -->
<header>*</header>
</allowed-headers>
</cors>
JWT の Parse と Set Header
JTW をパースして、B2Cで設定したクレームを取得して、ヘッダにセットしておきます。そうするとバックエンド側で、JWTのパースが不要になります。単に TryParseJwt(out jwt)
メソッドでパースして、Jwt のインスタンスを取得して、値をヘッダーにセットするset-header
タグを使っているだけですね。簡単。emails
というクレームを、x-b2c-email-claim
というヘッダにセットしています。
<set-header name="x-b2c-email-claim" exists-action="override">
<value>@{
var emails = "default@email.com";
var authHeader = context.Request.Headers.GetValueOrDefault("Authorization", "");
if (authHeader?.Length > 0)
{
string[] authHeaderParts = authHeader.Split(' ');
if (authHeaderParts?.Length == 2 && authHeaderParts[0].Equals("Bearer", StringComparison.InvariantCultureIgnoreCase))
{
Jwt jwt;
if (authHeaderParts[1].TryParseJwt(out jwt))
{
emails = jwt.Claims.GetValueOrDefault("emails", emails);
}
}
}
return emails;
}</value>
</set-header>
これで、API Management 側の設定は完了です。Policy のソース全体を載せておきます。
<policies>
<inbound>
<validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
<openid-config url="https://login.microsoftonline.com/someorganication.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_SPA-APIManagement" />
<audiences>
<!-- API Management Application ID -->
<audience>782a51e6-4186-4258-95cc-7c6720ec4e72</audience>
</audiences>
<!--<required-claims>
<claim name="id" match="all">
<value>xxx</value>
</claim>
</required-claims>-->
</validate-jwt>
<cors>
<allowed-origins>
<!--<origin>*</origin>-->
<!-- allow any -->
<!-- OR a list of one or more specific URIs (case-sensitive) -->
<origin>http://localhost:6420</origin>
<!-- URI must include scheme, host, and port. If port is omitted, 80 is assumed for http and 443 is assumed for https. -->
</allowed-origins>
<allowed-methods>
<!-- allow any -->
<method>*</method>
</allowed-methods>
<allowed-headers>
<!-- allow any -->
<header>*</header>
</allowed-headers>
</cors>
<set-header name="x-b2c-email-claim" exists-action="override">
<value>@{
var emails = "default@email.com";
var authHeader = context.Request.Headers.GetValueOrDefault("Authorization", "");
if (authHeader?.Length > 0)
{
string[] authHeaderParts = authHeader.Split(' ');
if (authHeaderParts?.Length == 2 && authHeaderParts[0].Equals("Bearer", StringComparison.InvariantCultureIgnoreCase))
{
Jwt jwt;
if (authHeaderParts[1].TryParseJwt(out jwt))
{
emails = jwt.Claims.GetValueOrDefault("emails", emails);
}
}
}
return emails;
}</value>
</set-header>
<base />
<rewrite-uri template="api/HttpTriggerCSharp1?code=h8o7xxxxxxxxxPqafQ==&name={name}" />
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
SPA側の設定変更
前回とソースは同じですが、b2cScopes と webApi の変更をお忘れなく。
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/backend/api.read"],
webApi: 'http://sometest.azure-api.net/backend/backend?name=Azure',
};
なお、現在のURLはデベロッパーポータルからとって来れます。スクリーンショットでは、401 になっていますが、これは、Policy で、JWTトークンの設定をしているからで、その設定をする前だと、ここでAPIのテストもすることができます。テストをする前だと、各種言語で、このAPIにアクセスするための、サンプルコードまでついていますので、これはいい感じ。
Azure Functions 側のコード
ここでは、SPA では、API Management のエンドポイントがバレるのは防げないので、トークンの確認をして保護しています。API Management からは、Azure Functions をhttps で接続するようにして、中を読み取られれないようにします。その上で、Policyで設定したクレームを単に表示してみましょう。C# の HttpTrigger に、// Get Auth Parameter value の項目を追加しただけです。
using System.Net;
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
log.Info("C# HTTP trigger function processed a request.");
// parse query parameter
string name = req.GetQueryNameValuePairs()
.FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0)
.Value;
// Get Auth Parameter value
foreach (var keyValue in req.Headers) {
log.Info(keyValue.Key + ": " + keyValue.Value.FirstOrDefault());
}
// Get request body
dynamic data = await req.Content.ReadAsAsync<object>();
// Set name to query string or body data
name = name ?? data?.name;
return name == null
? req.CreateResponse(HttpStatusCode.BadRequest, "Please pass a name on the query string or in the request body")
: req.CreateResponse(HttpStatusCode.OK, "Hello " + name);
}
実行
node server.js
を実行して、認証します。

login
をクリックして認証

トークンが帰ってきたので、Call Web API
クリック。バックエンドがコールされる

Azure Functions
が動く。ログを見ると、しっかりと、emails クレームが取得できている!

おまけ
ざっとみていると、どうやらここまでの設定は、Git に保存してそこからクローンとかできそう。これは、再作成の時に便利そうですね。
Save configuration to repository
をクリックすると、ここで、Git リポジトリができて、設定がセーブされる感じ
パスワードを生成したら、普通にクローンできた。これはいいね。