Azure API Management のインバウンドポリシーで Box REST API に必要なアクセストークン取得処理を試してみた

## 背景と目的

Azure などのパブリッククラウドが普及した理由の一つは、REST API でインフラやデータが簡単に操作できる事だと思います。簡単に操作できるが故に誰でもどの API でも使えると、チームや組織では何かしらのインシデントが発生します。そこでカスタムロールを作り、使って良いまたは使ってはダメな API をチームや組織で精査して管理し、役割分担でインフラ担当とアプリ担当でカスタムロールを分けてインシデント発生リスクを低減する対応がとられます。

クラウドストレージを提供する Box を例にしますが SaaS 全般に言える事として、カスタムロール的な機能が備わっていないため、REST API ごとの利用可否を Box のカスタムアプリ別に管理する事が出来ません。そこで API のリバースプロキシを用意して、チームや組織で利用可と判断した Box REST API が利用でき、かつセキュアにカスタムアプリ別に管理できないかと考えます。API を管理できる製品は色々とありますが、Azure なら API Management があります。

次に Box のカスタムアプリ認証をおこなって Box REST API で必要なアクセストークンを取得する処理は、Azure Functions で実装する事を思い付きますが、Azure API Management にはインバウンドポリシーでどのみち Azure Functions を呼び出して、Box REST API のアクセストークンを Authorization ヘッダーにセットする事になります。だったら Azure Functions を使わず、Box のカスタムアプリ認証を行えないかを考えます。インバウンドポリシーで使えるポリシー式は C# で記述しますが、使用できるクラスライブラリは限定的です。Box カスタムアプリ登録時に選べるサーバー認証には、JWT と クライアント資格情報が選択できます。チームや組織のルールとして、どうしても JWT を使わなければいけない場合は、ポリシー式で使えるクラスライブラリでは対応できないため、Azure Functions を使う必要があります。

前置きが長くなりましたが今回の例は、Box カスタムアプリのクライアント認証部分の処理を Azure API Management のインバウンドポリシーで行い、Authorization ヘッダーにアクセストークンを自動でセットして、Box REST API を実行してみたいと思います。

## 前提条件

無料の個人向けシングルユーザーで Box にサインアップした環境を用意し、開発者コンソールからカスタムアプリ登録を行います。

<https://app.box.com/developers/console>

1. アプリの新規作成
1. カスタムアプリ
1. サーバー認証（クライアント資格情報許可）
1. OAuth2.0資格情報：クライアントシークレットを取得
1. クライアントID、クライアントシークレット、一般設定タブのEnterpriseIDをメモる
1. アプリアクセスレベル：アプリ＋Enterpriseアクセス
1. 承認タブ：確認して送信
1. メールを受け取って承認

### Box カスタムアプリ認証の動作確認

```bash:bash
# 環境変数をセット
client_id=588966....................hzen8h
client_secret=LnxCCGOKs....................WH
enterprise_id=99.....99

# アクセストークンが取得できる事を確認
curl -i -X POST "https://api.box.com/oauth2/token" \
     -H "Content-Type: application/x-www-form-urlencoded" \
     -d "client_id=$client_id" \
     -d "client_secret=$client_secret" \
     -d "grant_type=client_credentials" \
     -d "box_subject_type=enterprise"  \
     -d "box_subject_id=$enterprise_id"
```

以下のようなレスポンスがあれば良い。

```bash:bash
HTTP/2 200 
date: Sat, 21 May 2022 01:14:28 GMT
content-type: application/json
content-length: 110
strict-transport-security: max-age=31536000
set-cookie: box_visitor_id=62883cf4947b81.30726174; expires=Sun, 21-May-2023 01:14:28 GMT; Max-Age=31536000; path=/; domain=.box.com; secure
set-cookie: bv=OPS-45238; expires=Sat, 28-May-2022 01:14:28 GMT; Max-Age=604800; path=/; domain=.app.box.com; secure
set-cookie: cn=62; expires=Sun, 21-May-2023 01:14:28 GMT; Max-Age=31536000; path=/; domain=.app.box.com; secure
set-cookie: site_preference=desktop; path=/; domain=.box.com; secure
cache-control: no-store

{"access_token":"Hrh2QIjtTv..................vmVA","expires_in":3600,"restricted_to":[],"token_type":"bearer"}
```

## Azure API Management リソース作成

```bash:bash
# 環境変数をセット
region=japaneast
prefix=mnrapim

# リソースグループ作成
az group create \
  --name ${prefix}-rg \
  --location $region

# APIM 作成
az apim create \
  --name ${prefix} \
  --resource-group ${prefix}-rg \
  --sku-name Consumption \
  --publisher-email $(az account show --query user.name --output tsv) \
  --publisher-name ${prefix}

# APIM の名前付きの値を作成
az apim nv create \
  --service-name ${prefix} \
  --resource-group ${prefix}-rg \
  --display-name "client_id" \
  --named-value-id client_id \
  --secret true \
  --value $client_id
az apim nv create \
  --service-name ${prefix} \
  --resource-group ${prefix}-rg \
  --display-name "client_secret" \
  --named-value-id client_secret \
  --secret true \
  --value $client_secret
az apim nv create \
  --service-name ${prefix} \
  --resource-group ${prefix}-rg \
  --display-name "box_subject_id" \
  --named-value-id box_subject_id \
  --secret true \
  --value $enterprise_id

# API 作成
az apim api create \
  --service-name ${prefix} \
  --resource-group ${prefix}-rg \
  --api-id myapp \
  --path '/myapp' \
  --display-name myapp \
  --service-url 'https://api.box.com/2.0'

# API の Operation 作成
az apim api operation create \
  --resource-group ${prefix}-rg \
  --service-name ${prefix} \
  --api-id myapp \
  --url-template "/users" \
  --method "GET" \
  --display-name users

# API の All Operations に Inbound ポリシー追加
az rest \
  --method put \
  --url "https://management.azure.com/subscriptions/$(az account show --query id --output tsv)/resourceGroups/${prefix}-rg/providers/Microsoft.ApiManagement/service/$prefix/apis/myapp/policies/policy?api-version=2021-08-01" \
  --body '{
  "properties": {
    "format": "xml",
    "value": "<!-- box bearer token -->
<policies>
    <inbound>
        <base />
        <send-request ignore-error=\"true\" timeout=\"20\" response-variable-name=\"bearerToken\" mode=\"new\">
            <set-url>https://api.box.com/oauth2/token</set-url>
            <set-method>POST</set-method>
            <set-header name=\"Content-Type\" exists-action=\"override\">
                <value>application/x-www-form-urlencoded</value>
            </set-header>
            <set-body>@(\"client_id={{client_id}}&amp;client_secret={{client_secret}}&amp;grant_type=client_credentials&amp;box_subject_type=enterprise&amp;box_subject_id={{box_subject_id}}\")</set-body>
        </send-request>
        <set-header name=\"Authorization\" exists-action=\"override\">
            <value>@(\"Bearer \" + (String)((IResponse)context.Variables[\"bearerToken\"]).Body.As&lt;JObject&gt;()[\"access_token\"])</value>
        </set-header>
    </inbound>
    <backend>
        <base />
    </backend>
    <outbound>
        <base />
    </outbound>
    <on-error>
        <base />
    </on-error>
</policies>"
  }
}
'

# APIM のサブスクリプションキーを取得
apimkey=$(az rest \
  --method post \
  --uri "https://management.azure.com/subscriptions/$(az account show --query id --output tsv)/resourceGroups/${prefix}-rg/providers/Microsoft.ApiManagement/service/${prefix}/subscriptions/master/listSecrets?api-version=2019-12-01" \
  --query primaryKey \
  --output tsv)

# APIM の動作確認
curl -s https://${prefix}.azure-api.net/myapp/users \
  -H "Ocp-Apim-Subscription-Key: $apimkey" \
  | jq .

# 以下のようなレスポンスを確認
{
  "total_count": 1,
  "entries": [
    {
      "type": "user",
      "id": "9999999999",
      "name": "Test Taro",
      "login": "dummy@example.jp",
      "created_at": "2022-05-10T17:23:33-07:00",
      "modified_at": "2022-05-20T16:38:46-07:00",
      "language": "ja",
      "timezone": "Asia/Tokyo",
      "space_amount": 10737418240,
      "space_used": 1890326,
      "max_upload_size": 2147483648,
      "status": "active",
      "job_title": "",
      "phone": "",
      "address": "",
      "avatar_url": "https://app.box.com/api/avatar/large/9999999999",
      "notification_email": []
    }
  ],
  "limit": 100,
  "offset": 0
}
```

## 参考

```bash:bash
# リソースグループを削除
az group delete \
  --name ${prefix}-rg \
  --yes
```

https://docs.microsoft.com/ja-jp/azure/api-management/api-management-policy-expressions#CLRTypes

https://ja.developer.box.com/reference/