9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-02-15

はじめに

お国の方面でも話題となっているデータ連携基盤ですが、その中でも非パーソナルなデータ連携基盤の構築にはKong API GatewayFiware 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用認証・認可プラグインを実装してみます。

consept.jpg

動かしてみる

とにもかくにも、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の起動

  1. リポジトリをclone
    git clone https://github.com/nmatsui/kong-plugin-FiwarService-KeyAuth.git
    cd kong-plugin-FiwarService-KeyAuth
    
  2. プラグインをインストールしたdocke imageのビルド
    docker compose build
    
  3. コンテナを起動
    docker compose up -d
    

プラグインの有効化

  1. fiwareservice-keyauthプラグインが読み込まれていることを確認
    curl -s http://localhost:8001/ \
    | jq '.plugins.available_on_server."fiwareservice-keyauth"'
    
    result
    {
      "priority": 1000,
      "version": "3.1.1"
    }
    
  2. 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"
    
  3. 定義したRouteでプラグインを有効化
    curl -i -X POST http://localhost:8001/services/orion/plugins \
    -d "name=fiwareservice-keyauth"
    

Consumerの定義

Kongには、APIを利用するユーザーを表す Consumer というオブジェクトが備わっています。Fiware-Orionのテナント名をusernameとしてConsumerを登録し、認証キーを割り当てます。今回はtenant1tenant2を登録します。

認証キーの値は適当に変更してください。

  1. 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"
    
  2. 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の分離

  1. 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__
    
    result
    HTTP/1.1 201 Created
    ...
    
  2. 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": {}
      }
    }
    
  3. 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
    []
    
  4. 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"
    
    result
    HTTP/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"
    
    result
    HTTP/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"
    
    result
    HTTP/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"
    
    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へ渡すか否かの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メソッドはエラーハンドリングを行っているだけです。

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. リクエストからヘッダを取り出す
  2. 認証ヘッダを探し対応するcredentialオブジェクトを取り出す
  3. 認証キーに対応するconsumerオブジェクトを検索する
  4. Fiware-Serviceヘッダがconsumerオブジェクトの名前と一致するか確認する
  5. 必要に応じてupstreamへ渡すヘッダを書き換える
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を返します。

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を返します

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を返します。

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

9
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?