要約

  • LACNICの公開PAIクライアントは権限情報を2分間キャッシュする。条件に合う更新失敗では古いエントリの時刻を更新し、同じ情報を返す。
  • 同梱テストは形式の壊れたJSONに対する継続を明示的に期待している。正常に読み取れた新しい拒否応答は、古い結果を置き換える別の経路を通る。
  • 確認したのは公開実装と模擬テストの内容であり、本番導入、失効した権限の維持、不正操作を立証したものではない。更新された容器と新しい判断は別物である。

呼び出しが成功しても、確認が成功したとは限らない

権限を問い合わせる処理がオブジェクトを返せば、アプリケーションは次の仕事に進める。だが、その結果がいま権限サービスから届いたのか、障害の間だけ以前の結果を受け入れたのかは、同じ戻り値だけでは分からないことがある。「処理を続けられる」と「権限を新しく確認できた」は、違う成功である。

この違いは、サービスが常に正常なら目立たない。新しい応答とキャッシュの更新が一緒に起きるからだ。意味が分かれるのは、新しい応答を読めなかったのに、保存した値の有効期間だけを再開する場合である。キャッシュは作り直したばかりに見えても、その中にある判断は前のまま残る。

LACNICの公開リポジトリpai-auth-ws-clientには、その具体的な処理がある。本稿はコミットb85523718bfdbd834c85add8e06c3b4a1b813e59に固定して読む。2026年9月14日に観測した主要ブランチ上の実装であり、実際のサービスに導入された版を特定したものではない。公開日と導入日、権限変更日はそれぞれ別の証拠を要する。

READMEはPAIの認証、権限確認、セッション関連機能を扱うJavaクライアントとしてプロジェクトを説明する。それは用途の根拠であって、利用環境の一覧ではない。LACNICに関係するすべてのアプリケーションがこのコードを通る、と読むことはできない。

2分間という数字の主語

PortalWSClientのキャッシュは、プロセス内の静的なConcurrentHashMapである。呼び出し時に渡されたトークンをキーとし、TokenDataと変更可能なタイムスタンプを保存する。CACHE_DURATION_MSが指定する期間は2分で、現在時刻と保存時刻の差によって期限を判断する。

期限内なら保存済みのデータを返す。このメソッドのその経路では、/authorizationから新しい応答を取得しない。問い合わせ回数を減らし、短い依存先障害の影響を抑える通常のキャッシュ処理である。キャッシュがあるだけでアクセス制御が不適切だと結論する理由にはならない。

期限を過ぎたエントリはマップから削除され、応答の取得と読み取りをやり直す。ただし、実行中のメソッドは削除したエントリへのローカル参照を持ったままである。共有マップから消すことと、その呼び出しが古いオブジェクトを利用できなくなることは一致しない。継続処理は、この参照を使って成立する。

新しい応答を正常に読み取れれば、新しいデータでエントリを作る。ここには、読み取れた否定的な権限情報も含まれる。したがって、最新の拒否が常に過去の許可に負ける、という説明は誤りだ。拒否の意味を理解できた場合と、応答自体を理解できなかった場合を混同してはいけない。

注目すべき経路は、外側のIOException処理である。以前のエントリが手元にあれば、cached.extend()で時刻を現在に変更し、同じエントリをマップへ戻す。そして保存済みのデータを返す。この操作で新しい権限判断を読み取ったわけではない。新しくなったのは、以前の結果を入れたキャッシュの計時基準である。

その後の更新も条件に合う失敗となれば、同じ延長経路を繰り返せる。このクラスには、最初の成功応答時刻を変更せずに残す別のフィールドも、それを起点とする絶対的な古さの上限も、延長回数の制限も見当たらない。ここで述べているのはクラス自身の範囲であり、呼び出すアプリケーションに別の制限がないことの証明ではない。

2分という数値は、エントリの現在の寿命を説明する。その中にある権限応答の最大年齢を、その数値だけで説明することはできない。更新失敗のたびに計時基準を動かすなら、直近の延長からの経過時間は短くても、最後の成功応答からの経過時間は増え続けうる。この二つを分けるのが本稿の中心である。

継続は偶然ではなく、テストの期待である

PortalWSClientTestのtestGetTokenDataReturnsCachedDataWhenRefreshFailsは、この動作の意図を明確にする。最初の模擬HTTP応答で認証済みのサンプルデータを得る。次にキャッシュの時刻を人為的に5分前へ動かし、2回目の模擬応答には形式の壊れたJSONを渡す。

テストは同じ認証済みオブジェクトが返ることと、HTTP実行が2回行われたことを期待する。5分は実際に待った時間でも、本番環境で観測したアクセス継続時間でもない。テスト用の時刻を変更した値である。実運用の被害時間に読み替えれば、証拠の意味が変わってしまう。

本稿ではテストを読んだだけで、実行していない。本番の認可エンドポイントへの問い合わせ、実際のトークンの使用、権限失効後の操作は行っていない。それでも、同じ結果を維持することが明示的に期待されている点は重要だ。例外処理を見て偶然を推測したのではなく、公開テストが継続を守っている。

継続の目的には合理性がある。依存サービスから正しく読める応答が一時的に来ないことは、利用者の権利がすべて撤回されたことを意味しない。そこで全セッションを止めれば、小さな障害を大きな業務停止へ増幅しかねない。短い猶予を設ける設計そのものを否定する必要はない。

ただし、猶予には始点と終点、対象となる操作が要る。以前の答えを受け入れる判断と、権限サービスが新しく答えた事実を区別できなければ、下流の担当者は何を引き継いだか理解できない。可用性を守るための裁量が、説明のない権限確認として流れていくことになる。

ログには警告がある。戻り値には年齢がない

このコードは、延長したキャッシュを一時的に使う旨の警告を記録する。したがって、動作が完全に無言だとか、監視の手掛かりが皆無だとか書くのは正しくない。運用担当者は警告を観測できる可能性がある。評価にはこの反証も含める必要がある。

一方で、警告があることと、呼び出し側が結果の古さを知れることは別である。TokenDataの項目は認証状態、トークン、ロール、エラー、ipAllowedなどである。最後に成功した応答の日時、鮮度の分類、古い値へのフォールバック表示は、このデータ型にない。

そのため、これらの項目だけを受け取った呼び出し側は、新しい応答と延長された旧応答を見分けられない。周辺アプリケーションが別途日時を保存することは可能だ。トークンの暗号学的な期限を検証したり、IP制限を確認したり、重要な書き込みの前に別の権限照会を行ったりする場合もありうる。公開クライアントだけでは、それらの有無は決まらない。

トークン自体の有効期間、キャッシュの更新後の有効期間、発行側での現在のロール状態は三つの命題である。どれか一つが成立しても、ほかが自動的に成立するわけではない。正の認証値が残ることから、不正な業務操作が許可されたと逆算するのも同様に飛躍である。

成功応答を何に対して信頼するのか

もう一つ、鮮度の始点に関わる限定がある。PortalHttpClientはTrustAllStrategyとNoopHostnameVerifierを用いてクライアントを構築する。この補助クラスでは、HTTPSのURLから得てJSONを読めたというだけで、発行元の同一性を暗号学的に検証した証拠にはならない。

これは公開実装に限った別の観察であり、実際のTLS構成、通信の傍受、攻撃を立証してはいない。重要なのは、最後の「信頼できる応答」の時刻を設計するなら、その応答の出所を誰が確認したかも定義する必要があることだ。時計を厳密にしても、受け入れる回答者の境界が曖昧なら、すべての信頼問題は解決しない。

同じ慎重さは失敗の種類にも必要である。readUrlTokenは幅広く例外を捕捉し、nullを返す場合がある。外側で古い値を延長する処理はIOExceptionを対象にする。形式の壊れたJSON、応答本文がない状態、接続失敗を、実行確認なしに同一の経路として説明することはできない。

「認可サービスが落ちれば必ず動き続ける」という保証も、「どんな障害でも失効したアクセスが残る」という非難も、証拠を越える。前者は可用性を、後者は危険性を過大に一般化する。確認できたのは、同梱テストの不正JSONの場合に古いオブジェクトを返し、該当する延長経路がキャッシュ時刻を動かすということだ。

猶予を明示的な状態にする

より明確な契約なら、出所を認証して正常に読み取れた最後の権限応答時刻と、キャッシュを最後に触った時刻を分けて保存する。古いエントリを延長しても前者は動かさない。呼び出し側には、新鮮な結果、猶予中の結果、利用不可を区別する情報を渡すという方法が考えられる。

猶予の限界は、直近の失敗からではなく、最後の信頼できる応答から絶対的に測れる。処理の種類に応じた許容差も設けられる。取り消せる参照操作と、管理者変更や不可逆な指示に同じ古さを認める必要はない。これらは分析上の操作分類であって、LACNICの特定機能がこの経路を使うと確認した例ではない。

失敗を繰り返すテストでは、元の成功時刻が維持されるか、同じ起点から上限を測るか、上限後の状態を呼び出し側が見分けられるかを問うべきだ。新しい拒否応答を正常に読めた場合も対照として残す。これは提案する検証方法であり、本稿で実施した結果ではない。

検証の単位も区別したい。クライアントが以前のオブジェクトを返したという事実と、アプリケーションが最後の業務操作を承認したという事実の間には、呼び出し側の判断がある。同じオブジェクトであっても、独立した期限や操作制限が適用される可能性がある。結果の同一性だけでは、その後の全判断を復元できない。

拒否、利用不可、猶予は、それぞれ異なる主体についての情報である。拒否は発行側の判断、利用不可は現在確認できる能力の不足、猶予はアプリケーションが一時的に旧回答を受け入れる方針だ。この三つを一つの正負だけに押し込めれば、復旧した時に何を解除すべきか、誰の決定が有効だったかも曖昧になる。

明示的な猶予があれば、すべての短い不調を利用者向けのエラーにする必要はない。宣言した範囲で不要な問い合わせを避け、業務を続ける余地は残る。違うのは、発行側がいま再確認したと説明する代わりに、以前の回答を限られた条件で受け入れていると説明できることだ。継続の責任が、その説明の中に現れる。

交代する運用担当者にも同じ情報が必要になる。キャッシュの時刻が新しいというだけで依存先が復旧したと判断すべきではない。最後の信頼できる応答の年齢、許される操作、絶対的な終了点が分かれば、次の担当者は前の担当者の暗黙の推測を引き継がずに済む。これは提案する運用契約であり、現在の実践を調査した結果ではない。

監視は警告、応答年齢、降格した運用状態を関連づければよい。生のトークンや完全なセッション情報をログに増やす必要はない。新しい信頼できる応答が来れば猶予は終わり、新しい拒否は拒否として扱われる。鮮度の説明を改善するために、別の場所へ秘密を蓄積するのは避けたい。

以上は編集上の提案であり、LACNICが公表した変更約束ではない。公開コードから周辺の制御を消し去って問題を作ることも、未確認の制御を補って問題を消すこともできない。キャッシュの2分を否定するのではなく、その数字では証明できない権限の年齢を別途表現することが課題である。

絶対的な上限を確かめるには、最初の成功時刻を固定したまま複数回の更新失敗を与え、制約の起点がずれないことを確認するという方法が考えられる。単に一回だけ古い値が返るテストとは、証明したい性質が異なる。後者は継続を、前者は継続の終点を扱う。本稿はその区別を示すにとどまり、反復テストを実行したと主張しない。

そして新しい正常な否定応答は、必ず比較対象に残すべきだ。公開実装には新しいエントリを作る経路があるので、失敗時の説明だけで全体を代表させることはできない。読めた拒否を障害として扱えば既存の決定を薄め、読めない応答を拒否として扱えば停止を広げる。状態を分ける目的は、その二つの誤読を同時に避けることにある。

資料

一次資料は、同じコミットに固定したREADME、PortalWSClient、PortalWSClientTest、TokenData、PortalHttpClientの五つで、各節に直接リンクした。公開実装と模擬シナリオの分析であり、テスト実行、本番導入、トークン失効の迂回、侵害、不正操作を示すものではない。