Envoy入門(その5)Auth0 で RBAC
はじめに
マイクロサービスやWeb API界隈では、サービス間のネットワークの制御をライブラリではなく、プロキシのコンテナをサイドカーとして使うのだとか。そのデファクトスタンダード的な立ち位置なのが Envoy さん。
(その4)のおわりに で匂わせた
今回、Lua を取り上げたのは、Auth0 で RBAC(Role-Based Access Control)を設定して、JWT に反映された内容に応じた認可を Envoy で実現したかったためです。
をやります。
やってみた
環境構築
Envoy
(その3)の環境構築と同じリポジトリを git clone して頂いて、Auth0 の送信先をご自分のものに変更すると動くかと思います。
Auth0
Auth0 さんも(その3)の環境構築 の続きで設定をしていきますので、(その3)をやっていない方は、(その3)からお願いします。
Permission
下から設定していきます。
今回は、read:service と write:service を Permission として、これを API に対する許可とします。User には、Role を介して、Permission を設定するため、User は Role で管理できます。Role を実現するための Permission は別で管理しても、User に適切な Permission が付与できるということになります。
Permission は、API から設定するので、Applications の APIs でご自分の API を選択して、Permissions タブを選んで、Add a Permission します。今回は、上のとおり、read:service と write:service の2つを追加しました。
RBAC
同じく、ご自分の API を選択した状態で、Settings タブの下の方の RBAC Settings で、Enable RBAC と Add Permissions in Access Token を ON にします。ON にしただけで反映された感のある UI になっていますが、さらに下の方の Save ボタンを押さないと反映されません。(ハマりポイントです)
Role
Role は、左側のトップメニューの User Management の Roles を選んで、Create Role ボタンを押します。
Name は、Reader と Writer にしましたが、Role よりも上は、Auth0 の世界で整合性が合っていれば、Envoy というか API プログラムの動作・設定には影響はありません。Role の Permissions タブで Permission を指定できますので、Reader には、read:service を Writer には、write:service を Add Permissions ボタンで設定します。
User
Role と同様、User Management の Users を選んで、Create User ボタンを押します。Email は、+ のエイリアスでも通るので、テスト用のユーザーは量産可能です。以下の4ユーザーを作成しました。
| User | Reader | Writer |
|---|---|---|
| +none | ✕ | ✕ |
| +reader | 〇 | ✕ |
| +writer | ✕ | 〇 |
| +both | 〇 | 〇 |
Access Token 取得用 Application
ブラウザから上で作った User で Log in して、Access Token を取得するための Application を作ります。
Applications の Applications から(自分はビビりましたが、ビビらずに)Create Application します。
Name は、My App のままでも良いです。Single Page Web Applications で進めます。(もっと簡単なものもあるかもしれませんが、試してません。)
My App の Quickstart で JavaScript を選んで、Download すると vanillajs-01-login.zip がダウンロードされます。これのコピーは、今回のリポジトリにあるのですが、Download したものは、自分用の auth_config.json が入っていますので、それをクローンしたリポジトリにコピーすると早いです。
その他、画面に書かれているとおり、以下の3か所に同じ URI を設定します。これで、Auth0 さんとアプリが連携できるようになります。
| Name | Value |
|---|---|
| Allowed Callback URIs | http://localhost:3000 |
| Allowed Logout URIs | http://localhost:3000 |
| Allowed Web Origins | http://localhost:3000 |
で、Node.js がセットアップされた環境であれば、基本的には、Download した vanillajs-01-login.zip を解凍して、以下のようにすると動くのですが、まだ、API 側で認可可能な Access Token は取得できません。
$ npm ci
$ npm run dev
いつもと同じように、curl で実験していくので、Chrome のディベロッパーツールのコンソールに Access Token の文字列が表示できるところまで、最小限の改造を行います。
-
auth_config.jsonにaudienceを追加します。ここには、(その3)で作った API の audience を指定します。(その3)の例のままならhttps://zenn.dev/take0aです。 -
auth0/01-login/public/js/app.jsの認証にaudienceを加えて、ログインの直後にAccess Tokenを取得して、コンソールに書きだします。
--- a/auth0/01-login/public/js/app.js
+++ b/auth0/01-login/public/js/app.js
@@ -54,7 +54,10 @@ const configureClient = async () => {
auth0Client = await auth0.createAuth0Client({
domain: config.domain,
- clientId: config.clientId
+ clientId: config.clientId,
+ authorizationParams: {
+ audience: config.audience
+ }
});
};
@@ -121,6 +124,10 @@ window.onload = async () => {
}
console.log("Logged in!");
+
+ const token = await auth0Client.getTokenSilently();
+ console.log("Access Token", token);
+
} catch (err) {
console.log("Error parsing redirect:", err);
}
これで、上で作った4人のユーザーのアクセストークンのJWTが入手できます。
take0a/envoy-samples/auth0
(その3)の復讐
このリポジトリを持ってきて、以下のようにすると、(その3)ができます。
$ docker-compose down
$ FRONT_ENVOY_YAML=config/auth0/v3.yaml docker-compose up --build -d
$ docker-compose ps
RBAC 認可
RBAC 認可できる Envoy で動作確認するには、以下のようにします。
$ docker-compose down
$ FRONT_ENVOY_YAML=config/rbac/v3.yaml docker-compose up --build -d
$ docker-compose ps
Auth0 用の Application のコンソールからコピペした Access Token を使用して、実験します。
まずは、+none のユーザー。認可でエラーになります。
$ curl -v localhost:8000/service --header 'authorization: Bearer eyJ...Bg'
* Trying [::1]:8000...
* Connected to localhost (::1) port 8000
> GET /service HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.3.0
> Accept: */*
> authorization: Bearer eyJ...Bg
>
< HTTP/1.1 403 Forbidden
< content-length: 22
< content-type: text/plain
< date: Thu, 07 Nov 2024 09:49:51 GMT
< server: envoy
<
* Connection #0 to host localhost left intact
JWT validation failed.
次に、+both のユーザー。認可されているので、内側のサービスまで通ります。
$ curl -v localhost:8000/service --header 'authorization: Bearer eyJ...Cg'
* Trying [::1]:8000...
* Connected to localhost (::1) port 8000
> GET /service HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.3.0
> Accept: */*
> authorization: Bearer eyJ...Cg
>
< HTTP/1.1 200 OK
< content-type: text/plain; charset=utf-8
< content-length: 29
< date: Thu, 07 Nov 2024 09:52:04 GMT
< server: envoy
< x-envoy-upstream-service-time: 0
<
* Connection #0 to host localhost left intact
Hello None from behind Envoy!
+reader のユーザーも同様に通ります。
$ curl -v localhost:8000/service --header 'authorization: Bearer eyJ...2g'
* Trying [::1]:8000...
* Connected to localhost (::1) port 8000
> GET /service HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.3.0
> Accept: */*
> authorization: Bearer eyJ...2g
>
< HTTP/1.1 200 OK
< content-type: text/plain; charset=utf-8
< content-length: 29
< date: Thu, 07 Nov 2024 09:52:18 GMT
< server: envoy
< x-envoy-upstream-service-time: 0
<
* Connection #0 to host localhost left intact
Hello None from behind Envoy!
+writer のユーザーは認可エラーになります。
$ curl -v localhost:8000/service --header 'authorization: Bearer eyJ...xg'
* Trying [::1]:8000...
* Connected to localhost (::1) port 8000
> GET /service HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.3.0
> Accept: */*
> authorization: Bearer eyJ...xg
>
< HTTP/1.1 403 Forbidden
< content-length: 22
< content-type: text/plain
< date: Thu, 07 Nov 2024 09:59:09 GMT
< server: envoy
<
* Connection #0 to host localhost left intact
JWT validation failed.
おまけで、+none ユーザーの古い Access Token を使用した場合は、前段の JWT を Auth0 に検証してもらうステップでエラーになります。
$ curl -v localhost:8000/service --header 'authorization: Bearer eyJ...Ng'
* Trying [::1]:8000...
* Connected to localhost (::1) port 8000
> GET /service HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.3.0
> Accept: */*
> authorization: Bearer eyJ...Ng
>
< HTTP/1.1 401 Unauthorized
< www-authenticate: Bearer realm="http://localhost:8000/service", error="invalid_token"
< content-length: 14
< content-type: text/plain
< date: Thu, 07 Nov 2024 09:43:33 GMT
< server: envoy
<
* Connection #0 to host localhost left intact
Jwt is expired
できました。しゅーりょー。
裏側を見ておきます。(その3)との差分です。
40a41
> payload_in_metadata: jwt_payload
46c47,62
<
---
> - name: envoy.filters.http.lua
> typed_config:
> "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
> default_source_code:
> inline_string: |
> function envoy_on_request(request_handle)
> local jwt_authn = request_handle:streamInfo():dynamicMetadata():get("envoy.filters.http.jwt_authn")
> if (jwt_authn ~= nil and jwt_authn.jwt_payload ~= nil and jwt_authn.jwt_payload.permissions ~= nil) then
> for k, v in pairs(jwt_authn.jwt_payload.permissions) do
> if(v == 'read:service') then
> return
> end
> end
> end
> request_handle:respond({[":status"] = "403",},"JWT authorization failed.")
> end
まず、jwt_authn のフィルタで JWT の Payload を jwt_payload として metadata に入れておきます。
続くフィルタは、(その4)で学んだ Lua フィルタで、上の jwt_payload があって、read:service パーミッションがあった場合だけ正常終了して、その他は、403 エラーにします。
この permissions の検査を本番の API のものにすれば、Auth0 の認証に加えて、Auth0 の RBAC 設定による認可も API に反映できました。
おわりに
Envoy で、API のセキュリティを確保するという本来の目的は、一旦、達成できましたので、このシリーズは終了にします。
番外編として、envoy getting started などで調べると、先輩方は、Try Envoy という Katacoda という教材を使用していたという記事が見つかりますが、Katacoda がサービス終了していて、以下の教材のリポジトリはあるもののバージョンが古くて動きません。
この入門記事を書くにあたり、この教材をなんとかできないか?ということで、Katacoda クローンの Killercoda で動くようにしてみました。
このコースのリポジトリは、以下にありますが、今回のシリーズの方が実用的かな?と思いますので、メンテナンスの予定はありません。
Discussion