Keycloakを使ってAPI GatewayでAPIをアクセス制限する

# 今日やること

アドベンド19日目は、API Gatewayを使ったアクセス制限（認可）をやります。スマホアプリ向けAPIサーバーを、どう防御するかという話です。１日で書きたかったので、かなり長い記事になってしまいました。

<table><tr><td>
:warning: AWS API Gatewayの話は全く出てきません。"AWS"でググって来てしまった人は、申し訳ないですが、ブラウザバックをお願いします。
</td></tr></table>

# API Gatewayとは何か？

例えばあなたが何かWebサービスを作って公開するとしよう。jspや(xがない)aspのような昔ながらのWebアプリケーションでもいいが、今風にサービスのやることはREST APIの公開だけにしよう。さて、このAPIは誰でも使っていいものだろうか？　もちろんそんなことはなく、使える人には制限がいるだろう... 認証は必要そうだ。しかしAPIは通常（認証した人）誰でも等しく使っていいことは無く、○○APIは誰でも可だが××APIは特定の人だけ使用可、とするのが一般的だ。...つまり**アクセス制限（認可）**がいる。

別に俺流認可でもいいが、どうせなら世間でよく使われているプロトコルに合わせたいのが人情だ。世間で使われているプロトコル、OAuth2.0だろう。

OAuth2.0なら作らなくても何かモジュールがあるだろう。Webで検索すれば、OAuth2.0のモジュールやライブラリはたくさん見つけることができる。しかしAPIの立場からみると、これらのモジュールやライブラリからは何の情報を得る必要があっただろうか。ユーザー情報（≒IDトークン）だけである。それだけだったら、わざわざWebコンテナ上のモジュールとして入れなければならないのだろうか。リクエストの前に認可でアクセス制限をするのであるなら、**リバースプロキシーサーバー**ではダメなのだろうか？

API Gatewayとは、雑な理解で済ますと、**アクセス制限をうまいことやってくれるリバースプロキシ―サーバー**である。実際にはアクセス制限だけではなく、流量制限とかロードバランスとか監査ログの出力とかApacheで言うvirtual hostの機能とかこれらの機能を可視化するとか、「認可サービス」として欲しい機能を集中させて、更にWebアプリケーションからも切り離して（疎結合にして）１つの独立したサービスとして提供しよう、という考えである。偉そうな言葉でいうとWebサービス層(service layer)に前に置く認可層(authorization layer)、とか言うのでしょうか。

# API Gatewayの一般的な構成

API Gatewayの本体は、先に書いたようにリバースプロキシ―サーバーなので、APIサーバーの前に置く構成なります。また、ユーザー（というかAPIを使うクライアント）は、このAPI Gatewayの置き場所は通常DMZになり、APIサーバーはその後ろになるかと思います。

![2017-11-22_105314.png](https://qiita-image-store.s3.amazonaws.com/0/208281/932880bd-d464-00a9-ccc8-600743ecf863.png)

ここで一番重要なポイントはAPIサーバー... ではなく「アプリケーション（クライアント）」の部分です。このアプリケーションはもちろんWebアプリケーションでもいいのですが、今回はスマホアプリ（記事中ではWindowsアプリケーションですが）になります。

API利用シーンでのフローは、大体こんな感じになるのではないかと思います。

![2017-11-22_105538.png](https://qiita-image-store.s3.amazonaws.com/0/208281/929043b5-1108-502f-1df6-d777bc7be528.png)

①　ユーザーがアプリケーションを操作する
②　アプリケーションがAPIを使いたいので、**APIサーバーの前にある**API Gatewayにアクセスする
③　権限が無いので 401(Unauthorized)を返す
④　APIへのアクセス権限をもらうために、アプリケーションが認証・認可リクエストをOPに送る
⑤　OPはユーザーに対して認証＆同意を行う

![2017-11-22_105641.png](https://qiita-image-store.s3.amazonaws.com/0/208281/afee0ac5-9a08-e8eb-7d6a-173af61edc65.png)

⑥　OPは認可コードをアプリケーションに渡す
⑦　アプリケーションは認可コードからアクセストークンをもらう
⑧　アプリケーションは、再びアクセストークンを付けてAPI Gatewayへアクセスする
⑨　API Gatewayは、今度はOKなので、APIへプロキシーする
⑩　APIは、ただリクエストに対してレスポンスを返すだけ。アプリケーションはAPIの結果をもらってユーザーにコンテンツを返す。

# 環境を構築する

前置きが長くなってしまいましたが、環境を作っていきましょう。今回はフローで描いたように、OPとしてのKeycloak以外にはAPIとAPI Gateway、APIのクライアントが必要になります。これらを１つづつ作っていきましょう（長いです）。

## Kong（インストール）

API Gatewayとして、オープンスースである[Kong](https://getkong.org/)を使いたいと思います（AWSのAPI Gatewayを期待していた人、すみません）。

### Kongの構成

Kongのインストールの前に、簡単に構成を説明します。そうしないと、何をインストールして良いか分からないので。Kong本体は、最近良くあるWebコンテナが同梱されており、中身は`Nginx`です。Kongの設定は外部のデータベースで（＝別途インストールが要る）、ドキュメントによるとサポートしているデータベースは PostgreSQL 9.4以上 or Cassandra 3系（2017/12時点）だそうです。今回の記事では特にドキュメントデータベースにする必要性はないので、PostgreSQLにしました。

![2017-11-22_105828.png](https://qiita-image-store.s3.amazonaws.com/0/208281/b5f1ff0e-43ae-3fc8-3424-e73bd0e234c5.png)

Kongの設定は、最近良くあるパターンなのか、ユーザー（APIクライアント）がアクセスするモジュールと同じ場所であり、ポートが異なっているだけです。これが困るのは、Kongはユーザー（APIクライアント）からアクセスされるため、DMZに置かざるを得ず、ネットワーク管理者を困らせることになるかもしれない、ということです。ポートが違うのでファイアーウォールの設定を頑張れ、ということなのでしょうが、Kongの管理者機能が外から見えるかもしれない位置にあるのは、ちょっと気持ち悪いです。ただ将来のKongでは分離されるかもしれません。

デフォルトのポートは、ユーザー（APIクライアント）が使う番号は 8000(http)と8443(https)で、管理者が8001(http)と8444(https)です。このポート番号はKongの設定で変えられますが、この記事では面倒なのでデフォルトのまま使ってしまいます。つまりユーザー(APIクライアント)のリクエストは、ポート8000を通してAPIにアクセスされることになります。

### Kongのインストール

前置きが長くなってしまいましたが（２回目）、やっとインストールです。公式のインストール手順を見たい人は[こちら](https://konghq.com/install/)になります。手っ取り早く３行で構築したい人には[dockerが提供](https://ncarlier.gitbooks.io/oss-api-management/content/kong.html)されています。

- PostgreSQLのインストール

PostgreSQLのインストール自体は、通常のインストールと何も変わりません。私はMySQL派なので、細かい設定（特に権限周り）は良く分からないので、適当になっています。PostgreSQLに慣れている人ならば、適切に設定していただいてOKです。

バイナリの入手は[この辺](https://www.postgresql.org/download/linux/redhat/)からRPMを落とします。この記事では、特に深い理由はないですが、 9.6 を選択しました。 何のRPMをインストールして良いかよく分からないので、`-devel`以外を入れましたが、どうやら本体と`-server`だけでよさそうです。

```bash
$ yum install pgdg-centos96-9.6-3.noarch.rpm
$ yum install postgresql96 postgresql96-server postgresql96-libs postgresql96-contrib
$ /usr/pgsql-9.6/bin/postgresql96-setup initdb
```

データベースの権限の設定もしておきます。PostgreSQLはKong本体からのアクセスだけなので、`localhost`(`127.0.0.1`)だけで良いですが、外からデータベースの中を見られると便利なので、この記事では制限なしにしました。また、パスワード有りを設定したかったのですが(`md5`)、OSユーザーとの紐付けを止められず、あきらめました(`trust`)。

```/var/lib/pgsql/9.6/data/pg_hba.conf
# IPv4 local connections:
-host    all             all             127.0.0.1/32            ident
# IPv4 local connections:
+host    all             all             0.0.0.0/0               trust
```

また、PostgreSQLではデフォルトで`locahost`しか`LISTEN`しないので、これも変更しておきます。

```/var/lib/pgsql/9.6/data/postgresql.conf
# - Connection Settings -
-#listen_addresses = 'localhost'
# - Connection Settings -
+listen_addresses = '*'
```

これでサービス起動ができます。

```bash
$ systemctl enable postgresql-9.6
$ systemctl start postgresql-9.6
```

Kongが接続するPostgreSQLのユーザー： `kong` を作ります。

```sql
$ psql -U postgres
postgres=# CREATE USER kong; CREATE DATABASE kong OWNER kong;
postgres=# \l
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
 kong      | kong     | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 postgres  | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 |
 template0 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | en_US.UTF-8 | en_US.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
```

- Kongのインストール

[Kongインストールのページ](https://getkong.org/install/centos/)からRPMをダウンロードします。この記事ではCentOS7用を選択しています。2017/12時点では、Kongのバージョンは 0.11.1でした。ダウンロードしたら`yum`するだけです。

```bash
$ yum install kong-community-edition-0.11.1.el7.noarch.rpm
```

データベースの設定をKongのデフォルト設定で動作するようにしてしまったので、このままKongを起動できますが、PostgreSQLのユーザー名を変えたり、パスワードを設定した場合は、Kongの設定ファイルを変更する必要があります。

Kongの設定ファイルは、実はデフォルトでは無く、そのためすべてデフォルト値で動作します（エラーじゃない）。そのため設定変更する場合は、テンプレート(`/etc/kong/kong.conf.default`)をコピーする必要があります。**設定ファイルは `/etc/kong/kong.conf` に配置**します（Kongの起動時オプションで、パスを指定可）。

```bash
$ cp /etc/kong/kong.conf.default /etc/kong/kong.conf
```

Kongの設定ファイルの説明については、[ドキュメント](https://getkong.org/docs/0.11.x/configuration/)を参照してください。PostgreSQLへ接続する設定に関しては、この辺の値を設定すればよさそうです。

```/etc/kong/kong.conf
database = postgres
pg_host = ...
pg_port = ...
pg_user = ...
pg_password = ...
pg_database = ...
```

migrationした後、サービスを起動します。

```bash
$ kong migrations up
$ kong start
```

Kongの設定ファイルを指定する場合は、オプションに `-c (設定ファイルのパス)` を付けます。ちなみに`kong stop`で停止、`kong restart`で再起動です。

- Kongの起動を確認をする

`curl`で試します。**ポートは管理用なので8001**です。何か長いJSON（Kongの設定情報です）が出てきます。

```bash
$ curl -X GET http://127.0.0.1:8001
{"version":"0.11.1","plugins": ...(超長い)...
```

これでKongのインストールは完了です。

## APIサーバー

Kongで防御されるAPIも用意しましょう。このAPIはAPI Gatewayで防御してくれることを期待して、アクセス制限が全くない張りぼてアプリにします。APIの中身はただのJSONを返すだけのApache上で動作するアプリケーション、どころかただの静的コンテンツ(`plain/text`)を返すだけです。

![2017-11-15_123957.png](https://qiita-image-store.s3.amazonaws.com/0/208281/5e957b20-bbe2-7778-93e6-fa71dff1936d.png)

エンドポイントは次のようにしました。典型的なAPIはCRUD操作だと思うので、それに見立てて４つ用意しました。「許可する予定のメソッド」は、APIとしてはこのメソッドしか許可したくはないが、アクセス制限をさぼって何もしていない、という意味です。（ApacheはデフォルトでPUTとDELETEが許可されていませんが）

|エンドポイントURL|許可する予定のメソッド|説明|
|---|---|---|
|http://172.26.22.66/r/|GET|何かをREADするエンドポイント|
|http://172.26.22.66/c/|POST|何かをCREATEするエンドポイント|
|http://172.26.22.66/u/|PUT|何かをUPDATEするエンドポイント|
|http://172.26.22.66/d/|DELETE|何かをDELETEするエンドポイント|

こんなAPIでも、Kongがちゃんとアクセス制限をしてくれるか、試してみましょう。

## APIクライアント

冒頭の辺りの一般的な構成で描いた通り、APIクライアントはWebアプリケーションでもよいのですが、この記事ではWindowsアプリケーションにしました。OAuth2.0の文脈では、スマホアプリのWindowsアプリケーションもネイティブアプリケーションであるため、Windowsアプリケーションでのやり方や考え方はスマホアプリでも全く同じです。

![2017-11-15_163614.png](https://qiita-image-store.s3.amazonaws.com/0/208281/c788e3f3-a265-0494-7739-7413222d6742.png)

このアプリケーションの左側のフォームは、上部にAPIのエンドポイントURLを指定して「送信」を押すとそのエンドポイントにアクセスするようになっています。下部はそのAPIのレスポンスを、そのまま出力するテキストボックスです。いちおうスマホアプリの画面だと思ってください。右側のモードレスダイアログは、OP(=Keycloak)の設定を入れる画面で、実際にはユーザーに入力させる項目ではありません。そのため右側によけて表示しています。このアプリはテスト用のため入力出来るようになっていますが（その割にはいろいろ入力項目が足りませんが）、実際はインストール時やアプリケーションにハードコーディングすることで、アプリに設定します。

この自作アプリケーションの動作を簡単に説明すると、APIにアクセスして 401(Unauthorized) だった場合、OPに対して認可コードフローを開始します。OPから認可コードをもらってアクセストークンをもらったら、APIに対してもう一度アクセストークンを付けてアクセスします。401(Unauthorized) 以外だった場合は、そのままAPIのレスポンスを表示領域（フォーム下側のテキストボックス）に表示するだけです。また、認可コードフローとは何ぞや、という話は、この記事では書ききれないので、２日前のアドベントを見て欲しいです。

このアプリケーションの動作確認として、直接API (http://172.26.22.66/r/) にアクセスしてみます。今はまだ右側の窓は何の関係がないので省略しています。見ての通り、１行目にステータスコードと２行目以降にレスポンスボディをそのまま表示します。

![2017-11-15_175046.png](https://qiita-image-store.s3.amazonaws.com/0/208281/e7653f6f-6f70-8d1b-e41a-d042e90af569.png)

POSTでも試してみます。APIは何の防御もしていないため、GETと全く同じ内容が返ってきます。

![2017-11-15_175403.png](https://qiita-image-store.s3.amazonaws.com/0/208281/5f74c034-a6d9-ca80-a3de-b543d3f73954.png)

## Keycloak(OP)の設定

APIクライアントはOAuth2.0クライアントでもあるため、Keycloakに設定が必要です。クライアントID：`kong`を作りました。他のパラメータは次のように設定しています。

![2017-11-16_104017.png](https://qiita-image-store.s3.amazonaws.com/0/208281/695371ac-fe4e-6822-1ad3-ffdd122e6157.png)
![2017-11-16_104039.png](https://qiita-image-store.s3.amazonaws.com/0/208281/8eebfaeb-416a-f429-6445-1c5ab1b3d809.png)

一つ注意点があって、リダイレクトURIは http://localhost にしています。もちろんAPIクライアントを実行するWindows PCにWebサーバーがある訳ではないので、宛先はありません。これについては後で説明します。

## 環境の整理

環境構築は終了ですが、登場人物が４つでてきて複雑なため、サーバー情報を整理しておきます。

|サーバー名|役割|接続先URL|
|---|---|---|
|APIサーバー|API提供|http://172.26.22.66|
|Kong|API Gateway|http://127.26.22.29:8000|
|Keycloak|OP|http://172.26.22.5/auth/realms/master|
|Windowsアプリ|APIクライアント|自分のPC|

# Kongで防御してみる

## 単純なプロキシ―のみ（まだ防御なし）

ようやくKongの設定に到達しました。まずはAPIへのプロキシー設定だけやってみます。Kongの管理画面... は無いので、`curl`でREST APIを直接叩くしかないです。管理画面はそのうち実装されるでしょう（Enterprise版だと [あります](https://konghq.com/kong-enterprise-edition/) ）。

KongにAPIを登録します。今やりたい設定は http://172.26.22.29:8000/r/ → http://172.26.22.66/r/ へのプロキシーで、許可するメソッドはGETだけです。（※レスポンスボディのJSONは、見やすいように整形しています）

```bash
$ curl -i -X POST \
  --data "name=myapi-read" \
  --data "hosts=172.26.22.29"
  --data "upstream_url=http://172.26.22.66/r" \
  --data "uris=/r" \
  --data "methods=GET" \
  http://172.26.22.29:8001/apis

HTTP/1.1 201 Created
Date: Wed, 15 Nov 2017 09:07:24 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/0.11.1

{
    "created_at": 1510736844046,
    "strip_uri": true,
    "id": "3d4772a4-9826-4691-8809-ccbc9f62a831",
    "hosts": [
        "172.26.22.29"
    ],
    "name": "myapi-read",
    "methods": [
        "GET"
    ],
    "http_if_terminated": false,
    "preserve_host": false,
    "upstream_url": "http://172.26.22.66/r",
    "uris": [
        "/r"
    ],
    "upstream_send_timeout": 60000,
    "upstream_connect_timeout": 60000,
    "upstream_read_timeout": 60000,
    "retries": 5,
    "https_only": false
}
```

Kong管理者の認証が無いのですが、まぁそのうち実装されるでしょう。パラメータは少し独特です。

|リクエストパラメータ名|説明|
|---|---|
|name|APIの名前（何でも良い）|
|upstream_url|プロキシー先のURL|
|hosts|Kongが受けつける、リクエストのHostヘッダの値。複数ある場合はカンマ区切り|
|uris|Kongが受け取るパス|
|methods|許可するメソッド。複数ある場合はカンマ区切り|

**`uris`が`/r`なので、Kongは http://172.26.22.29:8000/r で受け付け、それを`upstream_uri`の http://172.26.22.66/r へプロキシーする**、と読みます。そして多分`hosts`の存在が奇妙に見えると思います。Kongへリクエストを送信するのですから、KongのFQDN＝Hostヘッダの値になるはずですので、この値の存在意味がよく分からないかもしれません。これは、おそらくApacheで言うvirtual hostのServerNameのようなものを想定しているのだと思われます。

同じように、残り３つのエンドポイント(/c, /u, /d)もKongに登録してしまいましょう。本当はKong上の設定は１つのAPIにしたかったのですが、URLとメソッドが両方異なる場合は、うまく１つの設定にする方法が思い浮かびませんでした。そのうちAPIグループなるものが実装されるでしょう。`curl`を３つ並べても見にくいだけなので、設定を表にしておきます。

|name|hosts|uris|methods|upstream|
|---|---|---|---|---|
|myapi-read|172.26.22.29|/r|GET|http://172.26.22.66/r|
|myapi-create|172.26.22.29|/c|POST|http://172.26.22.66/c|
|myapi-update|172.26.22.29|/u|PUT|http://172.26.22.66/u|
|myapi-delete|172.26.22.29|/d|DELETE|http://172.26.22.66/d|

では、試してみましょう。右窓はまだ不要なので省略しています。

- GET

![2017-11-16_105645.png](https://qiita-image-store.s3.amazonaws.com/0/208281/5b201a69-412a-03c3-0f56-6ea2577376a5.png)

- POST

![2017-11-16_105730.png](https://qiita-image-store.s3.amazonaws.com/0/208281/2e062bbc-4b1d-3d79-b5a8-32b2cf6d3357.png)

ちゃんとPOSTが防御されています。ちなみにstatusが 404(Not Found) でボディが "no API..." となっているのは、[Kongの仕様](https://getkong.org/docs/0.11.x/proxy/#overview)です。

お前の作ったアプリなんか信用できない、という人のために`curl`の結果も載せておきます。

```bash
$ curl -i -X GET http://172.26.22.29:8000/r/
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 15
Connection: keep-alive
Date: Thu, 16 Nov 2017 02:03:34 GMT
Server: Apache/2.4.25 (Unix) OpenSSL/1.0.2k-fips
Last-Modified: Mon, 13 Nov 2017 08:55:41 GMT
ETag: "f-55dd96f59dd40"
Accept-Ranges: bytes
X-Kong-Upstream-Latency: 2
X-Kong-Proxy-Latency: 0
Via: kong/0.11.1

{"hoge","READ"}


$ curl -i -X POST http://172.26.22.29:8000/r/
HTTP/1.1 404 Not Found
Date: Thu, 16 Nov 2017 02:03:39 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: kong/0.11.1

{"message":"no API found with those values"}


$ curl -i -X PUT http://172.26.22.29:8000/r/
HTTP/1.1 404 Not Found
Date: Thu, 16 Nov 2017 02:03:43 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: kong/0.11.1

{"message":"no API found with those values"}


$ curl -i -X DELETE http://172.26.22.29:8000/r/
HTTP/1.1 404 Not Found
Date: Thu, 16 Nov 2017 02:03:50 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: kong/0.11.1

{"message":"no API found with those values"}
```

## APIを防御する

続いてAPIを防御の設定をいれましょう。

- plugin:`jwt`を設定する

JWTで保護するために、APIに対してpluginを入れます。JWTで保護ってアクセストークンじゃないじゃん！　まあKeycloakのアクセストークンはJWTだから問題ないでしょ。普通のOPではアクセストークンはJWTじゃないから、IDトークンにするのかもしれません。（※レスポンスのJSONは整形）

```bash
$ curl -X POST http://172.26.22.29:8001/apis/myapi-read/plugins --data "name=jwt"
{
    "created_at": 1510798731000,
    "config": {
        "secret_is_base64": false,
        "key_claim_name": "iss",
        "anonymous": "",
        "run_on_preflight": true,
        "uri_param_names": [
            "jwt"
        ]
    },
    "id": "bb3cae8b-0bf9-44f4-bf3b-9ea27f418f8c",
    "name": "jwt",
    "api_id": "8478b314-f0aa-49b7-804f-ce0320d4461c",
    "enabled": true
}
```

**API名`myapi-read`に対して、plugin:jwtを有効**にします。`name=jwt`は固定値です。すでにコレだけで防御が掛かっています、が、許可の条件を入れていないので、常に 401(Unauthorized)になります。

- consumerを設定する

consumerというのが、ドキュメントに何の説明もないので意味不明なのですが、APIを使う人（消費者）という意味みたいです。Kongの[API Reference](https://getkong.org/docs/0.11.x/admin-api/#consumer-object)に３行で説明しているので、載せておきます。

> The Consumer object represents a consumer - or a user - of an API. You can either rely on Kong as the primary datastore, or you can map the consumer list with your database to keep consistency between Kong and your existing primary datastore.

> ConsumerオブジェクトはAPIのconsumer、あるいはユーザーを表します。Kongを主のデータストアとして使うか、Kongとあなたが使用している主のデータストアとの間で一貫性を保つために、consumer listをあなたのデータベースにマッピングできます。

```bash
$ curl -X POST http://172.26.22.29:8001/consumers --data "username=myapi"
{"created_at":1510800004000,"username":"myapi","id":"087f5be4-cff5-4e40-b8a0-9b2925712834"}
```

パラメータ `username` は consumerの名前で何でもいいです。

- JWTの署名を検証する公開鍵を設定する

まずはKeycloakから公開鍵を入手しましょう。Keycloakが発行するトークンに対する署名、ということなので、クライアントではなくレルム設定にある公開鍵になります。メニューの「レルム設定」＞「鍵」タブ　から、タイプ「RSA」の「公開鍵」を押して、公開鍵の値をコピーします。

![2017-11-16_115916.png](https://qiita-image-store.s3.amazonaws.com/0/208281/dc621839-6b89-1c9c-4a74-feaaf236d714.png)

![2017-11-16_120001.png](https://qiita-image-store.s3.amazonaws.com/0/208281/2fd57552-657b-0ea5-755d-57927fd8181c.png)

公開鍵を取得したら、いつものように`curl`で登録します。公開鍵の書式は、律儀にPEM形式を要求するため、少し加工してやる必要があります。さっきコピーした公開鍵をテキストに貼りつけて、ヘッダとフッタを追加します。

```text
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1iSFWp3kfdxipF9TtHnB0ZQPUDGCpC7KKpPDq9Rp7TEkMfqpVw7Sq/3iMSsNJtpx1jusmqaMoz8KD7hffVm5UGv7gbsOAAlEp6jYFfZT2kk1iEEBgCW5d67PZQqPbx63rcxAE1wWD42v+3gzKzzVPx+7uqBzfTIXPi1a9eWMmyITYMfYamLnXM8x0NYW8ME4ggrHqEyoHn71xPOkl4KKMOSdKyMg0NnG5YIWq13F0CR/YuPHxbK9FF2ssNkfOwWxpfYBl/oKmjs7BOTgXJnF9cHCs8fRspjJTVOfNdZ3ZH6fHZnBpbh+OxJUGLR1ShG34hZdkjHaRliRxDpddJ8NiQIDAQAB
-----END PUBLIC KEY-----
```

PEMってMIMEだから１行76byteで改行がいるのでは？　というPEMマニアな人のために補足しておくと、不要でした。ヘッダ＆フッタをくっつけたら、`curl`で送信するのでURLエンコードをします。

```text
-----BEGIN+PUBLIC+KEY-----%0D%0AMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1iSFWp3kfdxipF9TtHnB0ZQPUDGCpC7KKpPDq9Rp7TEkMfqpVw7Sq%2F3iMSsNJtpx1jusmqaMoz8KD7hffVm5UGv7gbsOAAlEp6jYFfZT2kk1iEEBgCW5d67PZQqPbx63rcxAE1wWD42v%2B3gzKzzVPx%2B7uqBzfTIXPi1a9eWMmyITYMfYamLnXM8x0NYW8ME4ggrHqEyoHn71xPOkl4KKMOSdKyMg0NnG5YIWq13F0CR%2FYuPHxbK9FF2ssNkfOwWxpfYBl%2FoKmjs7BOTgXJnF9cHCs8fRspjJTVOfNdZ3ZH6fHZnBpbh%2BOxJUGLR1ShG34hZdkjHaRliRxDpddJ8NiQIDAQAB%0D%0A-----END+PUBLIC+KEY-----
```

これでようやく`curl`で送信できます。

```bash
curl -X POST http://172.26.22.29:8001/consumers/myapi/jwt \
  --data "key=http://172.26.22.5/auth/realms/master" \
  --data "algorithm=RS256" \
  --data "rsa_public_key=-----BEGIN+PUBLIC+KEY-----%0D%0AMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1iSFWp3kfdxipF9TtHnB0ZQPUDGCpC7KKpPDq9Rp7TEkMfqpVw7Sq%2F3iMSsNJtpx1jusmqaMoz8KD7hffVm5UGv7gbsOAAlEp6jYFfZT2kk1iEEBgCW5d67PZQqPbx63rcxAE1wWD42v%2B3gzKzzVPx%2B7uqBzfTIXPi1a9eWMmyITYMfYamLnXM8x0NYW8ME4ggrHqEyoHn71xPOkl4KKMOSdKyMg0NnG5YIWq13F0CR%2FYuPHxbK9FF2ssNkfOwWxpfYBl%2FoKmjs7BOTgXJnF9cHCs8fRspjJTVOfNdZ3ZH6fHZnBpbh%2BOxJUGLR1ShG34hZdkjHaRliRxDpddJ8NiQIDAQAB%0D%0A-----END+PUBLIC+KEY-----"

{
  "rsa_public_key": "-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1iSFWp3kfdxipF9TtHnB0ZQPUDGCpC7KKpPDq9Rp7TEkMfqpVw7Sq/3iMSsNJtpx1jusmqaMoz8KD7hffVm5UGv7gbsOAAlEp6jYFfZT2kk1iEEBgCW5d67PZQqPbx63rcxAE1wWD42v+3gzKzzVPx+7uqBzfTIXPi1a9eWMmyITYMfYamLnXM8x0NYW8ME4ggrHqEyoHn71xPOkl4KKMOSdKyMg0NnG5YIWq13F0CR/YuPHxbK9FF2ssNkfOwWxpfYBl/oKmjs7BOTgXJnF9cHCs8fRspjJTVOfNdZ3ZH6fHZnBpbh+OxJUGLR1ShG34hZdkjHaRliRxDpddJ8NiQIDAQAB\r\n-----END PUBLIC KEY-----",
  "created_at": 1510801636000,
  "id": "45cc56d0-660f-41dd-a5ab-6a833ecdfbd4",
  "algorithm": "RS256",
  "key": "http://172.26.22.5/auth/realms/master",
  "secret": "cOjsJz2daBE73YapxhHmde7YMchU4uK1",
  "consumer_id": "087f5be4-cff5-4e40-b8a0-9b2925712834"
}
```

パラメータの意味は次の通りです。

|パラメータ名|説明|
|---|---|
|key|検証するJWTの属性値|
|algorithm|署名のアルゴリズム名|
|rsa_public_key|署名を検証するRSA公開鍵|

この中で最重要パラメータは**`key`で、ここに設定した値とJWTの値を比較**して、許可/拒否を判断します。JWTのどの属性と？　それは`plugin`:**`jwt`で設定した"key_claim_name"**で、デフォルト値は`iss`です。つまり、**JWTの`iss`値と、ここで設定した`key`値を、文字列の完全一致**で比較されます。Keycloakが発行するアクセストークンJWTの`iss`値は "http://172.26.22.5/auth/realms/master" なので、コレを設定しました。

<table><tr><td>
:information_source: JWTの検証は、
<ul>
<li>"key_claim_name"で指定した属性値の完全に一致</li>
<li>JWTの署名の検証</li>
</ul>
が行われます。<strong>JWTの有効期限("exp")は検証されません。</strong>
</td></tr></table>

- 試してみる

設定はこれで完了なので、試してみましょう。アプリケーションの動作が分かりやすいように、フロー図を再掲しておきます。

![2017-11-22_105538.png](https://qiita-image-store.s3.amazonaws.com/0/208281/48f5c8b3-d75b-2a4e-4e70-b8f5ea999cc3.png)

今度は右窓にOPの設定も入力して、アプリケーションからKong(http://172.26.22.29:8000/r/ )にアクセスします**(①②)**。Kongは、アプリケーションのリクエストにアクセストークンが無いので 401(Unauthorized) を返します**(③)**。アプリケーションはアクセストークンをもらうためにKeycloakに対して認可リクエストを送ります**(④)**。Keycloakは未認証なのでログイン画面を返します**(⑤)**。

![2017-11-16_161733.png](https://qiita-image-store.s3.amazonaws.com/0/208281/1f72089d-74d1-80c5-fd1e-4d8ebf67b02d.png)

このアプリケーションでは、Keycloakの画面をアプリケーション内に表示させています（内部は TextBox コントロールを WebBrowser コントロールに切り替えています）。ログイン画面がうまくレスポンシブになってないですが、気にしないことにします。

![2017-11-22_105641.png](https://qiita-image-store.s3.amazonaws.com/0/208281/01bd1553-6d5e-a36c-c21b-9c7faa89ab6c.png)

ログイン画面でユーザー名＆パスワードを入力してログインすると、今はKeycloakには同意画面を出さない設定にしているため、認可コードがリダイレクトURIへ送られます**(⑥)**。認可コードを受け取るリダイレクトURIは... http://localhost です。つまり WebBrowser コントロールは http://localhost にアクセスしようとして、「そんなページは表示できません」画面になるのですが、アプリケーションが欲しいのは認可コードであり、それはクエリパラメータにあり、つまり WebBrowser コントロールの Uri プロパティから取得できます。つまり、アプリケーションに組み込みのブラウザを表示させてしまうと、リダイレクトURIの踏み倒しができてしまいます（組み込みブラウザがセキュリティ上まずい理由の１つでもある）。

セキュリティはともかく、リダイレクトURIを踏み倒して認可コードが奪えるので、アクセストークンが入手できます**(⑦)**。アクセストークンを入手したら、再度Kongにアクセスします**(⑧)**。今度はKongが認可するので、KongはAPIにプロキシーをし**(⑨)**、無事APIからレスポンスがもらえます**(⑩)**。

![2017-11-16_163607.png](https://qiita-image-store.s3.amazonaws.com/0/208281/3b33236e-9cf8-dc41-23d9-df4a613ae102.png)

<table><tr><td>
:warning: この記事のアプリケーションはテスト用であるため、雑な作りになっていますが、本来はアプリケーション上にHTMLを表示してしまうOAuth2.0クライアントアプリケーションはセキュリティ上問題があります。理由は、
<ul>
<li><code>client_secret</code> をアプリケーションに埋め込んでしまう（アプリケーションを解析することで<code>client_secret</code>がばれてしまう。</li>
<li>リダイレクトURIを踏み倒している。（アプリの開発者はリダイレクトURIが何の検証にも役に立っていないことを知っている）</li>
<li>ユーザーにどのURLにアクセスしているかが分からない。（URLバーをアプリに付けても、アプリが勝手に表示しているだけなので、いくらでも詐称できる）</li>
</ul>
といったものがあります。セキュアにやるには、外部ブラウザ（http(s)://～で起動するアプリ）を起動して認証・認可リクエストを投げます。<strike>リダイレクトURIには、アプリケーションが受け取れるようにカスタムスキーマをOSに登録します。カスタムスキーマとは、例えばブラウザのURLに hoge://～ と入力すると、アプリが起動するように関連づけておくことです。</strike> リダイレクトURIには、スマホアプリの場合<a href='https://developer.apple.com/library/content/documentation/General/Conceptual/AppSearch/UniversalLinks.html' target='_blank_'>iOS Universal Links</a>や<a href='https://developer.android.com/training/app-links/index.html' target='_blank_'>Android App Links</a>など、特定のURLをアプリに紐付ける仕組みを利用します。Windowsアプリケーションの場合は、WebBrowserコントロールを使えばURLを直接取得できるので、そのまま利用します。また、<code>client_secret</code>が秘密に保てないので、このOAuth2.0クライアントは publicクライアントとして扱う必要があります。スマホアプリでセキュアにOAuth2.0 / OpenID Connectで連携する方法は、これだけで大きなテーマであるため、この記事では割愛させていだたきます。
</td></tr></table>

- 認可周りの設定の関連（Kongのデータモデル）

GET用エンドポイント( http://172.26.22.29:8000/r/ )についてはこれでよいのですが、まだPOST( http://172.26.22.29:8000/c/ ), PUT( http://172.26.22.29:8000/u/ ), DELETE( http://172.26.22.29:8000/d/ )用のエンドポイントについては、まだ設定していません。API：`myapi-create`, `myapi-update`, `myapi-delete` については、それぞれ「plugin:`jwt`」の設定を入れます。「consumer」と「公開鍵の設定」は、GETと同じ設定が使えるので、APIごとに設定を追加する必要はありあせん。

認可の設定を入れるのに登場人物が多いので、簡単にですが設定の関連を描いておきます。この記事の例では、apisにはエンドポイントに対応して４つAPIを設定しました。pluginsは apis:plugins = 1:1 の関係で設定して、計４つ設定しました。consumersはpluginsとは無関係で設定して１つだけです。jwt_secretsはconsumers:jwt_secrets = 1:1 の関係で１つ設定しました。

![2017-11-22_111124.png](https://qiita-image-store.s3.amazonaws.com/0/208281/f8ba01b1-4db7-e3cd-fbab-e52e295a0854.png)

pluginsはconsumersと 1:1 の関係になれますが、無関係にもできます。この記事の例ではconsumerは理論上不要なはずなのですが、jwt_secretの設定にはconsumerが必須になっているため、consumerを設定しました。将来のKongでは変更されるかもしれません。

## ユーザー単位で防御する

APIの防御は、Keycloakが発行するアクセストークン（というよりJWT）で防御できました。しかし、その判定方法はJWTの`iss`値で、この値はKeycloak固有の値なので常に同じです（正確にはレルム単位で値は異なります）。つまり、APIへのアクセスはKeycloakからもらったアクセストークンを持っている人なら、だれでも使えることになります。このレベルの防御でも十分な場合はありますが、せっかく認可をやっているので、もう少し頑張りましょう。ユーザーによってアクセス可/否するようにします。

- JWTの指定した属性を見るようにする(plugin)

アクセス制限がユーザー単位でコントロールできていないは、JWTの`iss`値を見ているからで、これをユーザー名やユーザーIDから判断するようにすれば良い、ということになります。`iss`値を見る設定が入っているのはpluginなので、これを変更します。またKeycloakのアクセストークンは、どの属性にユーザー名が入っているかが重要なのですが、`preferred_username`が良さそうです。

いつものように`curl`で設定します。

```bash
$ curl -i -X PATCH --data "config.key_claim_name=preferred_username" \
"http://172.26.22.29:8001/apis/myapi-read/plugins/bb3cae8b-0bf9-44f4-bf3b-9ea27f418f8c"
HTTP/1.1 200 OK
Date: Fri, 17 Nov 2017 02:35:25 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Access-Control-Allow-Origin: *
Server: kong/0.11.1

{
    "created_at": 1510798731000,
    "config": {
        "key_claim_name": "preferred_username",
        "secret_is_base64": false,
        "anonymous": "",
        "run_on_preflight": true,
        "uri_param_names": [
            "jwt"
        ]
    },
    "id": "bb3cae8b-0bf9-44f4-bf3b-9ea27f418f8c",
    "name": "jwt",
    "api_id": "8478b314-f0aa-49b7-804f-ce0320d4461c",
    "enabled": true
}
```

パラメータがややこしいのですが、URLにある`myapi-read`はAPI名ですが、.../plugins/の後ろに`bb3c...`はpluginの`id`です。なぜかplugin名(`jwt`)が指定できないので（[ドキュメントには出来る](https://getkong.org/docs/0.11.x/admin-api/#update-plugin)と書いてある）、`id`で指定しています。設定値自体は、リクエストボディに`config.(属性名)`で指定します。今回は`key_claim_name`を`iss`→`preferred_username`にしたいので、`config.key_claim_name=preferred_username`を指定します。ちなみにHTTPメソッドは`PATCH`であって`PUT`はダメでした。Kongの更新系APIは`PATCH`と`PUT`が混じっていて、ちょっと整理されていない感じがします。

一旦消してから作りなおしてたい人は、DELETEで消すことができます。

```bash
$ curl -i -X DELETE http://172.26.22.29:8001/apis/myapi-read/plugins/bb3cae8b-0bf9-44f4-bf3b-9ea27f418f8c
```

- 比較する値を変更する(jwt_secret)

比較する属性名を変更したので、属性値自体を変更します。これは公開鍵を設定した箇所で、`key`の値で設定しました。ユーザー`u001`だけ許可するように設定します（ユーザーはKeycloakに作成しておいてください）。

```bash
$ curl -i -X PUT --data "key=u001" http://172.26.22.29:8001/consumers/myapi/jwt

{
    "created_at": 1510887959000,
    "id": "7c493182-f49d-4cd8-8cf1-a197f4763cce",
    "algorithm": "RS256",
    "key": "u001",
    "secret": "QLgb9vQK0r9YbSD0b0MP0U6IPBFEzCpl",
    "consumer_id": "087f5be4-cff5-4e40-b8a0-9b2925712834"
}
```

- 試してみる

試してみましょう。まずはユーザー`u001`でKongが認可してくれるかを試します。

![2017-11-17_121349.png](https://qiita-image-store.s3.amazonaws.com/0/208281/97b21223-6106-60a2-c5c5-95a08386ae6d.png)

![2017-11-17_121413.png](https://qiita-image-store.s3.amazonaws.com/0/208281/dec56c6b-4c7f-397c-61f5-5024d4fc5b9e.png)

無事に認可されました。続いて権限がないユーザー`u111`で試してみます。

![2017-11-17_121654.png](https://qiita-image-store.s3.amazonaws.com/0/208281/29269aae-0453-22b3-c418-f56544d231f7.png)

![2017-11-17_121711.png](https://qiita-image-store.s3.amazonaws.com/0/208281/6143ebfb-1adf-28da-3bfb-6f9094ab2269.png)

無事に認可されず 403(Forbidden)になりました。

# まとめ

長い記事になってしまったので、いくつかのポイントに分けてまとめたいと思います。

- Kongの認可について（JWT検証）

今回行った認可は OAuth2.0 というより、ただのJWTにある属性値の一致だけでした。属性値の検証条件は、一致だけではなく正規表現など、もう少し柔軟な設定がそのうち実装されると思います。しかし、一番行いたい検証は `scope` 値で、`scope=foo bar` と `scope=bar foo`は同一権限を表しますが、現在この２つを同じと見なす設定はできず、また正規表現でも不可能（か、ものすごく難しい）であるため、将来のバージョンでそこまで実装してくれるかが、気になるところです。

またJWTでの検証という点も少々使いずらく、というのはアクセストークンは通常ただの文字列であり、JWTではないからです。KeycloakはアクセストークンがJWT形式であるという仕様のため、特に何も考えずにKongで認可が出来ましたが、大抵のOPやOP製品はそうはいかないでしょう。IDトークンはJWT形式ですが、IDトークンの目的（認証に使用）とトークンの有効期限（リフレッシュがない）を考えると、ちょっと使いずらいように思います。

- Kongの認可について（アクセストークン）

OAuth2.0での認可は、やはりアクセストークンを使った方式なので、KongはRPからもらったアクセストークンをOPのトークン・インストロスペクション・エンドポイントに送信して、権限情報を取得するようになって欲しいです。実はデータベースを見ると、`oauth2_xxx`というテーブルがあるため、すでに実装されている可能性がありますが、まだドキュメントに記載がないため、どう設定してどう使うのかが良く分かりません。近い将来ドキュメントを書いてくれるでしょう。そのときにまた試してみていいと思います。

![2017-11-22_112756.png](https://qiita-image-store.s3.amazonaws.com/0/208281/f7373f66-8a9c-4ff1-5a1d-ed1e66715a36.png)

- Kong本体について

2017/12時点でバージョン 0.11.1 ですが、まだ時期尚早感があります。ドキュメントはそこそこ書かれていますが、すべてが書かれているのはAPI referenceくらいで、さらに例が無いため、なかなか厳しいものがあります。また管理画面（GUI）が無いのが、実際の運用を考慮するとかなり厳しく、これは本当に早く欲しいところです。`curl` での入力は、今回のような一番単純な例でも結構大変だったのではないでしょうか。ちなみに私は直接データベースを更新しました。

- 全体として

今回の記事は、一番簡単な認可の例だけで、しかも認可の機能しか見ていないのにも関わらず、長い記事になってしまいました。まぁ半分くらい構築なんですが。本当はログとかロードバランスも見てみたいところではありましたが、また別の機会にしたいと思います。

おそらくKongの直接の競合は、現時点では AWS API Gateway になると思いますが、機能面はともかく（１年後のKongはそこそこ実装されるでしょうし、多分API Gatewayとしての機能には大差が無くなっていると思う）、設定しやすさ、使いやすさが比較対象になりそうに思います。多分スクリプトを含めたノン・コーディングで設定ができるかが鍵になりそうな気がします。システム屋の人ならスクリプトにの１つや２つは造作のないことなのでしょうが、実際一番操作をする人は運用であることを忘れてはいけません。

また、単に要件上「AWSが使えない」ため、選択の余地が無い場合もあると思います。そういう人のためにも、早くKongバージョン1.0がリリースされて欲しいと思います。

# 参考資料

- [QiitaにあるKongの記事](https://qiita.com/tic40/items/c437bfecc7df0d6e7095)

QiitaにすでにKongを試した人がいました。KongのドキュメントはAPIリファレンスを見ながらで例もないので、参考になりました。

- [Kongのドキュメント](https://getkong.org/docs/)

見ない訳にはいかない。

- [KeycloakでKongと連携する](https://ncarlier.gitbooks.io/oss-api-management/content/howto-kong_with_keycloak.html)

どんなAPIを使う必要があるのかが分かります。

- [NRI OpenStandia Keycloak日本語ドキュメント](http://keycloak-documentation.openstandia.jp/master/ja_JP/authorization_services/index.html)