JWT Validation: Verifying Tokens for Authentication and Authorization

JWT検証の実践ガイド — JSON Web Tokenの署名、クレーム、構造をチェックしてリクエストが真正に認証・認可されていることを確認するプロセスについて解説。署名の検証、標準的なクレームチェック、鍵ローテーション、ASP.NET Coreでの検証、検証の破綻やバイパスにつながるよくあるミスまでをカバーします。


目次

  1. はじめに
  2. JWTの構造
  3. 署名アルゴリズム
  4. 「検証」で実際に行われること
  5. 署名の検証と鍵ローテーション
  6. 標準クレームの検証
  7. ASP.NET CoreでのJWT検証
  8. カスタム検証ロジック
  9. トークンの失効:JWTの根本的制約
  10. サービス間でのJWT検証
  11. よくある脆弱性
  12. 検証失敗のデバッグ
  13. クイックリファレンス表
  14. 結論

はじめに

Authorization: Bearer <token> ヘッダーで届くJWTは、実際に検証されるまでは単なる文字列に過ぎません — しかも、検証は最初に思えるよりかなり多くの処理を行っています。単に「JWTらしいか」や「署名が有効か」だけでなく、適切な検証ではトークンが信頼できる発行者から発行されたものであるか、この特定のAPI向けに意図されたものであるか、有効期限内か、改ざんされていないかなどを確認します。これらのチェックのいずれかを誤ったり省略したりすると、本来受け入れるべきでないトークンを受け入れるAPIになってしまう可能性があります。

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
        options.Audience = "api://my-api";
    });

Enter fullscreen mode Exit fullscreen mode

この2行はシンプルに見えますが、内部では実際に徹底した検証パイプラインを設定しています — 本ガイドでは、このパイプラインが実際に何をチェックしているのか、各チェックがなぜ重要なのか、検証の設定ミスやプレッシャー下でのバイパスでよく起こる問題について解説します。


1. JWTの構造

ドットで区切られた3つの部分

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL2F1dGhzZXJ2ZXIuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6ImFwaTovL215LWFwaSIsImV4cCI6MTcyMTQwNDgwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
└──────────────── header ────────────────┘└──────────────────────── payload ────────────────────────┘└──────── signature ────────┘

Enter fullscreen mode Exit fullscreen mode

ヘッダー

{ "alg": "RS256", "typ": "JWT", "kid": "abc123" }

Enter fullscreen mode Exit fullscreen mode

  • alg — 使用された署名アルゴリズム(セクション2)。
  • typ — これがJWTであることを示す。
  • kid(鍵ID) — このトークンの署名に使用された特定の鍵(現在有効な複数の鍵の中から)を識別。鍵ローテーションに必須(セクション4)。

ペイロード(クレーム)

{
  "iss": "https://authserver.com",
  "sub": "1234567890",
  "aud": "api://my-api",
  "exp": 1721404800,
  "iat": 1721401200,
  "nbf": 1721401200,
  "scp": "products.read products.write"
}

Enter fullscreen mode Exit fullscreen mode

トークン、主体、その有効期限に関するクレーム群 — セクション5で詳しく説明します。

署名

signature = Sign(base64url(header) + "." + base64url(payload), private_key)

Enter fullscreen mode Exit fullscreen mode

ヘッダーとペイロード全体に対して発行者の秘密鍵で計算された暗号署名 — これにより検証側は、発行者の対応する公開鍵のみを使って、トークンが本当に主張された発行者から来たものであり、署名後に内容が改ざんされていないことを確認できます(セクション4)。

重要:ペイロードは暗号化されていない

echo "eyJpc3MiOiJodHRwczovL2F1dGhzZXJ2ZXIuY29tIn0" | base64 -d
# {"iss":"https://authserver.com"}

Enter fullscreen mode Exit fullscreen mode

JWTのヘッダーとペイロードは単にbase64urlエンコードされているだけで、暗号化はされていません — トークンを傍受した人(またはユーザー自身が自分のトークンを調べる場合)は、クレームの内容を簡単にデコードして読むことができます。これはよくある重大な誤解です:JWTは完全性と真正性(改ざんされていないこと、発行者から来たことを信頼できること)を提供しますが、機密性は一切提供しません。本当に機密性の高いデータ(パスワード、シークレット、トークンの所持者や傍受者に見られてはならないもの)は、JWTのクレームに直接入れてはいけません。


2. 署名アルゴリズム

非対称(OAuth2/OIDCのアクセス・IDトークンの標準)

RS256 — RSA署名とSHA-256(最も一般的)
ES256 — ECDSA署名とSHA-256(署名が小さく、採用が増加中)

Enter fullscreen mode Exit fullscreen mode

非対称アルゴリズムでは、Authorization Serverが秘密に保持する秘密鍵でトークンに署名し、リソースサーバー(API)は対応する公開鍵で署名を検証できます — この公開鍵はAuthorization Serverが公開しています(jwks_uriディスカバリーエンドポイント経由、本シリーズのOAuth2/OpenID Connectガイドで解説)。この非対称性が、JWTを分散システムで実用的にしている理由です:数十の独立したAPIが、いずれも新しい有効なトークンを発行できる秘密鍵を持つことなく、トークンを検証できます。

対称(OAuth2/OIDCシナリオには通常不適切)

HS256 — HMACとSHA-256、署名と検証の両方に単一の共有秘密を使う

Enter fullscreen mode Exit fullscreen mode

対称アルゴリズムでは、同じ秘密が署名と検証の両方に使われます — つまりトークンを検証する必要があるすべての当事者が、新しい有効なトークンを作成できる秘密も持たなければなりません。これは単一の自己完結型アプリケーションが内部で独自のトークンを発行・検証する場合には問題ありませんが、1つのAuthorization Serverと多数の独立したリソースサーバーという典型的なOAuth2/OIDCシナリオには不向きです。署名秘密を検証が必要なすべてのAPIに配布すると、それらのAPIもトークンを偽造できてしまうためです。

alg: none攻撃 — アルゴリズム確認が重要な理由

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

Enter fullscreen mode Exit fullscreen mode

JWT仕様は技術的にalg値として"none"を許可しており、署名なしを意味します — トークンのヘッダーが主張するアルゴリズムを盲目的に信頼する(期待するアルゴリズムを強制しない)不適切な実装の検証器は、完全に署名なしで自由に偽造可能なトークンを受け入れてしまう可能性があります。セクション10でこの攻撃と密接に関連する攻撃(アルゴリズム置換)について詳しく解説します — 要するに、検証器は常に期待する特定のアルゴリズムを強制しなければならず、トークンが自己申告するalgヘッダーを単に信頼してはなりません。


3. 「検証」で実際に行われること

適切に実装されたJWT検証プロセスは、いくつかの本当に異なることをチェックします。そのいずれかを省略すると、検証全体が意味を失います:

1. 構造的妥当性  — 実際に整形式のJWTか(3つのbase64urlセグメント)
2. 署名の検証 — 信頼できる鍵で署名されたか、署名後に内容が改ざんされていないか
3. 発行者(iss)          — 実際に信頼するAuthorization Serverから来たか
4. オーディエンス(aud)        — このトークンはこのAPI向けに発行されたものか、他のもの向けではないか
5. 有効期限(exp)       — トークンの有効期限は既に過ぎていないか
6. Not-before(nbf)       — トークンが有効になる前ではないか
7. アルゴリズム確認 — 署名アルゴリズムは期待するもので、攻撃者が選んだものではないか

Enter fullscreen mode Exit fullscreen mode

現代のJWTライブラリ(セクション6)は、正しく設定されていればこれら7つすべてを自動的に処理します — ただし、各チェックを個別に理解することは、ライブラリを正しく設定するためにも、カスタムまたは手書きの検証実装で何かが欠けていないかを認識するためにも重要です。


4. 署名の検証と鍵ローテーション

公開鍵の取得

GET https://authserver.com/.well-known/jwks.json

Enter fullscreen mode Exit fullscreen mode

{
  "keys": [
    {
      "kid": "abc123",
      "kty": "RSA",
      "use": "sig",
      "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt...",
      "e": "AQAB"
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

JWKS(JSON Web Key Set)エンドポイントは、Authorization Serverが署名に使用する現在の公開鍵を公開します — 検証器はこのドキュメントを取得し(通常は毎回のリクエストではなく、適切な間隔でキャッシュ)、トークンのkidヘッダーに一致する鍵を使って署名を検証します。

鍵ローテーションが存在する理由とkidが重要な理由

旧鍵(kid: "abc123": 段階的に廃止中、ローテーション前に発行されたトークンに対して有効期限まで有効
新鍵(kid: "def456": 新しいトークンに積極的に署名中

Enter fullscreen mode Exit fullscreen mode

Authorization Serverはセキュリティのベストプラクティスとして定期的に署名鍵をローテーションします — 単一の鍵が使用される期間を制限し、万一鍵が漏洩した場合の回復経路を提供するためです。鍵で署名されたトークンは、その自然な有効期限まで有効であるため、JWKSエンドポイントはローテーション期間中に通常複数の現在有効な鍵を同時に公開し、各トークンのkidヘッダーが検証器にそのトークンに使用すべき公開鍵を正確に伝えます。

鍵のキャッシュ — ただし永遠ではない

options.TokenValidationParameters.ConfigurationManager =
    new ConfigurationManager<OpenIdConnectConfiguration>(metadataAddress, retriever)
    {
        AutomaticRefreshInterval = TimeSpan.FromHours(24),
    };

Enter fullscreen mode Exit fullscreen mode

JWKSドキュメントをすべての受信リクエストで取得するのは無駄であり、不要なレイテンシを追加します — 検証器は取得した鍵を適切な期間キャッシュしますが、定期的に更新する必要があり(理想的には、トークンが未知のkidを参照する場合に即時更新できる)、Authorization Server側での正当な鍵ローテーションがAPI側で検証失敗の波を引き起こさないようにします。適切に構築されたOIDCクライアントライブラリ(セクション6)はこの更新ロジックを自動的に処理します — これは手書きの実装で微妙に誤りやすい種類の詳細です。


5. 標準クレームの検証

発行者(iss

options.TokenValidationParameters.ValidIssuer = "https://login.microsoftonline.com/{tenant-id}/v2.0";

Enter fullscreen mode Exit fullscreen mode

トークンが実際にアプリケーションが信頼するAuthorization Serverから発行されたことを確認します — このチェックがなければ、署名のみを検証する検証器は、公開鍵が何らかの方法で入手可能であれば、信頼できない別の発行者から完全に有効に署名されたトークンを受け入れてしまう可能性があります(マルチテナントや設定ミスのシナリオで現実のリスク、セクション10でさらに解説)。

オーディエンス(aud

options.TokenValidationParameters.ValidAudience = "api://my-api";

Enter fullscreen mode Exit fullscreen mode

トークンがこのAPI向けに発行されたものであることを確認し、同じAuthorization Serverを信頼する他のAPIやクライアントアプリケーション向けではないことを確認します。これは正しく設定すべき最も重要なチェックの1つです — これがなければ、同じ信頼できるAuthorization Serverによって発行されたが、まったく別の内部API向けのトークンがあなたのAPIに対してリプレイされ、署名/発行者検証を通過する可能性があります。両方のチェックは本当に成功するからです;オーディエンスチェックだけがこの特定のケースを捉えます。

有効期限(exp)とnot-before(nbf

exp: 1721404800   — このUnixタイムスタンプ以降はトークン無効
nbf: 1721401200   — このUnixタイムスタンプ以前はトークン無効(稀だが、将来使用向けに発行されたトークンで使用)

Enter fullscreen mode Exit fullscreen mode

単純な時間窓チェック — ただし、ほとんどの検証ライブラリは、発行サーバーと検証サーバー間の軽微なクロックドリフトを許容するため、小さなクロックスキュー許容値(通常数分)を適用することに注意してください。秒単位で完全に同期されたクロックを要求するわけではありません。

options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5); // .NETのデフォルト

Enter fullscreen mode Exit fullscreen mode

過度に寛大なクロックスキュー許容値は、有効期限切れ(または未有効)のトークンがまだ受け入れられる可能性のある実用的な窓を意味のある範囲で延長します — 本当に高セキュリティなシナリオでは、デフォルトをそのままにせず意図的に下げて調整する価値があります。

サブジェクト(sub

var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);

Enter fullscreen mode Exit fullscreen mode

発行者/オーディエンスのように期待値に対して「検証」されるわけではありませんが、アプリケーションが内部で認証されたユーザーの安定した正規識別子として使用すべきクレームです — ユーザーの生涯で変更される可能性のあるメールやユーザー名クレームではなく、subは一般的に変更されません。


6. ASP.NET CoreでのJWT検証

JWT Bearer認証ハンドラー

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
        options.Audience = "api://my-api";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ClockSkew = TimeSpan.FromMinutes(2),
        };
    });

app.UseAuthentication();
app.UseAuthorization();

Enter fullscreen mode Exit fullscreen mode

Authorityを設定すると、ASP.NET CoreがOIDCディスカバリードキュメントとJWKSエンドポイントを自動的に取得し(セクション4)、更新を継続します — セクション3の7つのチェックはすべて、手書きの署名検証やクレームチェックロジックを一切記述することなく、すべての受信リクエストに対して自動的に実行されます。

エンドポイントでの認証要求

app.MapGet("/products", () => GetProducts()).RequireAuthorization();

Enter fullscreen mode Exit fullscreen mode

[Authorize]
public class ProductsController : ControllerBase { }

Enter fullscreen mode Exit fullscreen mode

RequireAuthorization()(minimal API)または[Authorize](MVCコントローラー)は、特定のエンドポイントに対して有効で正常に検証されたトークンが存在することを実際に強制します;AddJwtBearerハンドラーの設定だけでは、認証されていないリクエストを自動的に拒否しません;認証を要求すべき特定のエンドポイントにこれらの認可要件を組み合わせる必要があります。

期待するアルゴリズムの明示的な強制

options.TokenValidationParameters.ValidAlgorithms = new[] { "RS256" };

Enter fullscreen mode Exit fullscreen mode

セクション2とセクション10で説明したように、トークンのヘッダーが主張するものに関わらず、受け入れ可能な署名アルゴリズムを明示的に制限することは(期待する特定のアルゴリズムを強制する)、特に検証コードパスが複数のIDプロバイダーや異なるアルゴリズム期待を持つ可能性のある設定で共有される場合に価値のある、低コストの強化策です。


7. カスタム検証ロジック

標準チェックを超えたアプリケーション固有のチェックの追加

options.Events = new JwtBearerEvents
{
    OnTokenValidated = async context =>
    {
        var tenantId = context.Principal?.FindFirstValue("tid");
        if (tenantId != _expectedTenantId)
        {
            context.Fail("Token issued for an unexpected tenant.");
            return;
        }

        var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
        if (await _userService.IsUserSuspendedAsync(userId!))
        {
            context.Fail("User account is suspended.");
        }
    }
};

Enter fullscreen mode Exit fullscreen mode

OnTokenValidatedイベントは、すべての標準的な暗号・クレームチェックが成功したに発火し、追加のアプリケーション固有の検証のためのフックを提供します — マルチテナントトークンが期待されるテナントに実際に属していることを確認したり、ユーザーの現在のアカウントステータスを独自のデータベースに対してチェックしたり(純粋に暗号的なトークン検証だけでは知り得ないこと)、結果のClaimsPrincipalを他の場所から取得した追加のクレームで拡張したりします。

これが重要な理由:暗号的妥当性は「このリクエストを許可すべきか」とは同じではない

トークンはセクション3のすべてのチェックを通過する可能性があります — 信頼できる発行者によって真正に署名され、正しいオーディエンス、有効期限内 — それでもユーザーのアカウントがトークン発行から5分後に停止された、またはAuthorization Serverが知り得ない何らかのビジネスルールのために、リクエストが尊重されるべきでない場合があります。カスタム検証ロジックはまさにこれらのギャップを埋める場所であり、「トークンが検証された」ことが「このリクエストを許可すべき」ことと同等ではないことを、意図的に考慮する価値があります。


8. トークンの失効:JWTの根本的制約

核心的な緊張関係

JWTはステートレスに検証されます — これがAPIがすべてのリクエストでAuthorization Serverへのネットワークラウンドトリップなしにトークンの真正性をローカルで検証できるという、JWTの魅力のすべてです。しかし、この同じ特性により、一度発行されたJWTは、Authorization Serverが今すぐ失効させたい場合でも、自然な有効期限まで暗号的に有効なままです(ユーザーがログアウトした、管理者がアカウントを無効にした、トークンが盗まれたと検知された)。

トークン発行、有効期限:今から1時間後
5分後:ユーザーのアカウントが停止
...しかしトークンは追加の措置が取られない限り、残り55分間暗号的に有効なまま

Enter fullscreen mode Exit fullscreen mode

緩和策1:アクセストークンの有効期間を短くする

アクセストークンの有効期間:15分(一般的で妥当なデフォルト)

Enter fullscreen mode Exit fullscreen mode

最も一般的でシンプルな緩和策は、そもそもこの窓を大きくしないことです — 短寿命のアクセストークンは、「信頼できなくなったが技術的にはまだ有効期限が切れていない」トークンが使用可能な期間を制限しますが、(OAuth2/OpenID Connectガイドで解説されているリフレッシュトークン経由で処理されるためユーザーには見えないものの)より頻繁なトークン更新という代償を伴います。

緩和策2:カスタム検証でチェックされる失効/拒否リスト

OnTokenValidated = async context =>
{
    var jti = context.Principal?.FindFirstValue("jti"); // トークンの一意な識別子
    if (await _revocationStore.IsRevokedAsync(jti!))
    {
        context.Fail("Token has been revoked.");
    }
}

Enter fullscreen mode Exit fullscreen mode

短い有効期限窓を待つことさえ許容できないシナリオ(検知されたアカウント侵害後の即時アクセスの失効など)では、明示的な失効リストを維持し — カスタム検証(セクション7)中にトークンの一意なjti(JWT ID)クレーム経由でチェックする — JWTが避けるように設計されたステートフルなリクエストごとのチェックを、まさにこの特定の狭い必要性に対して再導入します。

緩和策3:本当に即時失効が必要な場合の不透明トークン

本シリーズのOAuth2/OpenID Connectガイドで解説したように、不透明トークン(すべてのリクエストでAuthorization Serverのイントロスペクションエンドポイントへのコールバック経由で検証)は、この制約を完全に回避します。Authorization Serverは失効したトークンを即座に認識しなくなることができるからです — JWTが存在するまさにその理由であるネットワークラウンドトリップとAuthorization Server可用性の依存を再導入するという直接的な代償を伴います。JWTと不透明トークンのどちらを選択するかは、大きな部分でまさにこのトレードオフについての選択です:有界な失効遅延を持つローカルで高速なステートレス検証、対してリクエストごとの依存を持つ集中型で即時失効可能な検証。


9. サービス間でのJWT検証

gRPC/マイクロサービスシナリオ

// 各内部サービスは同じ共有Authorityを使用してJWTを独立に検証
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
    {
        options.Authority = "https://internal-idp.mycompany.com";
        options.Audience = "api://internal-services";
    });

Enter fullscreen mode Exit fullscreen mode

マイクロサービスアーキテクチャ(本シリーズのgRPCとバックグラウンドサービスガイドに関連)では、各個別のサービスが同じ共有Authorization Serverの公開鍵を使用して受信JWTを独立に検証できます — どのサービスも他のサービスを直接信頼したり、独自の別個の資格情報ストアを維持したりする必要はありません;すべてが同じ中央の信頼源に対して検証します。これは、JWTベースの認証が大規模な内部サービス間通信で魅力的な特性の1つです。

サービス間のトークン伝播と再発行

Client → API Gateway (ユーザーのトークンを検証) → Service A (再検証? それともゲートウェイを信頼?)

Enter fullscreen mode Exit fullscreen mode

意図的に決定すべき設計上の選択:内部サービスは、APIゲートウェイまたは他の上流サービスを通過したトークンを再検証するか、または上流での検証が既に完了したことを信頼するか? すべてのホップで再検証するのはより防御的(侵害または設定ミスの上流サービスからの保護)ですが、レイテンシと複雑さを追加します;上流ゲートウェイの検証を信頼するのはシンプルですが、そのゲートウェイに信頼(とリスク)を集中させます。多くの組織は、サービス間通信がユーザーオリジナルのJWTを個別に再検証するかどうかにやや依存せず、トランスポートレベルの信頼を持つように、相互TLSを備えたサービスメッシュ(本シリーズのKubernetes/Helmガイドで言及)を使用します。

サービスチェーンのためのOn-Behalf-Ofフロー

ユーザーのトークン → Service AがService Bを呼び出すための新しいトークンと交換し、ユーザーの代理として

Enter fullscreen mode Exit fullscreen mode

中間サービスが下流サービスを元のユーザーとして呼び出す必要があるリクエストチェーンの場合(単に自分自身としてではなく)、OAuth2のOn-Behalf-Ofフローは、着信トークンを次のホップに適切にスコープされた新しいトークンと交換することを可能にします — 中間サービスが独自のサービス間資格情報を使用し、元のリクエストが実際には誰のためであったかの追跡を失うのではなく、チェーンを通じて元のユーザーのアイデンティティを保持します。


10. よくある脆弱性

alg: noneおよびアルゴリズム混乱攻撃

// ❌ 脆弱:トークンの自己申告アルゴリズムを盲目的に信頼
var algorithm = tokenHeader["alg"];
// algorithm == "none"の場合、署名チェックを完全にスキップ — 壊滅的

// ✅ 安全:検証器はトークンが主張するものに関わらず、明示的で期待されるアルゴリズムを強制
options.TokenValidationParameters.ValidAlgorithms = new[] { "RS256" };

Enter fullscreen mode Exit fullscreen mode

セクション2で言及したalg: none攻撃を超えて、関連するアルゴリズム混乱攻撃は、対称(HS256)と非対称(RS256)の両方のアルゴリズムをサポートする検証器を標的とします:RS256公開鍵を知る攻撃者は、その公開鍵をHMAC秘密として使用してHS256で署名されたトークンを作成できる場合があります — 検証器が期待する特定のアルゴリズムを厳密に強制しない場合、公開された既知の値であったものを共有秘密として扱い、偽造されたHS256署名を誤って検証してしまう可能性があります。現代の、適切に保守されたJWTライブラリはデフォルトでこれらの両方から保護しますが、これはまさに手書きのJWT検証ロジックが、確立された積極的に保守されたライブラリの使用と比べてリスクが高い、微妙な問題の種類です。

オーディエンス検証の欠如

// ❌ 署名と発行者のみを検証 — まったく別のAPI向けのトークンを受け入れる
options.TokenValidationParameters.ValidateAudience = false;

Enter fullscreen mode Exit fullscreen mode

セクション5で説明したように、オーディエンス検証をスキップすると、同じ信頼できるAuthorization Serverによって発行されたが別のAPI向けのトークンが yours に対してリプレイされる可能性があります — これは本当に一般的な設定ミスで、デバッグ中に(「エラーを解消するためにこのチェックを無効にしよう」)導入され、その後再有効化されないままになることがあります。

サーバー側検証なしでのクライアント提供クレームの信頼

// ❌ クライアントが署名なし/未検証で提供できたrole/permissionクレームを決して信頼しない
var isAdmin = Request.Headers["X-User-Role"] == "admin";

// ✅ 暗号的に検証されたトークン自体から来たクレームのみを信頼
var isAdmin = User.HasClaim("role", "admin");

Enter fullscreen mode Exit fullscreen mode

すべての認可決定は、実際の検証済み、署名済みJWTからのクレームに基づく必要があります — クライアントが望む値に簡単に設定できる、別個の署名なしヘッダーやパラメータからのものではありません。これは孤立した文脈では明らかですが、段階的に進化したシステムでは本当に一般的なミスです。「一時的な」デバッグヘッダーやクライアント提供パラメータが、静かに実際の認可決定に影響を与えるようになることがあります。

トークンタイプ/目的の未検証

// リフレッシュトークン、IDトークン、アクセストークンはすべて似た形状のJWTであり得る —
// クライアントが誤って(または悪意を持って)特定のタイプを期待するエンドポイントに間違ったものを送信するのを止めるものは何もない

Enter fullscreen mode Exit fullscreen mode

本シリーズのOAuth2/OpenID Connectガイドで触れたように、IDトークンとアクセストークンは異なる目的を持ち、通常異なる意図されたオーディエンスを持ちます — しかし、APIが着信トークンのクレームが自身の目的に期待されるものと一致することを具体的にチェックしない場合(正しいオーディエンス、期待されるスコープの存在)、トークンがそれ以外の点で完全に有効に署名されていても、その目的のために実際に意図されたものではなかったトークンを受け入れるリスクがあります。

マルチテナントシナリオでの過度に寛大なValidIssuers/ValidAudiences

// ❌ このIDプロバイダー下の任意のテナントからのトークンを受け入れ、 yours からのものだけではない
options.TokenValidationParameters.ValidIssuer = null;
options.TokenValidationParameters.ValidateIssuer = false;

Enter fullscreen mode Exit fullscreen mode

共有IDプロバイダー(Microsoft Entra IDのマルチテナントアプリ登録など)上に構築されたマルチテナントSaaSシナリオでは、マルチテナンシーを「動作させる」ために発行者検証を無効化したり過度に広げたりすると、実際にプロビジョニング/同意されたテナントのユーザーだけでなく、任意のテナントのユーザーのトークンがアプリケーションに対して認証することを意図せず許可してしまう可能性があります — マルチテナント検証には、有効なテナント発行者の意図的な明示的な許可リスト(またはセクション7のカスタム検証での同等のテナントIDクレームチェック)が必要であり、単にチェックを無効にするだけでは不十分です。


11. 検証失敗のデバッグ

実際の失敗理由の読み取り

options.Events = new JwtBearerEvents
{
    OnAuthenticationFailed = context =>
    {
        _logger.LogWarning(context.Exception, "JWT validation failed");
        return Task.CompletedTask;
    }
};

Enter fullscreen mode Exit fullscreen mode

デフォルトでは、ASP.NET CoreのJWT Bearerハンドラーはクライアントに詳細な理由を提示しません(良いセキュリティのデフォルト — 一般的に攻撃者に偽造トークンが拒否された理由を正確に伝えたくはありません)が、開発中および本番インシデント対応での正当な検証問題の診断には、サーバー側で実際の例外をログに記録することが不可欠です。

よくある失敗理由とそれが示すもの

エラー 可能性の高い原因
IDX10223: Lifetime validation failed. The token is expired アクセストークンが自然に期限切れ — クライアントはリフレッシュすべきだった
IDX10214: Audience validation failed トークンが検証するAPI/クライアントとは異なるもの向けに発行された
IDX10205: Issuer validation failed トークンが予期しないまたは信頼できないAuthorization Serverから来た
IDX10501: Signature validation failed. Unable to match key トークンのkidが現在キャッシュされているどの鍵とも一致しない — 検証器がまだ更新していない鍵ローテーションの可能性 IDX10223関連のクロック関連エラー 発行サーバーと検証サーバー間のクロックスキューが設定された許容値を超えている

手動でのトークンデコード(本番検証用ではない)

# デバッグ中の人間による検査のための、ペイロードセグメントの分割とbase64デコード(純粋に)
echo "<payload-segment>" | base64 -d | jq

Enter fullscreen mode Exit fullscreen mode

トークンのクレームを手動でデコードする(jwt.ioのようなツール、または簡単なシェルワンライナー経由)ことは、デバッグ中にトークンが実際に含むクレームを確認するための有用な、純粋に診断的なステップです — これはアプリケーショコードでの実際の暗号検証の代わりではなく、本番コードパスで単にデコードされた未検証のトークンを信頼できるものとして扱うべきではありません。


クイックリファレンス表

概念 目的
ヘッダー / ペイロード / 署名 JWTの3つのコンポーネント
kid 鍵ローテーションサポートのため、署名に使用された特定の署名鍵を識別
RS256/ES256(非対称) OAuth2/OIDCの標準 — 1つの発行者が署名し、多くの検証者がチェック
HS256(対称) 共有秘密での署名、一般的にマルチパーティOAuth2シナリオには不向き
iss検証 トークンが信頼できるAuthorization Serverから来たことを確認
aud検証 トークンがこのAPI向けに発行されたことを確認
exp/nbf検証 トークンが意図された有効期間内であることを確認
JWKSエンドポイント 署名の検証に必要な公開鍵を公開
OnTokenValidated 標準チェックを超えたカスタム、アプリケーション固有の検証のためのフック
失効リスト / jti 即時失効が必要なシナリオのためのステートフルチェックの再導入
アルゴリズム混乱攻撃 期待する署名アルゴリズムを厳密に強制しない検証器を悪用

結論

JWT検証は外から見ると欺瞞的にシンプルに見えます — 署名をチェックし、いくつかのクレームを読む — しかし、適切に実装された検証器は静かに7つの異なる、個別に重要なチェックを行っており、そのいくつか(オーディエンス検証、アルゴリズム強制、マルチテナントシナリオでの発行者制限)は、デバッグプレッシャー下で誤って弱められたり無効化されたりし、その後復元されないチェックと全く同じです。実践的な指針は、本シリーズのセキュリティ関連ガイドの残りと一貫しています:手書きの署名検証やクレームチェックではなく、確立された積極的に保守されたライブラリ(Microsoft.IdentityModel.Tokensに裏打ちされたASP.NET CoreのAddJwtBearer)を使用し、Authority経由で鍵ローテーションとディスカバリを自動的に処理させ、本当にアプリケーション固有のチェック(アカウントステータスやテナントスコープなど) — 純粋に暗号的な検証だけでは決して知り得ない — のためにその拡張ポイント(OnTokenValidated)を使用します。

これらの2、3行の設定の下で実際に何が起こっているかを理解することは、「認証ミドルウェアがエラーをスローしている」を謎から解決可能な問題に変えるものであり、一見合理的に見える検証設定が、決して無効にするべきでなかったチェックを静かに無効化していることを認識することを可能にするものです。


役に立ったと思ったら、リポジトリにスターを付けたり、修正に関するissueを開いたり、「エラーを消すためだけにチェックを無効にしない」ということを教えてくれたオーディエンス検証のバグを共有したりしてください。