Satoshi Warita

Satoshi Warita

Software Engineer

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

この記事では、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) にアップロードします。

GCS

次に Cloud Run サービスのデプロイを行います。

コンテナイメージは envoyproxy/envoy:v1.31-latest、サービス名は envoy-sample、リージョンは asia-northeast1、認証は「未認証の呼び出しを許可」とします。

Cloud Run Service

「コンテナ、ボリューム、ネットワーキング、セキュリティ」の「ボリューム」タブを開き、先ほど envoy.yaml をアップロードした GCS バケットをボリュームとして追加します。

Volume

「コンテナ、ボリューム、ネットワーキング、セキュリティ」の「コンテナ」タブを開き、コンテナポートを 8000 にし、先ほど追加したボリュームをマウントします。

Mount

「コンテナの追加」をクリックして、サンプル API のコンテナも追加します。

Add Another Container

ここまで設定できたらデプロイします。デプロイが完了したらアクセスしてみましょう。

$ 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 を追加するだけです。

Auth0

API を追加できたら、実際にアクセストークンを取得してみましょう。Applications → Applications から Auth0 Applications の一覧画面を開くと、Envoy Sample(Test Application) という Auth0 Application が作られているはずです。

Auth0

この Auth0 Application の設定画面を開き、DomainClient IDClient 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 に関心のある方にとって何かの参考になれば幸いです。



エムシーデジタルに興味を持ってくださった方は、ぜひ採用ページもチェックしてみてください。
エムシーデジタルの採用ページはこちら

参考文献

RSS

Tags

Previous

Masaaki Hirotsu

Masaaki Hirotsu

エムシーデジタルが取り組む Platform Engineering - Tachyon Platform

はじめに VPoT の廣津です。 エムシーデジタルでは、顧客のデータを分析するプロジェクト、機械学習や数理最適化の技術を活用した業務アプリケーション開発、「Tachyon 生成AI」をはじめとし

  • #TechBlog
  • #Platform Engineering
  • #Tachyon Platform

Next

Yuriko Ezaki

Yuriko Ezaki

「開発生産性Conference 2024」参加レポート

はじめに ソフトウェアエンジニアの江﨑です。私は日々の社内基盤の開発業務と並行して、チームの開発プロセスの改善に取り組んでいます。 開発生産性についての知見を深めるため、6月28日から

  • #TechBlog
  • #開発生産性