4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

KongAdvent Calendar 2024

Day 16

Kong を使用した JWT 検証の究極ガイド

Posted at

(https://tech.aufomm.com/the-ultimate-guide-of-using-jwt-with-kong/ より翻訳)

この記事は「究極の」ガイドとまでは言えないかもしれませんが、JWT 検証に使える公式の Kong プラグインを徹底的に解説することを目指しています。読み終えるころには、利用可能なオプションを全体的に理解し、それぞれのユースケースに最適なプラグインを選べるようになるはずです。

背景

JSON Webトークン(JWT)は、Web開発に欠かせない要素であり、システム間で重要な情報を安全に伝える役割を果たします。その信頼性を確保するために、JWTの検証は欠かせないステップです。一方、KongのようなAPIゲートウェイは、特にOAuth 2.0やOIDC標準が普及する中で、組織のAPIアーキテクチャにおいて重要な位置を占めています。
image.png

この記事では、KongでJWTを検証するためのツールを詳しく解説し、現代のWeb開発環境でこのプロセスが果たす役割を探ります。

JWTとは

すでにJWTについて理解している方は、どのプラグインを選ぶべきかの詳細を省略し、次のセクションから読み進めてください。

JSON Web Token(JWT)は、JSON Web Signature(JWS)とJSON Web Encryption(JWE)の2つの形式で構成されています。本記事では、主にJWSの検証に焦点を当てています。JWEの検証について知りたい方は、こちらの記事をご覧ください。

RFC7515によると、JSON Web Signature(JWS)は、デジタル署名やメッセージ認証コード(MAC)で保護されたコンテンツを表現するためのJSONベースのデータ構造です。簡単に言えば、JWSは重要なメッセージを含む「署名付きの手紙」のようなものです。この手紙の信頼性を確保するためには、署名を検証する必要があります。

アルゴリズム

JWSで使用される暗号アルゴリズムとその識別子は、別の仕様であるJSON Web Algorithms (JWA) JWAに定義されています。また、これらのアルゴリズムは、JWA仕様に基づいて作成されたIANAレジストリに記載されています。

JWSで使用される暗号アルゴリズムとその識別子は、別の仕様であるJSON Web Algorithms (JWA)仕様に記載されています。

通常、RS256やES256といった非対称アルゴリズムの使用が推奨されます。一方、HS256のような対称アルゴリズムの使用は推奨されていません。

JWTのデータ構造

まず、JWTの構造を見てみましょう。以下に例を示します。

eyJhbGciOiJFUzI1NiIsImtpZCI6Imh1SEE3RDVaTUNKTWhLaVJIZVgwaGZSWG9fX1VBbEpCZ0FkTjhxb0MwMXcifQ.eyJpc3MiOiAiZm9tbSIsICJhdWQiOiAiand0LWRlbW8iLCAic3ViIjogImRlbW8tdXNlciIsICJpYXQiOiAxNjcyNDAzMzc4LCAiZXhwIjogMTY3MjQwMzY3OH0.bxkLGEjN4pXQQ6eymBO_DYl24NGu07FFR1ZXgmdFYHPGsNX10r6iyqDEtCHeXWs7Hsn-QIasV_i4Lw2nCHmlAA```

  • JOSE header
    JOSEヘッダーには、通常、このJWSを保護するために使用されるハッシュアルゴリズム(alg)やキーID(kid)が記載されています。また、typ(タイプ)が含まれていることも一般的です
    {
      "alg": "ES256",
      "kid": "huHA7D5ZMCJMhKiRHeX0hfRXo__UAlJBgAdN8qoC01w"
    }
    
  • Payload
    ペイロードは、保護されて他の相手に渡されるメッセージです
    {
      "iss": "fomm",
      "aud": "jwt-demo",
      "sub": "demo-user",
      "iat": 1672403378,
      "exp": 1672403678
    }
    
  • Signature
    ユーザーは、JOSEヘッダーで定義されたアルゴリズムを使用して、保護されたヘッダーとペイロードの署名を計算し、署名が一致することを確認する必要があります

JWT生成

JWTは非常に人気が高いため、さまざまなプログラミング言語向けのライブラリが利用可能です。また、KeycloakやAzureのようなOIDCプロバイダーを利用している場合、これらが自動的にJWTトークンを生成してくれます。このセクションでは、jwt-cliとPythonのjwcryptoライブラリを使ってJWTを作成する方法を紹介します。

RSA鍵ペアの生成

ここでは、opensslを使用して、トークンの署名用にRSAの秘密鍵を生成し、その検証用に公開鍵をエクスポートします。

openssl genpkey \
  -algorithm RSA \
  -pkeyopt rsa_keygen_bits:2048 \
  -outform pem -out rsa-private.pem 2>/dev/null

openssl pkey -pubout \
  -in rsa-private.pem \
  -out rsa-public.pem

JWT CLI

nixがインストールされている場合は、nix shell nixpkgs#jwt-cliコマンドを使用してjwt-cliを利用できます。それ以外の場合は、公式のGitリポジトリを参照してください。

kid

RFC 7515 section 4.1.4によると、kid(キーID)の値の構造は特に指定されていません。その値は大文字と小文字を区別する文字列でなければなりません。このヘッダー項目の使用は任意です。しかし、ほとんどの場合、JOSEヘッダーにkidを含めることをお勧めします。これにより、受信者がJWK内のどの公開鍵を使用してトークンを検証すべきかを特定しやすくなります。

今回は、特定の公開鍵に関連付けられるように、ツールを使用してkidを生成します。

cat rsa-public.pem \
  | docker run --rm -i danedmunds/pem-to-jwk:latest --jwks-out \
  | jq -r '.keys[].kid' \
  | read JWT_KID

JWT 生成

次に、簡単なペイロードを使用して以下のようにJWTを生成します。

jwt encode \
  --alg RS256 \
  --exp=300s \
  --kid $JWT_KID \
  --iss fomm-jwt-cli \
  --secret @rsa-private.pem \
  '{"username":"fomm","roles":["demo"]}'

JWT 検証

RSAの秘密鍵を使用してトークンに署名しているため、その公開鍵を使って検証を行う必要があります。jwt-cliを使用する場合、以下のコマンドで検証できます。

jwt decode --secret @rsa-public.pem --alg RS256 <JWT>

出力

jwt decode --secret @rsa-public.pem --alg RS256 eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IkNfdTNJemNpSERKWTZ6TkdSeXJZN2prZjZrVWJMRFhEZ0M5TFlEeEtHUXMifQ.eyJleHAiOjE3MzM3MjQyMDAsImlhdCI6MTczMzcyMzkwMCwiaXNzIjoiZm9tbS1qd3QtY2xpIiwicm9sZXMiOlsiZGVtbyJdLCJ1c2VybmFtZSI6IldlbmhhbiJ9.vgVmu3OZXOyyIlmf6sEgsXbWZO88g3a_B9epFIMnFHU0uXhGCvUMiYUsWP8WzfOlWzacJhDsl9MHmQLySXn7IYHSVNBpIQUE-u4FdtKXNIMtvOFNTVZR9TI_ZXBlyvM6wm1wBq-06gwbiA8giGsG4n8Krc-qA9otLEFVCDUP6LtgxoJN_KAHSqEz3vERYEEcXGQeRNBYA6E3x_BzQGRV-royCQk8v-c6x42UQtkcHp1eVphAXTaMOlt-6nsP-nZvkOj1kAk1CbwiehbViDKIb2rJKhWu_aCLGZY0N-E7m6kU_gul9YUmzJqH5DtoAjum3kP3-1tZQk4rlsc7mzMVOw

Token header
------------
{
  "typ": "JWT",
  "alg": "RS256",
  "kid": "C_u3IzciHDJY6zNGRyrY7jkf6kUbLDXDgC9LYDxKGQs"
}

Token claims
------------
{
  "exp": 1733724200,
  "iat": 1733723900,
  "iss": "fomm-jwt-cli",
  "roles": [
    "demo"
  ],
  "username": "Wenhan"
}

Python

PyJWTは、以下のコマンドでpipを使ってインストールできます:

pip install PyJWT

私はnix developを使用する方を好みます。nixがインストールされている場合、以下の内容をflake.nixに保存してください。その後、次のコマンドを実行して環境をセットアップできます:

nix develop -c $SHELL
{
  description = "Example Python development environment for Zero to Nix";
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs";
  };
  outputs = {
    self,
    nixpkgs,
  }: let
    # Systems supported
    allSystems = [
      "x86_64-linux" # 64-bit Intel/AMD Linux
      "aarch64-linux" # 64-bit ARM Linux
      "x86_64-darwin" # 64-bit Intel macOS
      "aarch64-darwin" # 64-bit ARM macOS
    ];
    forAllSystems = f:
      nixpkgs.lib.genAttrs allSystems (system:
        f {
          pkgs = import nixpkgs {inherit system;};
        });
  in {
    # Development environment output
    devShells = forAllSystems ({pkgs}: {
      default = let
        # Use Python 3.11
        python = pkgs.python311;
      in
        pkgs.mkShell {
          # The Nix packages provided in the environment
          packages = [
            # Python plus helper tools
            (python.withPackages (ps:
              with ps; [
                jwcrypto
              ]))
          ];
        };
    });
  };
}

JWT 生成

次に、以下の内容をjwt.pyとして保存します。その後、python jwt.pyを実行することで、トークンを作成できるようになります。

from jwcrypto import jwk, jwt
from datetime import datetime

def generate_jwt(private_key_path, algorithm):
  timestamp = int(datetime.now().timestamp())
  payload = {
    "iat": timestamp,
    "exp": timestamp + 300,
    "iss": "fomm-jwtcrypto",
    "username": "fomm",
    "roles": ["demo"],
  }
  with open(private_key_path, "rb") as pemfile:
    private_key = jwk.JWK.from_pem(pemfile.read())
    jwt_token = jwt.JWT(header={"alg": algorithm, "kid": private_key.thumbprint(), "typ":"JWT"}, claims=payload)
    jwt_token.make_signed_token(private_key)
  return jwt_token.serialize()

def main():
  private_key_path = "rsa-private.pem"
  jwt_algorithm = "RS256"
  jwt_token = generate_jwt(private_key_path, jwt_algorithm)
  print(jwt_token)

if __name__ == "__main__":
  main()

JWT 検証

jwcryptoライブラリを使用した検証は非常に簡単です。以下に使用できるサンプルを示します。

from jwcrypto import jwk, jwt

def validate_jwt(public_key_path, jwt_token):
    with open(public_key_path, "rb") as pemfile:
        public_key = jwk.JWK.from_pem(pemfile.read())
        verified_token = jwt.JWT(jwt=jwt_token, key=public_key)
        try:
            verified_token.validate(public_key)
            return True
        except Exception as e:
            return False

def main():
    public_key_path = "rsa-public.pem"
    jwt_token = input("Enter JWT token: ")
    is_valid = validate_jwt(public_key_path, jwt_token)
    if is_valid:
        print("Token is valid.")
    else:
        print("Token is invalid.")

if __name__ == "__main__":
    main()

どのプラグインを使用すべきか?

JWTの検証には、Kongの公式プラグインが3つ提供されています:

  1. JWTプラグイン
  2. JWT Signerプラグイン(エンタープライズ版)
  3. OpenID Connect(OIDC)プラグイン(エンタープライズ版)

エンタープライズライセンスがない場合は、オープンソース版のJWTプラグインのみ使用可能です。このプラグインを実装するには、各コンシューマーごとに個別のjwt_secretsを作成する必要があります。複数のコンシューマーに同じRSA公開鍵を関連付けたい場合は、すべてのjwt_secrets内のキー値が一意であることを確認してください。詳しくは、こちらのブログ記事をご覧ください。

エンタープライズ版で全てのプラグインが利用可能な場合、以下のことを考えてみてください:

  • JWTはどのように生成されますか?

    • IDP(Identity Provider)によりトークンを生成する場合、OIDCプラグインまたはJWT Signerプラグイン
  • 複数のIDPから発行されたJWTを検証する必要がありますか?

    • OIDCプラグインは、複数のJWKから公開鍵の取得と自動ローテーションをサポートするので、JWT検証に利用可能
  • インターネットアクセスがないUpstream APIサービスがある場合は?

    • JWT Signerプラグインを使えば、トークンを再署名できます。upstream サーバーはKongの公開鍵のみを信頼すれば良いため、IDPのJWK取得が不要
  • JWT claimsを検証する必要がありますか?

    • スコープの検証が必要な場合、OIDCプラグインが最適です。同時に最大4つのクレームが検証可能
  • token claim を読み取り、ヘッダーとして上流や下流に渡す必要がありますか?

    • この場合は、OIDCプラグインを使用する必要がある
  • 異なるIDPから発行された2つのトークンを同時に検証する必要がありますか?

    • 例えば、access tokenとchannel tokenのように2つのトークンを検証する場合、JWT Signerプラグインが適切
  • アクセス制御のためにグループマッピングが必要ですか?

    • すべてのプラグインでコンシューマーマッピングが可能
    • ただし、IDPで開発者を管理したい場合、OIDCプラグインを使用することでトークンクレームに基づいた仮想認証情報を作成できます。この仮想認証情報は、レート制限やアクセス制御に活用可能

まだどのプラグインを選ぶべきか分からない場合は、まずOIDCプラグインから始めることをおすすめします。OIDCプラグインはKongの中で最も高度な認証プラグインであり、他の2つのプラグインよりも多くの機能を提供します。例えば、鍵のローテーション再検出、仮想認証情報、スコープ検証などです。

「OIDCプラグインを強く推奨しているけれど、OpenID Connectプロバイダーを使っていない場合でも使えるの?」と疑問に思うかもしれません。答えはYESです。このプロセスについてもガイドします。

OIDC JWT validation

仕組みは非常にシンプルです。OIDCプラグインはJWKを取得し、これらの公開鍵をキャッシュに保存してJWTの検証に使用します。IDPが鍵をローテーションし、新しい鍵でトークンに署名した場合、OIDCプラグインはJWKエンドポイントから公開鍵を再取得します。

Kongが保持している公開鍵を確認するには、<admin_api>/openid-connect/issuersにアクセスしてください:

JWT Signerは似たような仕組みで動作しますが、1つのURLからのみJWKを取得できる点と、トークンを再署名する点が異なります。
詳細については、こちらのブログ記事をご覧ください。

image.png

JWKs 生成

公開鍵JWKを生成するために、danedmunds/pem-to-jwkのDockerイメージを使用します。

cat rsa-public.pem \
  | docker run -i danedmunds/pem-to-jwk:latest --jwks-out \
  | jq '.keys[] += {alg:"RS256"}' \
  > jwks.json

docker network 生成

コンテナは同じDockerネットワークkong-net内で実行します。

docker network create kong-net

JWKs ホスティング

次に、これらの鍵をホストするためにjson-serverを使用します。

docker run --rm \
  --detach --name jwk \
  --network kong-net \
  -p "3000:3000" \
  --mount type=bind,src="$(pwd)"/jwks.json,dst=/jwk.json \
  williamyeh/json-server --watch /jwk.json

Kong EE起動

デモ用にKongをDBlessモードでデプロイします。Kong Enterpriseの有効なライセンスがKONG_LICENSE_DATAという環境変数に保存されていると仮定します。以下のコマンドで、Kong Enterprise 3.5をDBlessモードで起動できます。

docker run --rm --detach --name kong \
  --network kong-net \
  -p "8000-8001:8000-8001" \
  -e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \
  -e "KONG_PROXY_LISTEN=0.0.0.0:8000" \
  -e "KONG_DATABASE=off" \
  -e "KONG_LICENSE_DATA=$KONG_LICENSE_DATA" \
  kong/kong-gateway:3.5

Kong 設定内容の準備

以下の内容を /tmp/kong.yamlに保存しましょう

_format_version: "3.0"
routes:
- name: upstream-route
  paths:
  - /upstream
  plugins:
  - config:
      echo: true
      status_code: 200
    name: request-termination
services:
- name: test-svc
  host: localhost
  path: /upstream
  port: 8000
  protocol: http
  routes:
  - name: test-route
    paths:
    - /test
    plugins:
    - config:
        auth_methods:
        - bearer
        extra_jwks_uris:
        - http://jwk:3000/db
        issuer: http://jwk:3000/db
        issuers_allowed:
        - fomm-jwt-cli
        - fomm-jwtcrypto
      name: openid-connect

上記の設定で行うことは以下の通りです。

  • request terminationプラグインを使用し、200レスポンスを返し、リクエストをそのままエコーバックします。
  • すべてのJWKはconfig.extra_jwks_urisに配置し、OIDCプラグインが鍵を取得できるようにします。
  • すべてのIssuer (Pythonとjwt-cliコマンド) はconfig.issuers_allowedにリストされています。OIDCプラグインはデフォルトでIssuerを検証します。

最後に、このファイルをKongの/configエンドポイントにプッシュできます。

curl --request POST \
  --url http://localhost:8001/config \
  -F config=@/tmp/kong.yaml

JWT 検証

これで準備が整いましたので、セットアップをテストしてみましょう。最初に上記の手順に従ってJWTを生成してください。簡単に参照できるように、以下のコマンドでJWTをtoken変数に格納します。

cat rsa-public.pem \
  | docker run -i danedmunds/pem-to-jwk:latest --jwks-out \
  | jq -r '.keys[].kid' \
  | read JWT_KID

jwt encode \
  --alg RS256 \
  --exp=300s \
  --kid $JWT_KID \
  --iss fomm-jwt-cli \
  --secret @rsa-private.pem \
  '{"username":"fomm","roles":["demo"]}' \
  | read token

次に、このトークンをauthorization bearer tokenとして使用してエンドポイントを呼び出すと、200レスポンスが返されるはずです。

curl --request GET -i \
  --url http://localhost:8000/test \
  --header "authorization:  Bearer $token"

今日はここまでにします。適切なプラグインを選ぶための参考になれば幸いです。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?