Python-JOSE 完全ガイド: JWT/JWS/JWEをマスターしよう!🔐

Python

こんにちは! 👋 この記事では、PythonでJSONベースのセキュリティトークンを扱うための強力なライブラリ、python-joseについて徹底解説します。Web APIの認証・認可や、安全なデータ交換に関わる開発者にとって、JOSE (JSON Object Signing and Encryption) の概念とそれを実装するライブラリの理解は不可欠です。python-jose は、JWT (JSON Web Token), JWS (JSON Web Signature), JWE (JSON Web Encryption) といったJOSEファミリーの仕様をPythonで簡単に扱えるようにしてくれます。この記事を通して、python-jose の基礎から応用までを学び、あなたのプロジェクトに活かせる知識を身につけましょう!

JOSE とは何か? 🤔

まず、python-jose が実装しているJOSEについて簡単に触れておきましょう。JOSE (JavaScript Object Signing and Encryption) は、インターネット標準を定めるIETFによって策定された、JSONデータ構造を用いてデジタル署名や暗号化を行うための仕様群です。これにより、データの完全性(改ざんされていないこと)や機密性(内容が秘匿されていること)を保証しつつ、Webフレンドリーな方法で情報をやり取りできます。

JOSEは主に以下の仕様から構成されます:

  • JWS (JSON Web Signature): JSONデータにデジタル署名を付与するための仕様 (RFC 7515)。データの改ざん検知と送信者の認証に使われます。
  • JWE (JSON Web Encryption): JSONデータを暗号化するための仕様 (RFC 7516)。データの機密性を保護します。
  • JWK (JSON Web Key): 暗号鍵をJSON形式で表現するための仕様 (RFC 7517)。鍵の交換や管理を容易にします。
  • JWA (JSON Web Algorithms): JWSやJWEで使用される暗号アルゴリズムを定義する仕様 (RFC 7518)。
  • JWT (JSON Web Token): JWSまたはJWEを用いてクレーム(主張、属性情報)を安全に転送するためのコンパクトなトークン形式 (RFC 7519)。主に認証や認可情報(セッション情報など)の伝達に広く利用されています。

python-jose はこれらの仕様を包括的にサポートしており、Pythonアプリケーションで簡単にJOSEの機能を利用できるように設計されています。

インストール方法 💻

python-jose を使い始めるには、まずインストールが必要です。pip を使って簡単にインストールできます。python-jose は暗号化操作のためにいくつかのバックエンドライブラリに依存しており、用途に応じて選択してインストールすることが推奨されています。

最も推奨されるバックエンドは cryptography ライブラリです。以下のコマンドでインストールします。

pip install python-jose[cryptography]

cryptography をインストールすると、これが優先的に使用されます。

他のバックエンドとして pycryptodome や、rsaecdsa を利用する native-python バックエンドもありますが、cryptography が最も機能豊富で推奨されています。native-python バックエンドは常にインストールされますが、他のバックエンドが存在する場合はそちらが優先されます。

注意: python-jose の開発は2021年以降停滞しており、セキュリティ上の懸念(例:CVE-2024-33663, CVE-2024-33664)も報告されています (2024年情報)。また、Python 3.10以降では依存関係の問題が発生する可能性があります。新規プロジェクトでは、より活発にメンテナンスされている代替ライブラリ(PyJWT, Authlib/joserfc など)の利用を検討することをお勧めします。FastAPIのドキュメントなどでも、以前は python-jose を推奨していましたが、現在は代替ライブラリへの移行が議論されています。

JWT (JSON Web Token) の利用 🔑

JWTは、認証情報やユーザープロファイルなどの「クレーム」を安全に転送するための一般的な方法です。python-jose を使うと、JWTの生成(エンコード)と検証(デコード)が簡単に行えます。

JWTは以下の3つの部分から構成され、それぞれBase64URLエンコードされてピリオド (.) で連結されます。

  1. ヘッダー (Header): トークンのタイプ (通常 “JWT”) と署名アルゴリズム (例: “HS256”, “RS256”) を含むJSONオブジェクト。
  2. ペイロード (Payload): クレーム(実際の情報)を含むJSONオブジェクト。クレームには、予約済みクレーム(iss, sub, aud, exp など)、パブリッククレーム、プライベートクレームがあります。
  3. 署名 (Signature): ヘッダーとペイロードを結合したものに、指定されたアルゴリズムと秘密鍵を使って生成された署名。これにより、トークンが改ざんされていないこと、そして送信者が正規の鍵を持っていることを確認できます。

JWTの生成 (エンコード)

jose.jwt.encode() 関数を使ってJWTを生成します。ペイロード(辞書型)、秘密鍵、そして署名アルゴリズムを指定します。

from jose import jwt, exceptions

# ペイロード (含めたい情報)
payload = {
    'sub': '1234567890',
    'name': 'John Doe',
    'iat': 1516239022,
    'admin': True
}

# 秘密鍵 (実際には安全な方法で管理してください)
secret_key = 'your-256-bit-secret' # HS256の場合、256ビット(32バイト)以上の鍵が推奨されます

# アルゴリズム
algorithm = 'HS256' # HMAC-SHA256

# オプションのヘッダー
headers = {'kid': 'my-key-id-1'}

try:
    encoded_jwt = jwt.encode(payload, secret_key, algorithm=algorithm, headers=headers)
    print(f"生成されたJWT: {encoded_jwt}")
    # 例: eyJhbGciOiJIUzI1NiIsImtpZCI6Im15LWtleS1pZC0xIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJhZG1pbiI6dHJ1ZX0.bN6B...
except exceptions.JOSEError as e:
    print(f"JWTエンコードエラー: {e}")

JWTの検証 (デコード)

jose.jwt.decode() 関数を使ってJWTを検証し、ペイロードを取得します。トークン文字列、公開鍵または秘密鍵(アルゴリズムによる)、そして検証に使用するアルゴリズム(複数指定可能)を指定します。

from jose import jwt, exceptions

# 検証したいJWT (上記で生成したものなど)
token_to_verify = "eyJhbGciOiJIUzI1NiIsImtpZCI6Im15LWtleS1pZC0xIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJhZG1pbiI6dHJ1ZX0.bN6B..."

# 検証に使用する鍵 (エンコード時と同じもの)
secret_key = 'your-256-bit-secret'

# 検証を許可するアルゴリズムのリスト
algorithms = ['HS256']

# オプション: audience や issuer の検証
options = {
    # 'verify_signature': True, # デフォルトでTrue
    # 'verify_aud': True,
    # 'verify_iat': True,
    # 'verify_exp': True, # デフォルトでTrue
    # 'verify_nbf': True,
    # 'verify_iss': True,
    # 'verify_sub': True,
    # 'verify_jti': True,
    # 'leeway': 0, # 有効期限の猶予期間(秒)
}
# audience = 'urn:my-service' # もしaudienceクレームを検証する場合
# issuer = 'https://my-auth-server.com' # もしissuerクレームを検証する場合

try:
    # decoded_payload = jwt.decode(token_to_verify, secret_key, algorithms=algorithms, audience=audience, issuer=issuer, options=options)
    decoded_payload = jwt.decode(token_to_verify, secret_key, algorithms=algorithms, options=options)
    print(f"デコードされたペイロード: {decoded_payload}")
    # 出力: {'sub': '1234567890', 'name': 'John Doe', 'iat': 1516239022, 'admin': True}
except exceptions.ExpiredSignatureError:
    print("トークンの有効期限が切れています。")
except exceptions.JWTClaimsError as e:
    print(f"クレームの検証エラー: {e}")
except exceptions.JWTError as e:
    print(f"JWT検証エラー (署名不正など): {e}")
except Exception as e:
    print(f"予期せぬエラー: {e}")

# ヘッダーだけを検証せずに取得する
try:
    unverified_header = jwt.get_unverified_header(token_to_verify)
    print(f"検証前のヘッダー: {unverified_header}")
    # 出力: {'alg': 'HS256', 'kid': 'my-key-id-1', 'typ': 'JWT'}
except exceptions.JWTError as e:
    print(f"ヘッダー取得エラー: {e}")

# ペイロード(クレーム)だけを検証せずに取得する (非推奨: セキュリティリスクあり)
# try:
#     unverified_claims = jwt.get_unverified_claims(token_to_verify)
#     print(f"検証前のクレーム: {unverified_claims}")
# except exceptions.JWTError as e:
#     print(f"クレーム取得エラー: {e}")

decode 関数は、署名の検証だけでなく、exp (有効期限), nbf (Not Before), aud (Audience), iss (Issuer) などの予約済みクレームの検証も自動的に行います(オプションで指定した場合)。検証に失敗すると例外が発生します。

重要: JWTは署名によって改ざん検知はできますが、ペイロード自体は暗号化されていません(Base64URLエンコードされているだけ)。機密情報を含める場合は、次に説明するJWEを使用するか、HTTPSなどの安全な通信経路を使用することが不可欠です。

JWS (JSON Web Signature) の利用 ✍️

JWSは、任意のデータ(JSONである必要はない)に署名を付与するための仕様です。JWTはJWSのユースケースの一つで、ペイロードがJSON形式のクレームセットである場合です。python-jose では jose.jws モジュールを使ってJWSの操作が可能です。

JWSの生成 (署名)

jose.jws.sign() 関数を使用します。ペイロード(文字列またはバイト列)、鍵、アルゴリズム、オプションのヘッダーを指定します。

from jose import jws, exceptions
import json

# 署名したいデータ (JSON文字列化)
payload_data = json.dumps({'message': 'Hello, JWS!'}).encode('utf-8')

# 秘密鍵
secret_key = 'another-secret-key'
algorithm = 'HS256'

try:
    signed_jws = jws.sign(payload_data, secret_key, algorithm=algorithm, headers={'typ': 'JWS'})
    print(f"生成されたJWS: {signed_jws}")
    # 例: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJtZXNzYWdlIjogIkhlbGxvLCBKV1MhIn0.D8hU...
except exceptions.JOSEError as e:
    print(f"JWS署名エラー: {e}")

JWSの検証

jose.jws.verify() 関数を使用します。JWS文字列、鍵、検証アルゴリズムを指定します。検証が成功すると、元のペイロード(バイト列)が返されます。

from jose import jws, exceptions
import json

# 検証したいJWS
jws_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJtZXNzYWdlIjogIkhlbGxvLCBKV1MhIn0.D8hU..."

# 検証に使用する鍵
secret_key = 'another-secret-key'
algorithms = ['HS256']

try:
    verified_payload_bytes = jws.verify(jws_token, secret_key, algorithms=algorithms)
    verified_payload = json.loads(verified_payload_bytes.decode('utf-8'))
    print(f"検証成功、ペイロード: {verified_payload}")
    # 出力: {'message': 'Hello, JWS!'}
except exceptions.JWSSignatureError:
    print("JWS署名の検証に失敗しました。")
except exceptions.JOSEError as e:
    print(f"JWS検証エラー: {e}")
except json.JSONDecodeError:
    print("ペイロードのJSONデコードに失敗しました。")
except Exception as e:
    print(f"予期せぬエラー: {e}")

JWSは、データの完全性と送信元の認証を保証したい場合に役立ちます。JWTとの違いは、ペイロードがJSONクレームセットに限定されない点です。

JWE (JSON Web Encryption) の利用 🔒

JWEは、データを暗号化して機密性を保護するための仕様です。JWTやJWSとは異なり、JWEの内容は適切な鍵がなければ読み取ることができません。python-jose では jose.jwe モジュールでJWEを扱います。

JWEの暗号化プロセスは少し複雑です。

  1. コンテンツ暗号化キー (CEK) を生成します(通常はランダム)。
  2. 指定されたコンテンツ暗号化アルゴリズム (enc) を使用して、CEKで平文を暗号化します。
  3. 指定されたキー暗号化アルゴリズム (alg) を使用して、受信者の公開鍵などでCEKを暗号化します。
  4. ヘッダー(alg, enc など)、暗号化されたCEK、初期化ベクトル(IV)、暗号文、認証タグ(AAD)などを組み合わせてJWE文字列を生成します。

JWEの生成 (暗号化)

jose.jwe.encrypt() 関数を使用します。平文(バイト列)、公開鍵(キー暗号化用)、キー暗号化アルゴリズム(alg)、コンテンツ暗号化アルゴリズム(enc)、オプションの圧縮アルゴリズム(zip)などを指定します。

from jose import jwe, jwk, exceptions
from jose.constants import ALGORITHMS
import os

# 暗号化したい平文
plaintext = b'This is a secret message.'

# 受信者の公開鍵 (RSAキーペアを生成する例)
# 実際には、事前に安全に交換された受信者の公開鍵を使用します。
# ここでは例としてその場で生成します。
private_key_pem = None
public_key_pem = None
try:
    from cryptography.hazmat.primitives import serialization
    from cryptography.hazmat.primitives.asymmetric import rsa
    private_key_obj = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    private_key_pem = private_key_obj.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    )
    public_key_obj = private_key_obj.public_key()
    public_key_pem = public_key_obj.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    )
except ImportError:
    print("cryptographyライブラリが必要です。pip install cryptography")
    # 代替として簡単な鍵を使う (非推奨)
    public_key = {'kty': 'oct', 'k': base64url_encode(os.urandom(32)).decode('utf-8')} # A256GCMKW用
    private_key = public_key
    key_alg = ALGORITHMS.A256GCMKW
    enc_alg = ALGORITHMS.A256GCM
    # exit() # cryptographyがない場合は以下の処理をスキップ


# 公開鍵をpython-joseが扱える形式に変換 (PEMから)
if public_key_pem:
    public_key = public_key_pem # cryptographyバックエンドならPEM直接可
    private_key = private_key_pem # 復号用
    key_alg = ALGORITHMS.RSA_OAEP_256 # キー暗号化アルゴリズム (例)
    enc_alg = ALGORITHMS.A256GCM     # コンテンツ暗号化アルゴリズム (例)


try:
    encrypted_jwe = jwe.encrypt(plaintext, public_key, encryption=enc_alg, algorithm=key_alg)
    print(f"生成されたJWE:\n{encrypted_jwe.decode('utf-8')}")
    # 例: eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAtMjU2In0.gA...Uw.jI...Ew.AQ...AA.h7...6Q

except exceptions.JOSEError as e:
    print(f"JWE暗号化エラー: {e}")
except Exception as e:
    print(f"予期せぬエラー: {e}")

JWEの復号

jose.jwe.decrypt() 関数を使用します。JWE文字列と、対応する秘密鍵を指定します。

from jose import jwe, exceptions

# 復号したいJWE文字列 (上記で生成したものなど)
jwe_string = "eyJhbGciOiJSU0EtT0FFUC0yNTYiLCJlbmMiOiJBMjU2R0NNIn0..." # 上記の実行結果を代入

# 復号に使用する秘密鍵 (暗号化時に使った公開鍵に対応するもの)
# private_key は上記の暗号化部分で生成/設定されたものを使用

try:
    decrypted_plaintext = jwe.decrypt(jwe_string, private_key)
    print(f"復号成功、平文: {decrypted_plaintext.decode('utf-8')}")
    # 出力: This is a secret message.
except exceptions.JWEError as e:
    print(f"JWE復号エラー: {e}")
except Exception as e:
    print(f"予期せぬエラー: {e}")

キー管理: JWEでは、特に公開鍵暗号方式 (RSA, EC) を使う場合、鍵の管理が非常に重要です。受信者の公開鍵を安全に入手し、自身の秘密鍵を厳重に保管する必要があります。JWK (JSON Web Key) 仕様は、鍵をJSON形式で表現・交換するための標準を提供し、python-josejose.jwk モジュールでサポートされています。

2024年7月の報告によると、python-jose のバージョン3.3.0には、特定のJWEトークン(圧縮率が高いもの、いわゆる「JWT bomb」)を処理する際にサービス拒否 (DoS) を引き起こす可能性のある脆弱性 (CVE-2024-33664) が存在します。これは、圧縮されたデータを展開する際に大量のリソースを消費する可能性があるためです。この脆弱性も、python-jose の利用を再検討する理由の一つとなります。

対応アルゴリズム ⚙️

python-jose は、JWA (JSON Web Algorithms) 仕様で定義されている多くのアルゴリズムをサポートしています。以下は代表的なものです。

JWS (署名) アルゴリズム

アルゴリズム値 説明 鍵タイプ
HS256, HS384, HS512 HMAC using SHA-256/384/512 (共通鍵) Octet sequence (bytes)
RS256, RS384, RS512 RSASSA-PKCS1-v1_5 using SHA-256/384/512 (公開鍵/秘密鍵) RSA
ES256, ES384, ES512 ECDSA using P-256/P-384/P-521 and SHA-256/384/512 (公開鍵/秘密鍵) EC
PS256, PS384, PS512 RSASSA-PSS using SHA-256/384/512 (公開鍵/秘密鍵) RSA

JWE (キー暗号化) アルゴリズム (`alg`)

アルゴリズム値 説明 鍵タイプ
RSA1_5, RSA-OAEP, RSA-OAEP-256 RSAES-PKCS1-v1_5 / RSAES-OAEP (SHA-1 / SHA-256) RSA
A128KW, A192KW, A256KW AES Key Wrap (128/192/256 bit) Symmetric
dir Direct use of a shared symmetric key as CEK Symmetric
ECDH-ES Elliptic Curve Diffie-Hellman Ephemeral Static EC
ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW ECDH-ES using Concat KDF and CEK wrapped with AES Key Wrap EC
A128GCMKW, A192GCMKW, A256GCMKW Key wrapping with AES GCM (128/192/256 bit) Symmetric
PBES2-HS256+A128KW, etc. Password Based Encryption Schema 2 Password

JWE (コンテンツ暗号化) アルゴリズム (`enc`)

アルゴリズム値 説明 鍵長 (CEK)
A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 AES CBC with HMAC SHA-2 (128/192/256 + 256/384/512 bit) 256/384/512 bits
A128GCM, A192GCM, A256GCM AES GCM (128/192/256 bit) 128/192/256 bits

利用可能なアルゴリズムの完全なリストと詳細については、python-jose のドキュメントや JWA (RFC 7518) を参照してください。適切なアルゴリズムの選択は、セキュリティ要件や相互運用性の要件に依存します。

ユースケース 🚀

python-jose(およびJOSE全般)は、さまざまなシナリオで活用されています。

  • API認証・認可: おそらく最も一般的なユースケースです。ユーザーがログインすると、サーバーはJWTを生成してクライアントに返します。クライアントは以降のリクエストでこのJWTをAuthorizationヘッダーに含めて送信し、サーバーはJWTを検証してユーザーを認証し、アクセス権を確認します。FastAPIなどのフレームワークでは、OAuth2と組み合わせてJWT Bearerトークンを利用する仕組みが提供されています。
  • シングルサインオン (SSO): 複数のサービス間でユーザーの認証情報を安全に共有するためにJWTが使われます。ユーザーは一度ログインすれば、連携する他のサービスにも自動的にログインできます。
  • OpenID Connect (OIDC): OAuth 2.0を拡張した認証プロトコルで、IDトークンとしてJWTを使用します。ユーザーに関する情報を安全にRP (Relying Party, クライアントアプリケーション) に伝達します。
  • セキュアな情報交換: 署名付き (JWS) または暗号化 (JWE) されたJSONオブジェクトを使って、改ざん防止や機密性を確保しつつ、異なるシステム間で情報を安全に交換します。例えば、マイクロサービス間の通信や、サードパーティとのデータ連携などに利用されます。
  • 自己完結型トークン: JWTは必要な情報(ユーザーID、権限、有効期限など)を自身の中に含んでいるため、「自己完結型」と言われます。サーバー側でセッション状態を保持する必要がない場合があり、ステートレスなアーキテクチャと相性が良いです。
  • セキュリティイベントトークン (SET): RFC 8417で定義され、セキュリティ関連のイベント(アカウント変更、不正アクセス試行など)を通知するためにJWT形式を使用します。

python-jose は、これらのユースケースをPythonで実装するための基盤を提供します。ただし、前述の通り、ライブラリのメンテナンス状況には注意が必要です。

代替ライブラリの検討 🔄

前述の通り、python-jose の開発は停滞しており、セキュリティ上の懸念もあります。そのため、特に新規プロジェクトでは、代替となるライブラリの利用を検討することが推奨されます。

  • PyJWT: JWTのエンコード、デコード、検証に特化した軽量なライブラリです。JWS機能はカバーしますが、JWE(暗号化)はサポートしていません。API認証など、署名付きJWTが主な要件であれば、シンプルで活発にメンテナンスされているPyJWTは良い選択肢です。依存ライブラリも少なく、導入が容易です。
  • Authlib / joserfc: AuthlibはOAuth, OpenID Connect, JOSEなど、認証・認可に関する幅広い仕様を実装する包括的なライブラリです。AuthlibからJOSE関連の機能を切り出して独立させたのが joserfc ライブラリです。joserfc はJWS, JWE, JWK, JWA, JWTを完全にサポートし、型ヒントも充実しています。python-jose の完全な代替となりうる、高機能でメンテナンスされているライブラリです。APIが python-jose とは異なるため、移行にはコードの修正が必要です。
  • JWCrypto: もう一つのJOSE実装ライブラリで、JWS, JWE, JWKなどをサポートしています。こちらも選択肢の一つとなり得ます。

ライブラリ選定の際は、必要な機能(JWSのみか、JWEも必要か)、ライブラリのメンテナンス状況、コミュニティサポート、ドキュメントの充実度、既存コードからの移行コストなどを考慮すると良いでしょう。2024年現在、多くの開発者が python-jose から PyJWT や Authlib/joserfc へ移行する傾向にあります。

まとめ ✨

python-jose は、かつてPythonでJOSE標準 (JWT, JWS, JWE, JWK, JWA) を扱うための強力で包括的なライブラリでした。JWTによる認証トークンの生成・検証、JWSによるデータ署名、JWEによるデータ暗号化など、セキュアなWebアプリケーションやサービスを構築するための多くの機能を提供してきました。

この記事では、そのインストール方法から、JWT, JWS, JWEの基本的な使い方、対応アルゴリズム、そして主なユースケースについて解説しました。

しかしながら、python-jose は近年メンテナンスが活発ではなく、セキュリティ脆弱性も報告されているため、新規での採用や継続利用には注意が必要です。代替として、PyJWT (JWS/JWT特化) や Authlib/joserfc (JOSEフルサポート) といった、より活発に開発・保守されているライブラリの利用を強く推奨します。

JOSEの概念と標準は、現代のWebセキュリティにおいて依然として重要です。この記事が、python-jose およびJOSE技術への理解を深める一助となれば幸いです。 😊

コメント

タイトルとURLをコピーしました