SPA から AD B2C で認証し、API Management 経由で Azure Functions を使う

今回は前回の記事の続きです。[前回](https://qiita.com/TsuyoshiUshio@github/items/a2a4a7eef923af4e6267)は、SPA と Active Directory B2C を使って、認証、トークン発行して、Azure Functions の方では、トークンの認証だけを行う時のやり方を書いて見ましたが、今回は、Azure Functions と SPA の間に、API Management をかまして見ます。

<img width="500" alt="Slide1.jpeg" src="https://qiita-image-store.s3.amazonaws.com/0/3470/f54cfd9a-ce84-92af-ab57-d9f5b7592d3b.jpeg">

[API Management](https://docs.microsoft.com/en-us/azure/api-management/api-management-key-concepts)は、柔軟にバックエンドのAPIを公開するために、URLの付け替え、認証、Development Portalなどの機能が柔軟に揃っています。

![Screen Shot 2017-11-05 at 12.46.47 AM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/79a3bed9-b12a-c3c0-5c81-ed9db0fa52ab.png)

こんな感じの API 公開用のページも作ってくれて、なかなかいい感じです。サービスはいいのですが、ドキュメントがいまいちわかりにくいという問題があります。今回は、これを、Active Directory B2C で認証を行って、API マネジメントでトークンの確認を行い、APIマネジメントの部分で、JWT トークンをパースして、中にあるクレームをHTTP Header に入れて、バックエンドの Azure Functions に渡すということをして見ます。

# Azure Active Directory B2C の設定

設定の意味合い、方法は、前回の記事とほぼ同じになりますので、スクリーンショットのみ載せます。SPA は前回のと同じです。(URL のみ API Management に向けています)

* [SPA から Azure Functions を使う時のActive Directory B2C 認証設定方法](https://qiita.com/TsuyoshiUshio@github/items/a2a4a7eef923af4e6267)

## Application の設定 (API Management 用)

Reply URL に関しては、API Management 側では、トークンの確認しかしませんので、Reply URL は本来不要ですが、設定しておきます。今回は`backend` というサブディレクトリにしています。

<img width="749" alt="Screen Shot 2017-11-05 at 12.51.40 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/8407c8cc-44f3-d1ef-ac40-c4336bd61eba.png">

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

<img width="1105" alt="Screen Shot 2017-11-05 at 12.54.12 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/cf808813-adad-3845-3840-aec583497314.png">

## SPA の設定

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

<img width="802" alt="Screen Shot 2017-11-05 at 12.55.44 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/c1be4738-615e-158e-d957-46f936197821.png">

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

<img width="1209" alt="Screen Shot 2017-11-05 at 12.55.59 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/c7f16580-b8d7-c4a6-3419-ab9e1de94f25.png">

## Sign up/ Sign In policy

これも前回と同じです。

この赤で括った部分が、openid-configuration のメタデータです。あとで使います。

<img width="587" alt="Screen Shot 2017-11-05 at 12.59.51 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/4b275e8a-a591-a96b-ac03-e569fcd58915.png">

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

<img width="851" alt="Screen Shot 2017-11-05 at 1.00.13 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/b08b3c76-e91b-fbc1-f73a-40b541e69980.png">

# API Management

API Management を起動して、Publisher Portal のリンクをクリックすると、次のような画面になります。
![Screen Shot 2017-11-05 at 1.06.49 AM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/ebe81ec6-8fbd-a7b5-9050-2b606c8b8786.png)

設定していきます。

## Products

最初にプロダクトを設定します。今後、API を公開していきますが、そのグループとして、プロダクトという単位があります。この単位で、APIを公開するかどうかというのを設定します。最初は、APIを登録しても、API Management に接続しても、バックエンドのAPI (Azure Functions) にアクセスできない問題がありま下が、これは、Products を設定すればオッケーです。

![Screen Shot 2017-11-05 at 1.08.47 AM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/b7ce03a8-434a-879f-6292-a0baacc581d1.png)

### Product / Settings

ここでは、Require subscription のチェックを外します。デフォルトではこのチェックが付いています。JWT のチェックをする場合は、後ほど出てくる Policies の設定で行うので、ここでは、チェックを外しておきます。

![Screen Shot 2017-11-05 at 1.09.27 AM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/4b4c22de-4a12-904f-426a-757bd9a9dda4.png)

## 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 にしています。

![Screen Shot 2017-11-05 at 1.23.07 AM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/8e2bdf60-843a-d1dd-cd37-db253cfc852e.png)

### Operations

ここに、API をちゃんと登録しないと、公開されませんし、開発者ドキュメントに反映されません。必要な文を登録しましょう。

![Screen Shot 2017-11-05 at 1.23.18 AM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/b2830399-7b61-e7d9-a4c3-39b38fbdd723.png)

ここでは、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}
```

![Screen Shot 2017-11-05 at 1.46.07 AM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/f9eeed6d-9417-a3d7-b2d7-41aaa7f4d186.png)


## 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トークンに含まれいていることが必須になります。ですのでここでは、特に設定していません。

```xml
		<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を書いておきます。

```xml

		<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` というヘッダにセットしています。

```xml
		<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 のソース全体を載せておきます。

```xml
<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==&amp;name={name}" />
	</inbound>
	<backend>
		<base />
	</backend>
	<outbound>
		<base />
	</outbound>
	<on-error>
		<base />
	</on-error>
</policies>
```

# SPA側の設定変更

[前回](https://qiita.com/TsuyoshiUshio@github/items/a2a4a7eef923af4e6267)とソースは同じですが、b2cScopes と webApi の変更をお忘れなく。

```js
        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にアクセスするための、サンプルコードまでついていますので、これはいい感じ。

![Screen Shot 2017-11-05 at 1.54.57 AM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/efdbdaa7-069c-6ab6-4c3c-f6da2900e828.png)

# Azure Functions 側のコード

ここでは、SPA では、API Management のエンドポイントがバレるのは防げないので、トークンの確認をして保護しています。API Management からは、Azure Functions をhttps で接続するようにして、中を読み取られれないようにします。その上で、Policyで設定したクレームを単に表示してみましょう。C# の HttpTrigger に、// Get Auth Parameter value の項目を追加しただけです。

```C#
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
```
を実行して、認証します。

<img width="783" alt="Screen Shot 2017-11-05 at 2.03.39 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/05a12588-62a1-0bf7-9f32-6d52dbdb4e38.png">

`login` をクリックして認証

<img width="1816" alt="Screen Shot 2017-11-05 at 2.03.49 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/03b08ab8-b43c-afdc-8c52-c8182dfc4532.png">

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

<img width="798" alt="Screen Shot 2017-11-05 at 2.04.39 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/942ff8be-f04c-3e3a-7c3a-140c09ef681d.png">

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

<img width="1153" alt="Screen Shot 2017-11-05 at 2.04.57 AM.png" src="https://qiita-image-store.s3.amazonaws.com/0/3470/82bc09f5-b7e6-a4b5-3a14-e7bc837dcbd2.png">

# おまけ

ざっとみていると、どうやらここまでの設定は、Git に保存してそこからクローンとかできそう。これは、再作成の時に便利そうですね。

![Screen Shot 2017-11-05 at 2.05.23 AM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/32bfdf26-8cf7-9d26-8778-d1c2e0ccb488.png)

`Save configuration to repository` をクリックすると、ここで、Git リポジトリができて、設定がセーブされる感じ

![Screen Shot 2017-11-05 at 2.11.59 AM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/7d8195bb-091b-ae90-978e-b1d0937d03e6.png)

パスワードを生成したら、普通にクローンできた。これはいいね。

![Screen Shot 2017-11-05 at 2.14.06 AM.png](https://qiita-image-store.s3.amazonaws.com/0/3470/59fecaf3-5693-8f48-7976-6fbbe6cdb409.png)

# Resource

* [SPA から Azure Functions を使う時のActive Directory B2C 認証設定方法](https://qiita.com/TsuyoshiUshio@github/items/a2a4a7eef923af4e6267)
* [Extract JWT Claims in Azure API Management Policy](http://devjourney.com/blog/2017/03/23/extract-jwt-claims-in-azure-api-management-policy/)
* [API Management policy expressions](https://docs.microsoft.com/en-us/azure/api-management/api-management-policy-expressions)