かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Azure API Management を使って Azure Functions に認証つけてみた

とりあえず API Management + Azure AD B2C による認証をトライしてみようと思います。

あ、注意点としては、この手順を実施しても Azure Functions 自体は認証キーによる認証で保護してるだけなので、認証キーばれたら叩かれる感じではあります。

Azure AD B2C 作ろう

Azure ポータルから Azure Active Directory B2C を作ります。

f:id:okazuki:20190714150515p:plain

まず 新しい Azure AD B2C テナントを作成します から作成します。

作成時に国とリージョン選びますが日本はないので、ここらへんのドキュメントを参考に…

docs.microsoft.com

私は、とりあえず米国を選びました。

テナントが出来たら 既存の Azure AD B2C テナントを Azure サブスクリプションにリンクする から適当なリソースグループに紐づけます。

ID プロバイダーを登録しよう

Twitter, Facebook, etc... 色々対応しています。ここでは Microsoft Account を追加します。 普通の Azure AD に移動してアプリ登録をします。任意の組織のディレクトリ内のアカウントと、個人用の Microsoft アカウント (Skype、Xbox、Outlook.com など)を選択してコ ールバック URL に以下のような Azure AD のテナント ID を持ったコールバック URL を設定します。

https://azureadsampledomain.b2clogin.com/azureadsampledomain.onmicrosoft.com/oauth2/authresp

参考:

docs.microsoft.com

Azure AD にアプリを登録しよう

リソースグループを開くと Azure AD B2C のテナントがいるので選択した先の画面で移動を押して Azure AD B2C の画面にいきます。

f:id:okazuki:20190714151424p:plain

この手順とは、関係ありませんが Azure AD B2C 使うとここらへんのアカウント使ってログイン出来るようになるので便利。

f:id:okazuki:20190714151633p:plain

今後 Apple ID にも対応するらしいので、Sign with Apple にも割と簡単に対応できるようになるのでは…?という甘い期待。

さて、API 用のアプリを作ります。さくっとね

f:id:okazuki:20190714152301p:plain

とりあえず適当なスコープを 1 個用意しました。

f:id:okazuki:20190714152536p:plain

次に、Azure AD B2C のほうにクライアントアプリケーション用のアプリを登録します。新しいアプリケーションで以下のようにアプリを作ります。

f:id:okazuki:20190714163711p:plain

今回は Vue.js でアプリ作ろうと思うので npm run serve すると 8080 ポートで動くという前提で、リダイレクト URL に http://localhost:8080 を設定しています。

アプリが Azure AD B2C に作成されたら API アクセスを選んで API 用のアプリへのアクセスを追加します。

f:id:okazuki:20190714163849p:plain

そして、Azure AD B2C のユーザーフロー(ポリシー)に新しいユーザーフローを追加します。

f:id:okazuki:20190714175216p:plain

追加するのは サインアップとサインイン です。

適当に名前をつけてサインインできる ID プロバイダーを設定します。(Twitterとかをちゃんと構成したらここに出てくるはず)要求を返すところに表示名というのがあるので、それにチェックを入れておきます。

f:id:okazuki:20190714200659p:plain

ユーザーフローが出来たら、ユーザー フローを実行しますボタンを押して出てくる画面の一番上の URL をコピーしておきましょう。

f:id:okazuki:20190714200806p:plain

ここでアプリを選択してログインの確認も出来ます。

API を作ろう

とりあえず Azure に Function App を作ります。従量課金プランでいいでしょう。 ポータル上から適当に HTTP トリガーの関数を作ります。

とりあえずデフォルトの名前の HttpTrigger1 を作りました。

f:id:okazuki:20190714154031p:plain

続けて API Management を作ります。これもコンサンプションプランでいきましょう。

f:id:okazuki:20190714154253p:plain

API Management が出来たら Azure Functions に行ってプラットフォーム機能の API Management を選びます。 さっき作った API Management を選んで API のリンクをします。

HttpTrigger1 が選ばれてるのを確認してサクッと作りましょう。

f:id:okazuki:20190714154552p:plain

API Management で API を選ぶと Function App の名前の API があります。今回はサブスクリプションキーによる認証はしないで純粋に OAuth 2.0 での認証にしたいので、All APIs から Function App の名前の API を選んで Settings を選んで Subscription required のチェックを外して Save します。

f:id:okazuki:20190714161839p:plain

そして、All operations のポリシーの inbound に validate-jwt を追加します。

openid_config に先ほどのサインインポリシーで取得した URL になります。そして audience に アプリ ID を設定します。Azure AD B2C に登録したアプリ ID が 98852140-8e30-47dd-abd2-06a5b200bb7d だったので、以下のような感じになりました。

<policies>
    <inbound>
        <base />
        <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
            <openid-config url="https://azureadsampledomain.b2clogin.com/azureadsampledomain.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_signin" />
            <audiences>
                <audience>98852140-8e30-47dd-abd2-06a5b200bb7d</audience>
            </audiences>
        </validate-jwt>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

API を叩いてみました。私は Visual Studio Code の REST Client を使ってます。認証を何もしない状態なので以下のように 401 が返ってきますね!!素敵!

f:id:okazuki:20190714162004p:plain

この後 Vue.js で作ったアプリから叩いてみたいので、CORS の設定もしておきましょう。

CORS は、とりあえず localhost:8080 だけ許す感じでいきます。ということで最終的に API Management の API の All operations のポリシーは以下のようになりました。

<policies>
    <inbound>
        <base />
        <cors allow-credentials="true">
            <allowed-origins>
                <origin>http://localhost:8080</origin>
            </allowed-origins>
            <allowed-methods>
                <method>*</method>
            </allowed-methods>
            <allowed-headers>
                <header>*</header>
            </allowed-headers>
        </cors>
        <validate-jwt header-name="Authorization" failed-validation-httpcode="401" failed-validation-error-message="Unauthorized. Access token is missing or invalid.">
            <openid-config url="https://azureadsampledomain.b2clogin.com/azureadsampledomain.onmicrosoft.com/v2.0/.well-known/openid-configuration?p=B2C_1_signin" />
            <audiences>
                <audience>98852140-8e30-47dd-abd2-06a5b200bb7d</audience>
            </audiences>
        </validate-jwt>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>

本当は、アプリ ID とか openid-config のところの url とかは名前付きの値として定義しておいたほうが行儀がいいのでしょうが今回はハードコードします。

クライアントアプリの作成

とりあえず個人的に一番手軽な Vue.js 使って作ります。Vue CLI を使ってサクッと作ります。

$ vue create sample

で TypeScript と Linter を選んだ状態で作成します。クラススタイルではないものを使うようにしました。 認証ライブラリの msal を入れます。あとで Web API も呼ぶので axios も入れておきます。

$ npm i msal
$ npm i axios

そして、とりあえず今回は App.vue に全部コードを書きます。こんな感じで。

<template>
  <div>
    <button @click="signIn">Sign in</button>
    <button @click="callApi">Call API</button>
    <br/>
    <span>{{ message }}</span>
  </div>
</template>

<script lang="ts">
import Vue from 'vue';
import * as Msal from 'msal';
import axios from 'axios';
 
const redirectUri = window.location.origin;
const appConfig = {
  clientId: '6a8798cd-a9cd-4d06-86d3-32847d293cbc',
  redirectUri,
  scopes: ['https://azureadsampledomain.onmicrosoft.com/myapi/api.read'], // api 用のアプリに定義したスコープ
};
const app = new Msal.UserAgentApplication({
  auth: {
    clientId: appConfig.clientId,
    authority: 'https://azureadsampledomain.b2clogin.com/azureadsampledomain.onmicrosoft.com/B2C_1_signin', // URL の最後はユーザーフローの名前
    validateAuthority: false,
  },
  cache: {
    cacheLocation: 'localStorage',
    storeAuthStateInCookie: true,
  },
});

interface AppData {
  message: string;
  authResult: Msal.AuthResponse | undefined;
}

export default Vue.extend({
  name: 'app',
  data() {
    return {
      message: '',
    } as AppData;
  },
  methods: {
    async signIn() {
      try {
        const result = await app.loginPopup({ scopes: appConfig.scopes });
        this.message = `ログイン成功 ${result.account.name}`;
      } catch (e) {
        this.message = e.message;
      }
    },
    async callApi() {
      try {
        const result = await app.acquireTokenSilent({ scopes: appConfig.scopes });
        const r = await axios.get('https://spaauthtest.azure-api.net/authapitest222/HttpTrigger1?name=okazuki', {
          headers: {
            Authorization: `Bearer ${result.accessToken}`,
          },
        });
        this.message = r.data as string;
      } catch (e) {
        this.message = e.message;
      }
    },
  },
});
</script>

npm run serve で開発サーバー立ち上げて http://localhost:8080 にアクセスして Sign in ボタンを押すと以下のようにログイン画面になります。

f:id:okazuki:20190714200949p:plain

ログインに成功すると以下のように名前が出てきます。

f:id:okazuki:20190714201040p:plain

Call api ボタンを押すと API を呼んで結果が表示されます。

f:id:okazuki:20190714201133p:plain

感想

疲れた…。けどとりあえず動きますね。