Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

Slide1.jpeg

API Managementは、柔軟にバックエンドのAPIを公開するために、URLの付け替え、認証、Development Portalなどの機能が柔軟に揃っています。

Screen Shot 2017-11-05 at 12.46.47 AM.png

こんな感じの 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 というサブディレクトリにしています。

Screen Shot 2017-11-05 at 12.51.40 AM.png

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

Screen Shot 2017-11-05 at 12.54.12 AM.png

SPA の設定

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

Screen Shot 2017-11-05 at 12.55.44 AM.png

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

Screen Shot 2017-11-05 at 12.55.59 AM.png

Sign up/ Sign In policy

これも前回と同じです。

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

Screen Shot 2017-11-05 at 12.59.51 AM.png

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

Screen Shot 2017-11-05 at 1.00.13 AM.png

API Management

API Management を起動して、Publisher Portal のリンクをクリックすると、次のような画面になります。
Screen Shot 2017-11-05 at 1.06.49 AM.png

設定していきます。

Products

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

Screen Shot 2017-11-05 at 1.08.47 AM.png

Product / Settings

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

Screen Shot 2017-11-05 at 1.09.27 AM.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

Operations

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

Screen Shot 2017-11-05 at 1.23.18 AM.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

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

Screen Shot 2017-11-05 at 1.54.57 AM.png

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

を実行して、認証します。

Screen Shot 2017-11-05 at 2.03.39 AM.png

login をクリックして認証

Screen Shot 2017-11-05 at 2.03.49 AM.png

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

Screen Shot 2017-11-05 at 2.04.39 AM.png

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

Screen Shot 2017-11-05 at 2.04.57 AM.png

おまけ

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

Screen Shot 2017-11-05 at 2.05.23 AM.png

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

Screen Shot 2017-11-05 at 2.11.59 AM.png

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

Screen Shot 2017-11-05 at 2.14.06 AM.png

Resource

TsuyoshiUshio@github
プログラマ。自分の学習用のブログです。内容は会社とは一切関係ありません。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away