はじめに
お国の方面でも話題となっているデータ連携基盤ですが、その中でも非パーソナルなデータ連携基盤の構築にはKong API GatewayとFiware Orion Context Brokerの利用が推奨されています。
このデータ連携基盤のコアとなるFiware Orionには、 "Fiware-Service" HTTPヘッダを活用し内部的にDBを分割することで、複数の論理的に隔離されたテナントを一つのFiware Orionでまかなうマルチテナント機能が搭載されています。これは便利な機能なのですが、Fiware-Orionには認証・認可機構はついていないため、実際に利用するためにはOrionとは別のコンポーネントでマルチテナントを意識した認証・認可を実現しなければなりません。
その認証・認可を行うのがAPI Gatewayであり、もう一つの推奨モジュールであるKong API Gatewayの役割ということになります。Kongには公式・非公式問わず様々なプラグインが作られ公開されていますが、認証まわりではBasic認証や認証キーを用いた認証、OIDCなどが利用可能となっています(公式のOIDCプラグインはEnterprise版でしか利用できませんが)。
ただしこれらの認証プラグインはFiware-Orionのマルチテナント機能を解釈できないため、Kongによる認証が成功すれば、どのテナントのデータでもアクセスできてしまいます。これでは困りますので、今回はシンプルなkey-authプラグインをベースに、認証キーとFiware-Serviceを組み合わせたFiware-Orion用認証・認可プラグインを実装してみます。
動かしてみる
とにもかくにも、docker composeを用いてまずは動かしてみます。今回は以下のバージョンで試しました。
Module | Version | Repository |
---|---|---|
Kong API Gateway | kong:3.1.1-alpine | Docker official |
fiware orion | fiware/orion:3.8.0 | Fiware official |
PostgreSQL | postgres:15.1-bullseye | Docker official |
MongoDB | mongo:4.4 | Docker official |
準備
まずはKongとFiware-Orionの環境を準備します。
KongとFiware-Orionの起動
- リポジトリをclone
git clone https://github.com/nmatsui/kong-plugin-FiwarService-KeyAuth.git cd kong-plugin-FiwarService-KeyAuth
- プラグインをインストールしたdocke imageのビルド
docker compose build
- コンテナを起動
docker compose up -d
プラグインの有効化
-
fiwareservice-keyauth
プラグインが読み込まれていることを確認curl -s http://localhost:8001/ \ | jq '.plugins.available_on_server."fiwareservice-keyauth"'
result{ "priority": 1000, "version": "3.1.1" }
- KongにServiceとRouteを定義
curl -i -X POST http://localhost:8001/services \ -d "name=orion" -d "url=http://orion:1026"
curl -i -X POST http://localhost:8001/services/orion/routes \ -d "hosts[]=orion.example.com"
- 定義したRouteでプラグインを有効化
curl -i -X POST http://localhost:8001/services/orion/plugins \ -d "name=fiwareservice-keyauth"
Consumerの定義
Kongには、APIを利用するユーザーを表す Consumer というオブジェクトが備わっています。Fiware-Orionのテナント名をusernameとしてConsumerを登録し、認証キーを割り当てます。今回はtenant1
とtenant2
を登録します。
認証キーの値は適当に変更してください。
-
tenant1
の登録curl -i -X POST http://localhost:8001/consumers \ -d "username=tenant1"
curl -i -X POST http://localhost:8001/consumers/tenant1/key-auth \ -d "key=a_credential_of_tenant1"
-
tenant2
の登録curl -i -X POST http://localhost:8001/consumers \ -d "username=tenant2"
curl -i -X POST http://localhost:8001/consumers/tenant2/key-auth \ -d "key=a_credential_of_tenant2"
Fiware-Orionの認証・認可を試す
準備が整いましたので、Fiware-Orionのマルチテナント機能を試してみましょう。
Tenant1とTenant2の分離
- Tenant1がEntityを登録
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__
resultHTTP/1.1 201 Created ...
- Tenant1は登録したEntityを確認できる
curl -s http://localhost:8000/v2/entities/Room1 -H "Host: orion.example.com" \ -H "Authorization: a_credential_of_tenant1" \ -H "fiware-service: tenant1" | jq .
result{ "id": "Room1", "type": "Room", "pressure": { "type": "Float", "value": 720, "metadata": {} }, "temperature": { "type": "Float", "value": 23, "metadata": {} } }
- Tenant2ではEntityが登録されたことを検知できない
curl -s http://localhost:8000/v2/entities -H "Host: orion.example.com" \ -H "Authorization: a_credential_of_tenant2" \ -H "fiware-service: tenant2" | jq .
result[]
- Tenant2はTenant1のEntityは確認できない
curl -s http://localhost:8000/v2/entities/Room1 -H "Host: orion.example.com" \ -H "Authorization: a_credential_of_tenant2" \ -H "fiware-service: tenant1" | jq .
result{ "message": "Invalid tenant" }
認証されないパターン
- 認証ヘッダがない
curl -i http://localhost:8000/v2/entities -H "Host: orion.example.com"
resultHTTP/1.1 401 Unauthorized ... { "message":"No API key found in request" }
- 認証キーが間違っている
curl -i http://localhost:8000/v2/entities/Room1 -H "Host: orion.example.com" \ -H "Authorization: invalid" -H "fiware-service: tenant1"
resultHTTP/1.1 401 Unauthorized ... { "message":"Invalid authentication credentials" }
認可されないパターン
- Fiware-Serviceヘッダが無い
curl -i http://localhost:8000/v2/entities/Room1 -H "Host: orion.example.com" \ -H "Authorization: a_credential_of_tenant1"
resultHTTP/1.1 403 Forbidden ... { "message":"No Fiware-Service found in request" }
- Fiware-Serviceのテナント名と認証キーが対応しない
curl -i http://localhost:8000/v2/entities/Room1 -H "Host: orion.example.com" \ -H "Authorization: a_credential_of_tenant1" \ -H "fiware-service: tenant2"
resultHTTP/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へ渡すか否かの3つのパラメータだけ受け付けることにしています。それぞれデフォルト値が設定されているので、特に指定しなくても大丈夫です。
パラメータ | 型 | デフォルト値 | 概要 |
---|---|---|---|
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
メソッドはエラーハンドリングを行っているだけです。
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などを返すことになります。
- リクエストからヘッダを取り出す
- 認証ヘッダを探し対応するcredentialオブジェクトを取り出す
- 認証キーに対応するconsumerオブジェクトを検索する
- Fiware-Serviceヘッダがconsumerオブジェクトの名前と一致するか確認する
- 必要に応じてupstreamへ渡すヘッダを書き換える
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
を返します。
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
を返します
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
を返します。
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のマルチテナント機能を認証・認可することができました。今回はシンプルに認証キーを用いた機構を実装しましたが、他の認証プロセスでも同様の機能を実現できると思います。何か良いプラグインを思いついたら、ぜひ皆で共有しましょう!