FIWAREのマルチテナント機能をKongで認証・認可する

# はじめに
お国の方面でも[話題](https://www.cas.go.jp/jp/seisaku/digital_denen/dai4/siryou8.pdf)となっているデータ連携基盤ですが、その中でも非パーソナルなデータ連携基盤の構築には[Kong API Gateway](https://github.com/Kong/kong)と[Fiware Orion Context Broker](https://github.com/telefonicaid/fiware-orion)の利用が[推奨](https://data-society-alliance.org/area-data/module/)されています。

このデータ連携基盤のコアとなるFiware Orionには、 **"Fiware-Service"** HTTPヘッダを活用し内部的にDBを分割することで、複数の論理的に隔離されたテナントを一つのFiware Orionでまかなう[マルチテナント機能](https://fiware-orion.readthedocs.io/en/3.8.0/orion-api.html#multi-tenancy)が搭載されています。これは便利な機能なのですが、Fiware-Orionには[認証・認可機構はついていない](https://fiware-orion.readthedocs.io/en/3.8.0/user/security.html#authentication-and-authorization)ため、実際に利用するためにはOrionとは別のコンポーネントでマルチテナントを意識した認証・認可を実現しなければなりません。

その認証・認可を行うのがAPI Gatewayであり、もう一つの推奨モジュールであるKong API Gatewayの役割ということになります。Kongには公式・非公式問わず様々な[プラグイン](https://docs.konghq.com/hub/)が作られ公開されていますが、認証まわりではBasic認証や認証キーを用いた認証、OIDCなどが利用可能となっています（公式のOIDCプラグインはEnterprise版でしか利用できませんが）。
ただしこれらの認証プラグインはFiware-Orionのマルチテナント機能を解釈できないため、Kongによる認証が成功すれば、どのテナントのデータでもアクセスできてしまいます。これでは困りますので、今回はシンプルな[key-authプラグイン](https://docs.konghq.com/hub/kong-inc/key-auth/)をベースに、認証キーとFiware-Serviceを組み合わせたFiware-Orion用認証・認可プラグインを実装してみます。

![consept.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37648/59767c89-fd3a-72fc-a58a-854b8659c95c.jpeg)

# 動かしてみる
とにもかくにも、docker composeを用いてまずは動かしてみます。今回は以下のバージョンで試しました。

| Module | Version | Repository |
|:--|:--|:--|
| Kong API Gateway | kong:3.1.1-alpine | [Docker official](https://hub.docker.com/_/kong) |
| fiware orion | fiware/orion:3.8.0 | [Fiware official](https://hub.docker.com/r/fiware/orion) |
| PostgreSQL | postgres:15.1-bullseye | [Docker official](https://hub.docker.com/_/postgres) |
| MongoDB | mongo:4.4 | [Docker official](https://hub.docker.com/_/mongo) |

## 準備
まずはKongとFiware-Orionの環境を準備します。

### KongとFiware-Orionの起動
1. リポジトリをclone
    ```bash
    git clone https://github.com/nmatsui/kong-plugin-FiwarService-KeyAuth.git
    cd kong-plugin-FiwarService-KeyAuth
    ```
1. プラグインをインストールしたdocke imageのビルド
    ```bash
    docker compose build
    ```
1. コンテナを起動
    ```bash
    docker compose up -d
    ```
### プラグインの有効化
1. `fiwareservice-keyauth`プラグインが読み込まれていることを確認
    ```baah
    curl -s http://localhost:8001/ \
    | jq '.plugins.available_on_server."fiwareservice-keyauth"'
    ```
    ```json:result
    {
      "priority": 1000,
      "version": "3.1.1"
    }
    ```
1. KongにServiceとRouteを定義
    ```bash
    curl -i -X POST http://localhost:8001/services \
    -d "name=orion" -d "url=http://orion:1026"
    ```
    ```bash
    curl -i -X POST http://localhost:8001/services/orion/routes \
    -d "hosts[]=orion.example.com"
    ```
1. 定義したRouteでプラグインを有効化
    ```bash
    curl -i -X POST http://localhost:8001/services/orion/plugins \
    -d "name=fiwareservice-keyauth"
    ```
### Consumerの定義
Kongには、APIを利用するユーザーを表す **Consumer** というオブジェクトが備わっています。Fiware-Orionのテナント名をusernameとしてConsumerを登録し、認証キーを割り当てます。今回は`tenant1`と`tenant2`を登録します。
> 認証キーの値は適当に変更してください。
1. `tenant1`の登録
    ```bash
    curl -i -X POST http://localhost:8001/consumers \
    -d "username=tenant1"
    ```
    ```bash
    curl -i -X POST http://localhost:8001/consumers/tenant1/key-auth \
    -d "key=a_credential_of_tenant1"
    ```
1. `tenant2`の登録
    ```bash
    curl -i -X POST http://localhost:8001/consumers \
    -d "username=tenant2"
    ```
    ```bash
    curl -i -X POST http://localhost:8001/consumers/tenant2/key-auth \
    -d "key=a_credential_of_tenant2"
    ```
## Fiware-Orionの認証・認可を試す
準備が整いましたので、Fiware-Orionのマルチテナント機能を試してみましょう。
### Tenant1とTenant2の分離
1. Tenant1がEntityを登録
    ```bash
    curl -i -X POST http://localhost:8000/v2/entities -H "Host: orion.example.com" \
    -H "Content-Type: application/json" \
    -H "Authorization: a_credential_of_tenant1" \
    -H "fiware-service: tenant1" \
    -d @- << __EOS__
    {
      "id": "Room1",
      "type": "Room",
      "temperature": {
        "value": 23,
        "type": "Float"
      },
      "pressure": {
        "value": 720,
        "type": "Float"
      }
    }
    __EOS__
    ```
    ```text:result
    HTTP/1.1 201 Created
    ...
    ```
1. Tenant1は登録したEntityを確認できる
    ```bash
    curl -s http://localhost:8000/v2/entities/Room1 -H "Host: orion.example.com" \
    -H "Authorization: a_credential_of_tenant1" \
    -H "fiware-service: tenant1" | jq .
    ```
    ```json:result
    {
      "id": "Room1",
      "type": "Room",
      "pressure": {
        "type": "Float",
        "value": 720,
        "metadata": {}
      },
      "temperature": {
        "type": "Float",
        "value": 23,
        "metadata": {}
      }
    }
    ```
1. Tenant2ではEntityが登録されたことを検知できない
    ```bash
    curl -s http://localhost:8000/v2/entities -H "Host: orion.example.com" \
    -H "Authorization: a_credential_of_tenant2" \
    -H "fiware-service: tenant2" | jq .
    ```
    ```json:result
    []
    ```
1. Tenant2はTenant1のEntityは確認できない
    ```bash
    curl -s http://localhost:8000/v2/entities/Room1 -H "Host: orion.example.com" \
    -H "Authorization: a_credential_of_tenant2" \
    -H "fiware-service: tenant1" | jq .
    ```
    ```json:result
    {
      "message": "Invalid tenant"
    }
    ```
### 認証されないパターン
* 認証ヘッダがない
    ```bash
    curl -i http://localhost:8000/v2/entities -H "Host: orion.example.com"
    ```
    ```text:result
    HTTP/1.1 401 Unauthorized
    ...

    {
      "message":"No API key found in request"
    }
    ````
* 認証キーが間違っている
    ```bash
    curl -i http://localhost:8000/v2/entities/Room1 -H "Host: orion.example.com" \
    -H "Authorization: invalid" -H "fiware-service: tenant1"
    ```
    ```text:result
    HTTP/1.1 401 Unauthorized
    ...

    {
      "message":"Invalid authentication credentials"
    }
    ```
### 認可されないパターン
* Fiware-Serviceヘッダが無い
    ```bash
    curl -i http://localhost:8000/v2/entities/Room1 -H "Host: orion.example.com" \
    -H "Authorization: a_credential_of_tenant1"
    ```
    ```text:result
    HTTP/1.1 403 Forbidden
    ...

    {
      "message":"No Fiware-Service found in request"
    }
    ```
* Fiware-Serviceのテナント名と認証キーが対応しない
    ```bash
    curl -i http://localhost:8000/v2/entities/Room1 -H "Host: orion.example.com" \
    -H "Authorization: a_credential_of_tenant1" \
    -H "fiware-service: tenant2"
    ```
    ```text:result
    HTTP/1.1 403 Forbidden
    ...

    {
      "message":"Invalid tenant"
    }
    ```

# 処理の概要
動作することが確かめられましたので、このプラグインがどのような処理をしているか軽く触れてみましょう。実際のコードは https://github.com/nmatsui/kong-plugin-FiwarService-KeyAuth を参照してください。

## データモデルの定義
今回は要件的に**Consumer**オブジェクトを流用できたのため、新たなデータモデルを作ることまではしませんでした。必要であれば独自のEntityをKongに追加することも可能ですが、データベースのmigrationも必要となりちょっと面倒なので、今回は見送りました。

## パラメータの定義
KongのPluginは、登録時にパラメータを受け付けることができます。今回はシンプルに、認証ヘッダ名・Fiware-Serviceヘッダ名・認証キーをFiware-Orionへ渡すか否かの３つのパラメータだけ受け付けることにしています。それぞれデフォルト値が設定されているので、特に指定しなくても大丈夫です。

|パラメータ|型|デフォルト値|概要|
|:--|:--|:--|:--|
|config.auth_header|string|authorization|認証ヘッダ名|
|config.fiwareservice_header|string|Fiware-Service|Fiware-Orionのマルチテナント用ヘッダ名|
|config.hide_credentials|boolean|false|認証キーをupstream（=Fiware-Orion)に渡すか否か。trueにした場合、Orionへ渡されるリクエストヘッダから認証ヘッダと認証キーが削除される|

## 認証・認可処理
### リクエストの受け付け
Kongがクライアントからリクエストを受け付けると、upsteam（=Fiware-Orion）を呼び出す「前」に対応付けられたプラグインの`access`メソッドが呼び出されます。今回は主たる実装は`do_authentication`関数に委譲しており、`access`メソッドはエラーハンドリングを行っているだけです。

```lua:handler.lua
function plugin:access(plugin_conf)
  local ok, err = pcall(do_authentication, plugin_conf)
  if not ok then
    return kong.response.error(err.status, err.message, err.headers)
  end
end
```
### 認証・認可処理の流れ
実際の処理の流れはシンプルです。各関数で何らかエラーが発生した場合、リクエストをFiware-Orionへ転送せず401や403などを返すことになります。
1. リクエストからヘッダを取り出す
1. 認証ヘッダを探し対応するcredentialオブジェクトを取り出す
1. 認証キーに対応するconsumerオブジェクトを検索する
1. Fiware-Serviceヘッダがconsumerオブジェクトの名前と一致するか確認する
1. 必要に応じてupstreamへ渡すヘッダを書き換える

```lua:hander.lua
local function do_authentication(plugin_conf)
  local headers = kong.request.get_headers()
  local credential = get_credential(headers, plugin_conf.auth_header)
  local consumer = get_consumer(credential)
  check_fiwareservice(headers, plugin_conf.fiwareservice_header, consumer)
  rewrite_headers(plugin_conf, credential, consumer)
end
```
### credentialオブジェクトの取り出し
リクエストヘッダから認証ヘッダを取り出します。認証ヘッダが無ければ、`401 Unauthorized`を返します。
また認証ヘッダがあっても対応するcredentialオブジェクトが無く認証キーが不正な場合も、`401 Unauthroized`を返します。

```lua:hander.lua
local function get_credential(headers, auth_header_name)
  local key = headers[auth_header_name]
  if not key or type(key) ~= "string" or key == "" then
    kong.response.set_header("WWW-Authenticate", _realm)
    error(ERR_NO_API_KEY)
  end

  -- キャッシュを検索してcredentialオブジェクトを取得する処理

  if not credential or hit_level == 4 then
    error(ERR_INVALID_AUTH_CRED)
  end
  return credential
end
```
### consumerオブジェクトの検索
Credentialに対応するConsumerオブジェクトを検索します。もし存在しなかった場合は、何らかの設定ミスなので`500 Internal Server Error`を返します

```lua:hander.lua
local function get_consumer(credential)

  -- キャッシュを検索してconsumerオブジェクトを取得する処理

  if not consumer then
    kong.log.err(err)
    error(ERR_INVALID_PLUGIN_CONF)
  end
  return consumer
end
```
### Fiware-Serviceヘッダがconsumerオブジェクトの名前と一致するか確認
リクエストヘッダからFiware-Serviceヘッダを取り出します。Fiware-Serviceヘッダが無ければ、`403 Forbidden`を返します。
またFiware-Serviceヘッダがあっても、対応するConsumerオブジェクトusernameと一致しない場合は`403 Forbiden`を返します。

```lua:handler.lua
local function check_fiwareservice(headers, fiwareservice_header_name, consumer)
  local tenant = headers[fiwareservice_header_name]
  if not tenant or type(tenant) ~= "string" or tenant == "" then
    error(ERR_NO_FIWARESERVICE)
  end

  if tenant ~= consumer.username then
    error(ERR_INVALID_FIWARESERVICE)
  end
end
```

このように、プラグインが実際に行なっている処理内容は、非常にシンプルです。この程度の実装できっちり認証・認可が動作するとは、Kongは良くできてますね！

# まとめ
ということで、KongのプラグインでFIWAREのマルチテナント機能を認証・認可することができました。今回はシンプルに認証キーを用いた機構を実装しましたが、他の認証プロセスでも同様の機能を実現できると思います。何か良いプラグインを思いついたら、ぜひ皆で共有しましょう！