Cloud Run + Envoy + Auth0 で実現する JWT 認証付き API
はじめに
はじめまして。エムシーデジタルでソフトウェアエンジニアをしている割田です。この記事では、Cloud Run + Envoy + Auth0 による JWT 認証付き API の実装方法について説明します。
背景
広く利用されている API の認証方法として JWT(JSON Web Token) 認証があります。これは、クライアントがリクエスト時に認証サーバから取得した JWT 形式のアクセストークンを添付し、サーバがアクセストークンの正当性を検証するという仕組みです。 ほとんどの言語にはライブラリが存在するため、JWT 認証の実装はさほど難しくありません。とはいえ、アプリケーションごとに JWT 認証を実装していると手間がかかるため、アプリケーションのコードに手を入れることなく JWT 認証を実装する方法が欲しくなります。
ここで登場するのが Envoy です。Envoy はクラウドネイティブアプリケーション向けに設計されたオープンソースのプロキシです。アプリケーションの前に Envoy を配置し、Envoy 側で JWT 認証を行うことで、アプリケーションのコードに手を入れることなく JWT 認証を実装できます。
この記事では、Envoy を利用して、実際に JWT 認証付き API を実装し、 Cloud Run 上にデプロイする手順を説明します。
実装方法
0. 前提
以下、次のことを前提とします。
- ローカルに Docker 環境がある
- Google Cloud のアカウントをもっている
- Auth0 のアカウントをもっている
- Envoy のバージョンは 1.31
1. サンプル API の Docker イメージを用意する
hashicorp/http-echo
という Docker イメージをサンプル API として利用します。試しにこのイメージをローカルで動かしてみます。
$ docker run -p 5678:5678 hashicorp/http-echo -text="hello world"
localhost
のポート 5678 にアクセスするとレスポンスが返ってきます。
$ curl localhost:5678
hello world
2. Envoy のベース設定ファイルを用意する
次にベースとなる Envoy の設定ファイルを用意します。とくに何もせず、リクエストをバックエンドにパスするだけの設定を以下に示します。
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: 8000
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: edge
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
route_config:
virtual_hosts:
- name: all_domains
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: backend
clusters:
- name: backend
type: STRICT_DNS
connect_timeout: 1s
load_assignment:
cluster_name: backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: 127.0.0.1
port_value: 5678
Envoy の基本要素として listener
, filter
, cluster
があります。listener
はリクエストをどのポートで受け付けるかといった設定、filter
はリクエストに適用するフィルタ(ルール)の設定、cluster
はバックエンドの設定です。これらを順番に見ていきましょう。
listeners:
と書いてあるところが listener
の設定です。ここではポート 8000 でリクエストを受け付けるという設定をしています。
filters:
と書いてあるところが filter
の設定です。ここでは、全てのリクエストを backend
という名前の cluster
にルーティングするという設定をしています。
clusters:
と書いてあるところが cluster
の設定です。ここでは backend
という名前の cluster
を定義し、そのアドレスは localhost
のポート 5678 であると設定しています。
さらに詳しく知りたい方は公式の Configuration reference を参照してください。
上記の設定内容を envoy.yaml
という名前で保存します。
3. Cloud Run にデプロイする
準備ができたので、サンプル API と Envoy を Cloud Run にデプロイしてみましょう。
まず、 コンテナにマウントできるよう envoy.yaml
を GCS(Google Cloud Storage) にアップロードします。
次に Cloud Run サービスのデプロイを行います。
コンテナイメージは envoyproxy/envoy:v1.31-latest
、サービス名は envoy-sample
、リージョンは asia-northeast1
、認証は「未認証の呼び出しを許可」とします。
「コンテナ、ボリューム、ネットワーキング、セキュリティ」の「ボリューム」タブを開き、先ほど envoy.yaml
をアップロードした GCS バケットをボリュームとして追加します。
「コンテナ、ボリューム、ネットワーキング、セキュリティ」の「コンテナ」タブを開き、コンテナポートを 8000 にし、先ほど追加したボリュームをマウントします。
「コンテナの追加」をクリックして、サンプル API のコンテナも追加します。
ここまで設定できたらデプロイします。デプロイが完了したらアクセスしてみましょう。
$ curl https://envoy-sample-zk2mdxs4yq-an.a.run.app
"hello world"
4. Auth0 の設定
Envoy とサンプル API を Cloud Run にデプロイできたので、JWT 認証の実装に進みましょう。
JWT 認証では、クライアントと API に加えてアクセストークンをクライアントに払い出す認証サーバが必要になります。認証サーバを自前で用意することもできますが、ここでは認証プラットフォームの Auth0 を利用することにします。
設定としては、Auth0 コンソールの Applications → APIs から API 一覧画面を開き、新規 API を追加するだけです。
API を追加できたら、実際にアクセストークンを取得してみましょう。Applications → Applications から Auth0 Applications の一覧画面を開くと、Envoy Sample(Test Application)
という Auth0 Application が作られているはずです。
この Auth0 Application の設定画面を開き、Domain
、Client ID
、Client Secret
の値を確認します。確認できたら以下のコマンドを実行して、Auth0 が提供するトークンエンドポイントからアクセストークンを取得します。
$ curl --request POST \
--url 'https://{Your Domain}/oauth/token' \
--header 'content-type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials \
--data client_id={Your Client ID} \
--data client_secret={Your Client Secret} \
--data audience=https://envoy-sample
{"access_token":"{Access Token}","expires_in":86400,"token_type":"Bearer"}%
5. Envoy の設定の修正
Envoy の設定を修正し、JWT 認証を実装しましょう。これは JWT Authentication フィルタを追加するだけで OK です。envoy.yaml
の差分を以下に示します。
diff --git a/envoy.yaml b/envoy.yaml
index 35cdb8d..1e14cba 100644
--- a/envoy.yaml
+++ b/envoy.yaml
@@ -12,6 +12,26 @@ static_resources:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: edge
http_filters:
+ - name: envoy.filters.http.jwt_authn.v3.JwtAuthentication
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
+ providers:
+ auth0:
+ issuer: https://{Your Domain}/
+ audiences:
+ - https://envoy-sample
+ remote_jwks:
+ http_uri:
+ uri: https://{Your Domain}/.well-known/jwks.json
+ cluster: auth
+ timeout: 1s
+ forward: true
+ forward_payload_header: x-jwt-payload
+ rules:
+ - match:
+ prefix: /
+ requires:
+ provider_name: auth0
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
@@ -38,3 +58,21 @@ static_resources:
socket_address:
address: 127.0.0.1
port_value: 5678
+ - name: auth
+ type: STRICT_DNS
+ dns_lookup_family: V4_ONLY
+ connect_timeout: 1s
+ load_assignment:
+ cluster_name: auth
+ endpoints:
+ - lb_endpoints:
+ - endpoint:
+ address:
+ socket_address:
+ address: {Your Domain}
+ port_value: 443
+ transport_socket:
+ name: envoy.transport_sockets.tls
+ typed_config:
+ "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
+ sni: {Your Domain}
issuer:
では JWT の iss
クレーム(JWT の発行者を表すフィールド)に入るべき値を設定しています。添付された JWT の iss
クレームの値が設定された値とマッチしない場合、Envoy は 401 Unauthorized エラーを返します。
audiences:
では JWT の aud
クレーム(JWT の利用対象を表すフィールド)に入るべき値を設定しています。iss
と同様、aud
クレームの値が設定された値とマッチしない場合、Envoy は 401 Unauthorized エラーを返します。
remote_jwks:
では JWT の検証に用いる公開鍵を取得する JWKS(JSON Web Key Set) の URL を設定しています。これを正しく設定しないと、JWT の検証に失敗します。
rules:
では JWT 認証をどのパスに対して適用するかを設定しています。今回は全てのパスに対して JWT 認証を適用する設定をしています。
envoy.yaml
の修正が完了したら、GCS にアップロードして元ファイルを上書きしましょう。Envoy は動作中に設定を修正してもすぐに反映されるため、再デプロイなどは不要です。あらためてアクセスしてみると、アクセストークンがないとエラーが返ってくるようになったことがわかります。
$ curl https://envoy-sample-zk2mdxs4yq-an.a.run.app
Jwt is missing%
$ curl -H "Authorization: Bearer $TOKEN" https://envoy-sample-zk2mdxs4yq-an.a.run.app
"hello world"
まとめ
この記事では Envoy を利用することで、アプリケーションのコードに手を入れることなく JWT 認証を実装する方法について説明しました。
Envoy は非常に拡張性が高く、今回紹介した以外にもさまざまなことができます。例えば、JWT Authentication フィルタの代わりに OAuth2 フィルタを用いることで、アクセストークンがないときにエラーを返すのではなく、ログイン画面にリダイレクトするといったことができます。とくにフロントエンドアプリケーションの場合はこのような挙動の方が望ましいでしょう。
この記事が Envoy に関心のある方にとって何かの参考になれば幸いです。
エムシーデジタルに興味を持ってくださった方は、ぜひ採用ページもチェックしてみてください。
▶エムシーデジタルの採用ページはこちら