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