カテゴリー: Story

  • JWTパーサー完全ガイド: JSON Web Tokenを安全にデコード・検証・検査する方法

    JWTパーサー完全ガイド: JSON Web Tokenを安全にデコード・検証・検査する方法

    本番環境で認証が突如として壊れた。ユーザーから「Invalid Token」エラーが報告され、原因を素早く突き止める必要がある。JWTを開いてみると、文字列はまるで暗号のように見える。ドットで区切られた3つのランダムな文字列のブロックだ。データはその中に確かに存在するが、パーサーがなければ読み解くことはできない。

    JWT Parserは、RFC 7519規格に準拠し、JSON Web Tokenを構成する3つの部分(Header、Payload、Signature)を分解する専用ツールである。2026年4月時点で、これらのパーサーはBase64URLエンコードされたデータをデコードし、シークレットや公開鍵を使って署名を検証することで、トークンが改ざんされていないことを確認し、「alg: none」攻撃のような脅威を遮断する。

    JWTパーサーが実際に行っていること

    JWTパーサーは翻訳機のようなものだと考えてほしい。長くて読めない不透明な文字列を受け取り、それを再び読めるJSONオブジェクトへと変換する。これは、現代のアプリケーションにおいてユーザーIDを管理し、データ交換を安全に行うための基盤となる機能である。

    内部的には、パーサーはトークンを3つのセクションに分割する2つのピリオド(.)を見つける。

    セクション 役割 エンコード方式 鍵なしで読めるか
    Header メタデータ: 署名アルゴリズム(HS256、RS256) Base64URL 読める
    Payload クレーム: ユーザーデータ、有効期限、ロール Base64URL 読める
    Signature 真正性を証明するデジタル署名 HMAC/RSA 読めない — 鍵が必要

    JWTトークンの簡略化された3部分構造

    ステップ別のデコード: 内部で何が起きているか

    実際のトークンを使って追ってみよう。次のサンプルJWTを見てほしい。

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE3MDAwMDAwMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    

    ステップ1: ピリオドで分割

    [0] eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    [1] eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE3MDAwMDAwMDB9
    [2] SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
    

    ステップ2: セクション[0](Header)をBase64URLデコード

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    

    ステップ3: セクション[1](Payload)をBase64URLデコード

    {
      "sub": "1234567890",
      "name": "John",
      "iat": 1700000000
    }
    

    ステップ4: セクション[2](Signature)を検証 — シークレット鍵が必要

    パーサーはBase64URLエンコードされたheader + “.” + payloadを取り出し、シークレットを使ってHMAC-SHA256を計算する。その結果がセクション[2]と一致すれば、トークンは真正であると判断される。

    重要なセキュリティ注意点: Base64URLは暗号化ではない

    新人開発者が陥りやすい罠は、エンコードされたheaderとpayloadが暗号化されていると思い込むことだ。そうではない。JustUse.meが指摘するように、Base64URLエンコードはJSONをURLやヘッダー経由で安全に送れるようにしているだけである。トークンを持っていれば、パスワードや鍵がなくてもpayloadをデコードできる。

    JWTのpayloadに機密データ(パスワード、SSN、APIキーなど)を決して保存してはいけない。 トークンを傍受した人は誰でもその中身を見ることができる。

    署名検証: セキュリティの門番

    トークンのデータは誰でも読めるが、システムを本当に安全に保つのは署名検証である。JWTパーサーは情報を読み取るだけでなく、それがどこから来たかを証明する。

    パーサーはheader、payload、そして鍵を使って署名を再計算し、その結果がトークン上の署名と一致するかを確認する。一致しなければ、トークンは改ざんされている。

    2つのアルゴリズムファミリー

    アルゴリズム 鍵の種類 仕組み 代表的なユースケース
    HS256(HMAC) 対称 — 署名と検証で同じシークレット鍵を使用 双方が1つのシークレットを共有 単一サービスの認証、1チーム内のマイクロサービス
    RS256(RSA) 非対称 — 秘密鍵で署名、公開鍵で検証 送信者は秘密鍵を保持、公開鍵を持つ人は誰でも検証可能 OAuth2プロバイダー、サードパーティAPI連携
    ES256(ECDSA) 非対称 — RSAと同じモデルだが楕円曲線を使用 より小さな鍵、より高速な検証 モバイルアプリ、パフォーマンス重視のサービス

    JWTパーサーの3ステップ検証ロジック

    「alg: none」攻撃

    これはJWTの脆弱性の中で最も危険なものの1つである。攻撃者はheaderを改ざんして"alg": "none"を主張し、署名を剥がす。不適切に実装されたパーサーはこれを受け入れ、一切の検証を行わずにトークンを有効なものとして扱ってしまう可能性がある。

    防御策: パーサーは、アルゴリズムが「none」であるトークン、あるいは想定するアルゴリズムと一致しないトークンを明示的に拒否しなければならない。ApifyのJWTツールを開発したStas Persiianenkoは、トークンは設計上透明であっても、そのセキュリティはパーサーが署名なしトークンや改ざんトークンを厳格に拒否するかにかかっていると強調している。

    decoded = jwt.decode(token, key, algorithms=None)  # NEVER do this
    
    decoded = jwt.decode(token, key, algorithms=["HS256"])
    

    標準的なJWTクレーム: 各フィールドの意味

    JWTパーサーはpayloadから「クレーム」を取り出す。これらはシステム間の互換性のためにJOSE (JSON Object Signing and Encryption)フレームワークに従っている。

    クレーム フルネーム 役割 例の値
    iss Issuer トークンを発行した主体 "auth.example.com"
    sub Subject トークンが表すユーザーやエンティティ "user:12345"
    aud Audience トークンの想定受信者 "api.example.com"
    exp Expiration Time トークンが無効になる時刻 1700000000(Unixタイムスタンプ)
    iat Issued At トークンが作成された時刻 1699999999
    nbf Not Before この時刻より前はトークンが無効 1699999999
    jti JWT ID トークンの一意識別子 "a1b2c3d4"

    非対称署名を使う場合、パーサーはよくJWK (JSON Web Key)を参照する。これは公開鍵を表すJSON構造体である。パーサーは発行者のメタデータエンドポイントから正しいJWKを自動的に取得してトークンを検証する。

    実装: 本番向けの実コード

    PHPとlcobucci/jwt

    PHPエコシステムの標準はlcobucci/jwtである。Packagistのデータによると、2026年4月時点で322 million以上のインストールがあり、LaravelやSymfonyプロジェクトでの定番となっている。

    use Lcobucci\JWT\Configuration;
    use Lcobucci\JWT\Signer\Hmac\Sha256;
    use Lcobucci\JWT\Signer\Key\InMemory;
    
    $config = Configuration::forSymmetricSigner(
        new Sha256(),
        InMemory::plainText('your-secret-key')
    );
    
    // Parsing and validating a token
    $token = $config->parser()->parse($jwtString);
    
    // Verify constraints: expiration, issuer, etc.
    $constraints = [
        new \Lcobucci\JWT\Validation\Constraint\IssuedBy('auth.example.com'),
        new \Lcobucci\JWT\Validation\Constraint\PermittedFor('api.example.com'),
        new \Lcobucci\JWT\Validation\Constraint\SignedWith(
            $config->signer(),
            $config->signingKey()
        ),
    ];
    
    $isValid = $config->validator()->validate($token, ...$constraints);
    

    Hono(Edge/Serverless)とWeb Crypto

    軽量なエッジアプリケーション向けには、Hono JWT Helperが最小限のdecode()関数を提供しており、高速なコールドスタートと最小限の依存関係を求めるサーバーレスプラットフォームに最適である。

    import { jwt } from 'hono/jwt'
    
    // Middleware to verify JWT on every request
    app.use('/api/*', jwt({ secret: 'your-secret' }))
    
    // Access decoded claims in your handler
    app.get('/api/profile', (c) => {
      const payload = c.get('jwtPayload')
      return c.json({ user: payload.sub })
    })
    

    MCPによるAI駆動のJWT解析

    2026年までに、Model Context Protocol (MCP)はClaude CodeやCursorのようなAIアシスタントがJWTツールと直接対話できるようにしている。MCPサーバーをセットアップすれば、開発者はAIに「これらのログ内の全JWTをチェックして有効期限エラーを探して」と依頼でき、エージェントがコマンドライン経由で解析を処理する。

    Apifyによると、2026年時点でバルク処理は10,000トークンあたり約$11.50のコストとなっている。この自動化により、AIエージェントは有効期限切れのトークンを発見し、アプリのセキュリティ設定に対するコード修正案を即座に提示できる。

    まとめ

    JWTパーサーは単なるデバッグの便利道具ではなく、重要なセキュリティチェックポイントである。署名チェックを通じてトークンが真正であることを確認し、クレーム検証を通じて有効性を保証する。最も重要な2つのルールを忘れないようにしたい。1つ目は、Base64URLは暗号化ではないため、payloadにシークレットを決して入れないこと。2つ目は、「alg: none」攻撃を防ぐため、許可するアルゴリズムを常に明示的に指定すること。

    本番アプリでは、自作のパーサーを転がすのではなく、lcobucci/jwtやHonoのJWTヘルパーのような実績あるライブラリを使うこと。デバッグやバルク解析には、AI駆動のMCPツールが、セキュリティ監査を自動化し網羅的に保つための現代的なアプローチである。

    FAQ

    ブラウザで見つけたJWTトークンをデコードするのは合法ですか?

    はい、完全に合法です。JWTは透明性を持つように設計されている。headerとpayloadは輸送のためにエンコードされているだけであり、秘密性のために暗号化されているわけではない。トークンを所持しているということは、そのクレーム内のデータにアクセス権があることを意味する。ただし、トークンに個人情報が含まれる場合は、GDPRのような地域のデータ保護法を常に遵守すること。

    生成したばかりのトークンで、JWTパーサーが isExpired: true を表示するのはなぜですか?

    これは通常、トークンを生成したサーバーとそれを解析するシステム間のクロックドリフトが原因である。両システムの時計が(UTC/NTP経由で)同期されていないと、expnbfクレームが無効に見えることがある。両システムでNTPを使った時刻同期を確保するか、解析ライブラリで小さな「leeway」(通常は60 seconds)を追加して、わずかなズレを吸収することで解決できる。

    シークレットや公開鍵を持っていなくてもJWTをデコードできますか?

    はい、headerとpayloadは単なるBase64URLエンコードされたJSONであるため、鍵がなくても常にデコードして読むことができる。ただし、対応するシークレット(HS256の場合)や公開鍵(RS256の場合)がなければ、署名を検証したり、データが真正であると信頼したりすることはできない。検証なしの場合、そのデータは未検証であり、改ざんされている可能性があるものとして扱うこと。

    「alg: none」攻撃とは何で、どう防げばいいですか?

    「alg: none」攻撃は、トークンのheaderに指定されたアルゴリズムを検証なしで受け入れるパーサーを悪用する。攻撃者はheaderを"alg": "none"に変更して署名を削除し、脆弱なパーサーを騙してトークンを有効として受け入れさせる。これを防ぐには、検証コードの中で許可するアルゴリズムを常に明示的に指定すること。「none」を決して受け入れず、トークンにどのアルゴリズムを使うかを指示させることも許可しないこと。