JWTの10年間 次の未来はどうなる? 課題と展望
引用元:https://news.ycombinator.com/item?id=44092102
俺がJWTで困るのは、ユーザーID以外の中身は長期間有効って思えないこと。
アカウントが危ない時はすぐに認証を止めたいでしょ?ってことは、結局リクエストごとに認証データベースを見なきゃダメってこと。それなら、JWTの中身もすぐそこで取れるじゃん。
役割だって、管理者から権限下げたらすぐ反映してほしいし。
そうすると、残るのはただ統一されたクライアントID形式だけで、それもまあ便利だけど、JWTの本来の「凄さ」じゃない気がするんだ。
アクティブなユーザーのログアウトとか削除、権限変更って、そんなに頻繁じゃないから、失効リストのサイズは存在するトークンの数と比べたらすごく小さいんだ。
超速いLookupシステム(例えばブロードキャストとメモリ内のストア)で失効リストを持っておけばいいし、トークンの有効期限を短めに(5分から60分とか)すれば大丈夫。
これでトークンの有効性チェックの回数が大幅に減らせるし、認証システムが止まってもある程度大丈夫になる。基本的なアプリだと認証データが他のデータと同じデータベースに入ってるから関係ないかもしれないけど、大きなシステムだとそうじゃないことが多いからね。
それ聞くと、めっちゃ狭い分野でしか働いたことないみたいに聞こえるな。
頻繁じゃない?いや、エンタープライズソフトウェアとかプロジェクト管理とか、共同作業する系だと常に起きてるよ。
JWTみたいな技術でイライラするのは、RedditとかNetflixみたいな、滅多にない超有名サイトには合うけど、それ以外には全然合わないってこと。
みんなトークンの期限切れを待つんじゃなくて、権利をすぐに無効にしたいのにさ。
それなのに、誰かがブログ書いて、バカな”ソフトウェアアーキテクト”どもがそれしか選択肢ないみたいにしたせいで、みんなこの微妙な技術に苦労させられてるんだ。
JWT使わないとなんか間違ってるみたいになってるけど、ほんとは大規模な認証方法としては超ニッチなやり方のはず。
シンプルなCookieベースのトークンの方が、今でも多くのアプリにとってはるかに良い選択肢だよ。
>一番シンプルなクライアント認証状態のユースケースだと、アカウントが侵害されたらすぐに認証を無効化したい。ということは、結局リクエストごとに認証データベースを確認しないといけないし、クレームに入ってる他の情報もそこで素早く取れただろうと。
ついでに言うと、俺が前に作ったシステムでこの「アクセスするたびに失効チェックでDBを見なきゃいけない」って問題をうまく回避できた方法があるよ。大事なのは2つ気づくこと。
1. 失効(というか通常は「明示的なログアウト」のこと)って、多くのユーザーアプリのパターンでは実はかなり珍しいんだ。例えば、多くのウェブアプリでユーザーはめったに明示的にログアウトしないし、モバイルアプリだとさらに珍しい。
2. 失効リストは、トークンの有効期限と同じ期間だけ持っておけば良いんだ。例えばトークン有効期限が30分なら、正午にユーザーのトークンを失効させても、12時半にはその失効情報を捨てて良い。なぜなら、その失効の影響を受けるトークンはすべて期限切れになってるから。
だから、トークンの有効期限を比較的短く(例えば30分)すれば、失効リストのサイズはほぼ常にメモリに収まるんだ。で、俺が作ったのはこれ。
1. トークンが失効したかどうかのインターフェースは基本的に「getEarliestTokenIssuedAt(userId: string): Date」で、要は特定のユーザーのトークンが有効とみなされる一番古い発行時刻は何時か、っていうもの。だから、ユーザーの以前発行されたトークンを失効させるってことは、この日付をNow()に設定するだけで、それより前に発行されたトークンは無効とみなされるわけ。
2. postgresに、ユーザーIDと有効なトークンの一番早い日付だけを保存するテーブルを持っていた。でも、postgresのNOTIFY機能を使って、このテーブルに何か行が追加されたら、全サーバーにブロードキャストを送るようにしてたんだ。
3. 俺のサーバーは、そのテーブルのローカルコピーをメモリに持っていただけ。繰り返しになるけど、一番長いトークン有効期限より古いエントリは捨てて良かったから、これはメモリに収まったんだ。
万が一、何らかの理由で現在の失効リストがメモリに収まらなくなったとしても、システム内に「メモリがいっぱいだ」ってことを知らせる仕組みを作っておいて、そうしたらpostgres’にコールバックするようにしてたんだけど、これも数分後には自然と解消されるんだ。失効が減って、トークン有効期限の窓が過ぎればね。
これって実際より複雑に聞こえるけど、メリットはこれ。
1. ほとんどステートレスじゃないこと。スケーラビリティにすごく良かった。
2. トークン検証はほぼ常にメモリ内でできたこと。システムを数年運用したけど、メモリ内の失効リストが大きくなりすぎた状態には一度も当たらなかったよ。
失効リストの提案RFCとかってあるの?
不正利用されたトークンに関する思いつきなんだけど、うまくいくか分からないよ。
クライアントのIPアドレスとかをトークンに入れたらどうかな?そうすれば、別のIPアドレスからリクエストが来た瞬間にサーバーは拒否(して不正利用としてマーク)できるんじゃない?
ただし、大きなビルの中のDHCP/無線みたいに、IPアドレスが変わる人を無効にしちゃうってのは分かってる。
これを見てみてもいいかも(まだドラフトだけど):
https://datatracker.ietf.org/doc/draft-ietf-oauth-status-lis…
誰かの認証を5分以内に無効にしないといけなくて、遅延が許されなかった例を何か教えてくれる?それって現実の生活より、架空の”ファイブナイン”(高可用性の指標)エンジニアリングみたいなものだと思うけどな。
Status Listは、ニアリアルタイム(ほぼリアルタイム)の失効要件は解決しないと思うな。
Status List自体にTTLがあって、そのTTLが切れるまで再読み込みされない。これは、ステートフルなRefresh TokenとステートレスなAccess Tokenを持つ一般的なやり方と実質的に似てるんだ。Status Listの”ttl”クレームは、その点でAccess Tokenの”exp”クレームと同じで、同じトレードオフがある。Status ListのTTLを短くすることはできるけど、それはキャッシュミスによる高遅延のネットワーク呼び出しの頻度が高くなるコストを伴う。
これを避けるための古典的な解決策(失効リスト全体がメモリに収まる一般的なケースで)は、トークン検証者に失効情報を伝播するためのプッシュ型かPub/Sub型のメカニズムを持つことだよ。
JWTじゃないトークンって、本当に”Bearer”トークンって言えるの?
あんたの説明だと、どのデバイスでログアウトしても「全デバイスからログアウト」になっちゃいそうじゃん。ユーザーがそんなこと望んでないだろうし、他に選択肢ないならそうなるかもだけどさ。
ちょっと視点が狭いと思うな。例えばメールで送るmagic linkだって、有効期限かなり長くできるしさ。
ユーザーの”ログアウト”ってのは、使ってるデバイスからJWTを消すだけでいいはずだよ。トークンが漏れてないなら、バックエンドの作業はいらない。有効期限内のトークンをブラックリスト化するほど安全?いや、そこまでじゃないけど、まあまあのセキュリティと実装の簡単さの間で妥当な妥協点だろ。
従業員がクビになったときは?
Status ListのTTLについて、ドラフトではオプションで、キャッシュ更新はTTL満了を待たずにできる。TTLは古さの限界を示す。失効リスト更新頻度には限界があり、特権操作には短命トークンが推奨。長寿命トークンで特権操作を許してるなら、問題は失効リストじゃないかも。
なんでJWTが認可(authorization)に使えないと思ってるのか分かんないな。それを”puppynoob”なやり方ってけなすのは、逆にあんたの知識が怪しいって言ってるようなもんだよ。GoogleのFirebaseみたいに何百万ユーザーもいる大きなシステムでは、ユーザーのクレームをトークンに保存して、それをパーミッション検証に使ってるんだからさ。
クライアントのIPアドレスって、あんたが思ってるよりずっとよく変わるんだぜ。特にモバイルネットワークだとね。
これが最善って感じかな(まあ、Bloom filter使えば失効リストのサイズをもっと縮められるけどね)、でもDoS攻撃に弱い気がするな。アカウントを1万個作って同時に全部ログアウトさせて、サーバーを遅いPostgreSQLモードに強制できるんじゃ?
magic linkは大体認証にしか使われない。だからプリンシパル削除や権限下げても、認証したやつは関連操作しかできず、チェックは検証後。magic link自体が認可クレームも運んでる場合は別。
解雇するって言ってからすぐアクセス消すとかじゃなくてさ、事前に消しとくか、少し猶予あげなよ(今日いっぱいは大丈夫とか、この会議の後ねとか)。どっちにしろ、JWTの有効期限が毎秒切れるか5分おきかとか、そんなこと全然関係ないよ。現実的なこと話したいんだ、ありもしないシナリオを夢見てるわけじゃないんだから。
Statuslistはリアルタイム取り消しには向いてないってば。あれはキャッシュしたり定期更新するリストの話なんだよ。5分遅れでもOKなら、Statefulなリフレッシュトークンで十分じゃん。Auth0とかKeycloakは対応してるよ。でも、管理画面みたいに即時取り消しが必要な場面もあって、その場合はStatelessトークンの速さとStatefulの安全性を両立させる別のアプローチ(APIチェックとか)が必要なんだ。WSO2とかがやってるやつね。Statuslistはその解決にならない。
> ユーザーの「ログアウト」アクションは、使ってるデバイスからJWTを削除するだけでいい
そうある「べき」とは言わないかな。そうなる「かも」ね。他のデバイスのセッションを終わらせられなくても気にしないなら、それでいいけどさ。
JWTを5分でリフレッシュするのはマジで問題多すぎ。間違って招待しちゃった人のアクセス即止めたいとか、金融系で不正アクセスあった時にすぐ止めたいとか、ロール変更がすぐ反映されないとか、共同作業で困る場面がいっぱいあるんだ。あと、アイドルユーザーが頻繁にバックエンド叩いたり、IOTデバイスやモバイルアプリだとトークン期限切れでデータ取れないとか、プッシュ通知と連携しにくいとか、デメリットだらけなんだよ。
アカウント侵害時の取り消しはシステムによるよ。OIDCなら、短いAccess/IDトークン(秒〜分)と長いRefreshトークン(日〜月)を組み合わせて、即時性と透過的な再認証を両立できるんだ。銀行みたいに厳しいとこは両方短く、普通のアプリならRefresh長く、みたいに柔軟に設定できるのが良い点だね。
取り消しリストはトークンが期限切れになるまで持っとけばいいだけなんだよ。例えば30分有効なトークンなら、取り消し記録も30分で消せる。こういうのはRedisみたいなシンプルなキーバリューストアを使うのにピッタリだね。dockerコンテナ立てて、手動で無効化されたトークンを、本来の有効期限と一緒に突っ込んどけばOK。
> それは珍しいことではなく、エンタープライズソフトウェア、プロジェクト管理ソフトウェア、共同作業を行うあらゆる場所で常に起こっている
そういうシステムでも、全ての有効なトークンに比べて取り消されるトークンの数はまだ少ないってことで意見一致するでしょ?
> 他の皆は、トークンが期限切れになるのを待つのではなく、権利の即時取り消しを望んでいる。
取り消しリストを使えば、それでも即時取り消しはできるんだよ。取り消しを全てのRelying Partyに伝播させちゃえば、トークンは事実上、早期に期限切れになるんだから。
> 認証システムのダウンタイムにシステムが耐えられるようにする
取り消しを扱うシステムのこと言ってる?もしそうなら、それがダウンしてる間も通常通り業務続けたら、君自身のシステムが脆弱になっちゃうじゃん。
JWTが一部のサイトにしか合わないなんて嘘っぱちだよ。サービス設計のスキルがないだけじゃないの?「皆即時取り消ししたい」って言うけど、JWTは全然それを妨げないから。短い有効期限とか、jti denylistとか、色んな方法で対応できるんだ。自分で問題を複雑にしておいて、JWTのせいにするのはおかしいよ。
トークン自体を保存する必要すらないかもね。Claimに含まれる、アカウントが良い状態にあるって示すデータだけを持っておけば。そうすれば何個でもトークン発行できるし、検証ステップでClaimが正しいことを確認すればいいだけ。
個人的には,めちゃくちゃ短命なアクセストークンを持つことの複雑さとコストは,リクエストごとに1回DB見るより全然マシじゃね?
もっとコメントを表示(1)
JWT使おうと思うたびに,なんか最適じゃない気がして,まじで使えるユースケースを見つけられてないんだよね.2FAを例に考えても,JWT使うよりDBでユニークなチャレンジキー管理した方が,セキュリティや試行回数制限の面で優れてると思うんだ.Server-server通信とかmicroserviceでも必要か疑問.結局,DB参照の方がマシなんじゃない?
JWTにはclaimsを含めることができるんだ.これが違い.JWTは最初から少し複雑なデータ構造になってる.認証(authN)と認可(authZ)をまとめてできるよ.
個別のブラウザクッキーでも全部できるけど,複雑になるだろうね.でも,セッションクッキーをDBに保存すれば,サーバー側でclaimsを扱って,そのクッキーで全部紐付けられる.だから,どっちの方法でもできると思うよ.JWTは相互認証(共有シークレット)だけど,クッキーはそうじゃない.
何だって「claims」を含めることはできるよ.ClaimsなんてJSONオブジェクトのフィールドにすぎないんだから.
もしLibsodiumのsecretboxベースで独自のトークン形式を使ってるなら,secretbox_seal(secret_key, json_encode(claims))
ってやるだけ.超簡単ワンライナーだよ.JSONじゃなくてMessagePackとかprotocol buffers使えば,トークンサイズも少し節約できる.
JWTは他に,key rotationの扱い方(「kid」claimとJWKs discovery urls使うとか),bearer tokenをPoP構造(DPoP)に紐付けるとか,そういう標準化をやってくれるかもしれないけど,それは全部標準化の話.そして標準として,JWTは柔軟すぎだし曖昧すぎる.もっと良い標準案も出てるし,JWTが使われてるほとんどのこと(相互運用性のないアクセストークン)には過剰だよ.
> 何がユースケースなの?
僕がいつも聞くJWTのユースケースって,だいたい”serverless”の流行り/バズの一部だったね.理論的にはこう説明されてた:JWTを使えば,認証に関してアプリケーションロジックをステートレスにできる.だから,セッションDB/KVストアからユーザー情報をロードする必要がない.リクエストの中に全部入ってるから….それが意味をなす唯一の方法は,アプリ自体がストレージを全く持たない場合だけだ.認証ソースも含めて,全部リモートのAPI/サービスに依存してる場合.そういうアプリも確かにあるだろうけど,ほとんどの場合はそう使われてるとは思えないね.
この業界が新しいキラキラしたものに夢中になる能力を,決して過小評価しちゃいけないよ.
何年も前,ジュニア開発者と仕事したときに,目から鱗の経験をしたんだ.彼が,CRUDなPHP/MySQLアプリを書いてたんだけど,mysql環境の設定で問題があって,”localhost”接続でよくあるソケット接続の問題だったんだ.でも,彼はその問題を理由にアプリ全体をMongoDBを使うように書き換えることにしたんだ.
言ったように:この業界が新しいキラキラしたものに夢中になる能力を,決して過小評価しちゃいけないよ.
JWTが存在するずっと前は,信頼できないチャネルを通して信頼できるデータを渡したいとき,独自の方法で署名/暗号化ペイロードを送ってたんだ.フォームフィールドに入れるとかね.今,JWTがあるから,それをやる標準的な方法があるんだ.だから,いろんな言語で同じ退屈なコードを何度も書かなくて済む.フィールド1つで文字列を渡すだけで,それがJWTだって誰かに言えば,どうパースすればいいか分かる.もう自分独自の特別なやり方をドキュメント化する必要もないんだ.
結局のところ,これは,以前は標準的な解決策がなかった特定の問題に対する単なる標準なんだ.もしそういうデータを渡すのがユースケースにとって問題じゃないなら,このツールは必要ない.
君のProtobufの例を使うと,Protobufやそれに似たツールが存在しない時代があった.Java,PHP,Pythonで全く同じプロトコルコードを手書きするのがどれだけ絶対的に退屈な作業だったか,僕は言えるよ.でも,自分でプロトコルを書かなきゃいけない状況になったことがないなら,手作業でやる苦痛も,Protobufを使う喜びも,どっちも知らないんだ.それでいいんだけどね.
JWTの問題は,標準なのにめっちゃよく間違った使われ方してることだね.
> わかんない,何がユースケースなの? サーバー間通信?
サーバー間でのJWTは,たいてい署名と一緒に使われるんだ.HMACモードじゃなくてね(だからグローバルに共有されるHMAC鍵はいらない).
そうすると,発行側はJWKSエンドポイントを公開するだけでいいんだ.使う側(ダウンストリーム)はそのエンドポイント見に行けばいいから,公開鍵を配るみたいな余計なメンテがいらないよ.
僕はJWTを使って,キャッシュされたリソースの認証をやってるよ.Edge Workerで権限確認して,DBにラウンドトリップせずにキャッシュリソースを提供できる.これをJWTなしでどうやるか(自作以外で)はよく分かんないな.ここには”ユースケースが見えない,X使えよ”みたいな意見が多いけど,こういう標準って,たとえそんなに一般的じゃなくても,有効なユースケースの結果としてほぼ必ず生まれるものなんだよ.
君のシナリオでも,偽造JWTをX回試行した後にユーザーIDに追加の保護を適用することはできるよ.少なくとも,無効な署名のJWTが来たらアラート出すとか.それか,2FAチャレンジキーをJWTの中に入れちゃうとか,ただJWTをクライアントと共有する情報を保持するコンテナとして使うとかね.
クッキーで既にできないこと以上のことはJWTではできないっていう意見には同意だけど,ユースケースはウェブブラウザじゃなくてアプリ,特に生のHTTP APIコールしてクッキーJarを実装してないアプリ向けだと思うんだ.
で,ほとんどの会社が”アプリファースト開発”するせいで,結局ウェブブラウザでもJWTをサポートしなきゃいけなくなって,既存のクッキーJarを活用する代わりに,手動でlocalstorageとかアプリケーション状態に入れちゃうんだよね.
つい最近,プラットフォームがJWTしか渡さなかったから,SSOソリューションをJWTで実装せざるを得なくなって,最終的にJWTを暗号化されたHttpOnlyクッキーの中に入れたんだ.なんか”hat-on-a-hat”(過剰)な感じだったけど,まあいいか.
webブラウザでJWTをlocalstorageとかに入れるのめんどいじゃん?cookieでよくね?
うちもSSOでJWT使ったけど、暗号化HttpOnly cookieに入れたよ。ちょっと変?なんで?cookieにJWT置くの普通じゃん。サーバー側でフロント作るなら最初のリクエストで認証必要だし、どうせcookie以外には頼れないしね。
jtiクレームっていうのがあって、トークンIDを保存できるから、サーバー側で発行した全てのトークンを追跡できるよ。256bitをブルートフォースで破るのは非現実的だし、それより危ないシステムいっぱいあるじゃん。孤立したJWT署名ってのはすごく特定の例に見えるな。俺にとってJWTのいい点は、asymm signedして検証できることだな(ID tokens)。
これ、数年前のIDカンファレンスであったJWT作者の一人、Brian Campbell氏の動画。見てみて。URLは
https://www.youtube.com/watch?v=IgKRGS6cQWw
JWTって人気だけど、セキュリティ界隈じゃめちゃくちゃ叩かれてるんだ(”ひどい標準”、”猿が作ったRFC”とか)。実際、仕様書に根本的な欠陥があって、それが脆弱性につながってる。この動画は、JWTのダメなところと、それが本当にダメなのかを真剣に見ていく感じ。
JWTってデカいし、JS使う人はエンコーディングが暗号化じゃないってよく忘れがちだよね。ニュースサイトのトラッカーがurl/headerで俺の氏名とかメールアドレス入りのJWTを3rd partyに送ってるの見たよ。プライバシーポリシー違反じゃん。でも、JWTはすごくオープンで便利。トークン見ればサイトのアーキテクチャ設計についていっぱい学べるから。
tptacek氏の調査(https://fly.io/blog/api-tokens-a-tedious-survey)は読むべき!みんなMediumとかLLMで決めてるけど、JWTは多くの用途に合わない。低トラフィックならランダムIDがベスト。軽くて安全、取り消しも簡単。セッション固定とタイミング攻撃に注意してね。対策は安全なID生成とDB検索時のハッシュ化。無理ならProtobufとかMacaroonsみたいな独自形式の方が、軽くて自由に設計できるからおすすめ。
俺はJWTとか他のいろんな標準を使ってるけど、自分で選んだわけじゃないんだよね。君が提案するみたいにできたら、すごくシンプルになって最高なんだけど、自分でmulti-org/SSO/2FAの認証プラットフォームなんて作らないし。そういう認証機能が必要だから、アプリがでかいからじゃなくて(小さいアプリだよ)、これらの標準を使うことになったんだ。
セッションIDをハッシュインデックスでインデックス化するだけじゃダメだよ。Postgresのハッシュとか暗号論的じゃないから、衝突が細工できて結局総当たりみたいになることがある。だから、DB検索する前にID自体を暗号学的ハッシュでハッシュ化してから検索する方が、タイミング攻撃対策としてはずっと安全なんだ。
sessionIDはcookie盗まれるとヤバいよね。IPに紐付ける手もあるけど手間かかる。JWTなら、ペイロードにIP入れとけば、違うIPからのリクエストは再認証させて新しいJWTを再発行すればいいんだ。ネットワークRoamingしてる顧客にも対応しやすいよ。
>中身?全然驚かないね、俺のフルネームとかメールアドレスとか<br>俺の記憶が正しければだけど、まさにこれがあるからJWTには個人を特定できる情報入れない方がいいって推奨されてるんじゃないの?
えーと、JWTは署名されてるんだよ−署名はそれ自体が暗号化じゃない。でも、リンクされた記事にも書いてあるけど、JWEっていうのもある。そっちは完全に暗号化されてるね。
サーバー間のJWTは好きだよ。でもサーバーとクライアント間で使うと、結局cookieとかsessionを作り直すことになるんだよね。あくまで俺の経験と意見だけど。みんなの意見も聞きたいな。
Cookiesはサーバー制御だけどJWTは双方の信頼が必要。自分のとこはApacheとかPrivacyIDEAも絡んでCookiesとJWTを両方使ってるよ。authNにCookies、authZにJWTって感じかな。Cookiesでも両方できそうだけど、claimsを渡すにはJWTの方が楽だね。
俺は主にRSA鍵ペアでJWTを使ってる。相手のサービスに鍵ペアを作ってもらって、公開鍵を送ってもらうんだ。秘密鍵は絶対に見ない。そうすれば、相手のトークンを公開鍵で検証できるんだよ。こうすれば秘密鍵を共有する心配がないね。秘密鍵は相手のサービスから絶対に出ないから。
それってOIDC/JWKSのことじゃないの? あれもPKIを使って公開鍵でJWTを検証するよね。
たぶん、公開鍵で検証って言いたかったんだと思うよ。
そうだよ。公開鍵で検証って言いたかったんだ。(顔面パーム、修正済み)
ごっちゃになりやすいよね。
>Then I can verify all their tokens with the private key.<
うーん。違うよ。トークンは公開鍵で検証するものだよ、秘密鍵じゃなくて。そんな誤用を許容するライブラリ、何使ってるの?
cookieはブラウザがドメインごとに制御するから、普通はドメインを跨いで再利用できないよ。あとcookieは暗号署名されてないからクライアント/ブラウザに簡単に偽造されちゃう。一方JWTはドメインを跨いで使えるんだ。だからIDPから発行されたJWTを別のドメインで信頼させることができる。暗号署名はデータの整合性検証に役立つね。sessionはたいてい単一のbackend/application serverに紐づいてる。別のアプリでsessionデータを再利用するのは難しいんだ。それに対してJWTは、異なるアプリサーバー/マイクロサービス間でsessionデータを共有できるんだよ。
”Also cookies are not cryptographically signed and thus easily forgeable by the client/browser.”
俺のApacheのWeb的なやつは、ちゃんと暗号化されたクッキーを喜んで配ってるぜ:
https://httpd.apache.org/docs/2.4/mod/mod_session_crypto.htm
そこで君が書いてるクロスサイトの問題も解説されてるよ。
JWTsは相互に共有された秘密鍵で、ノブが付いたまま渡せるー中にたくさんのデータを保存できるんだ。クッキーはどっちかっていうと一発使い切りで、一つのデータって感じかな。
暗号化だけじゃ普通は認証にはならないから、そのApacheのモジュールがユーザーにis_admin=0を1にひっくり返すのを許しちゃっても驚かないな。暗号化がそういう操作に対して十分もろいならね。特にあのページが3DESって書いてるからね。
もっとコメントを表示(2)
> Also cookies are not cryptographically signed and thus easily forgeable by the client/browser
確かにクッキーに署名しないこともできるけど、俺が知ってるサーバーライブラリでそれがデフォルトになってるものはないよ。もし使ってるライブラリが署名に秘密鍵を要求しないなら、それは報告すべき問題だね。
あと、JWTのライブラリでalgorithmに”none”をデフォルトにしてるのも知らないな(一部は仕様に反して完全に避けてるけど)、まあJWTも安全じゃない使い方はできるけどね。
どっちかって言うと、10年間ツール周りで苦労したって感じかな。
もっと良い解決策がどんなのかは知らないけど、OAuthとかJWTの設定って、どの技術使っててもなんか大変なんだよね。
> It’s often said that one sign of a standard having succeeded is that it’s used for things that the inventors never imagined.
それは確かに、何かの役立ち具合とか、いろんなことに使える汎用性の証拠だよね。おめでとうって感じかな。
Hacker NewsでJWTとかOAuthの記事を遡って検索してみると、JWTって何で何じゃないの?みたいな堂々巡りの議論が何百件も見つかると思うよ。みんなこの二つを分けて考えられないみたいだね。
正直まだあんまり理解してないんだよね。最後に使ったのはたぶん2016年か2018年くらいのクライアント案件で、その時学んだこと全部忘れちゃったよ。でもRFCがあるってのは結構すごいなと思うけど。
簡単に考えるなら、誰が作ったかを証明するちょっとした暗号的なオマケがくっついたただのJSONオブジェクト、って考えるのが一番わかりやすいと思うよ。
JSON Web TokensはJSON Object Signing and Encryption (JOSE)っていう標準ファミリーの一部で、Webで扱いやすい形で暗号の元を入れられる箱みたいなもんなんだ。JWS (署名されたデータ)は有名だけど、JWE (暗号化されたデータ)とかJWK (鍵のデータ)もあるよ。JOSEを使えば、暗号システムで必要なデータや鍵を表現するのにいちいち新しく考えなくて済むんだ。一番使われるのは認証システムでJWSが使われる場合だけど、他にも使い道はたくさんある。完璧じゃないけど、アプリ側で暗号を扱うのをすごく簡単にしてくれたんだよ。
SameSiteとかCSP-headersがある今、JWTの存在意義がよく分かんないんだよね。
バックエンドの視点からすると、ほとんどのフレームワークはクッキーでセッション管理機能が最初から入ってるから、特定のクライアントを無効にするのがめっちゃ簡単なんだ。
でもJWTだとそういうのがあんまりないから、クライアントを管理するためにセッション周りの機能を全部自分で作らないといけないんだよね。
SameSiteとかCSP HeadersとJWTって、全然関係ないよ。俺はSPAとRESTのバックエンドの認証にJWT使ってるけど、JWTはCookieに保存してるんだ(SameSite=strictとHttpOnlyでね)。
JWTは大好きなんだけど、複雑な認可情報を詳しくコンパクトに伝える、もっと良い標準があるといいなと思うんだ。
パーミッション(よくあるスコープより複雑なやつ)をJWTに入れようとしたら、すぐにサイズが大きくなっちゃった。ロール情報を入れても、結局ロジックが一箇所に集まっちゃう。
たぶん、複雑な認可情報を一つのオブジェクトで繰り返し渡すっていう考え方が根本的に間違ってるのかもだけど、もしアイデンティティ標準で何か一つ欲しいって言われたら、これが一番上に来るね。
権限をJWTに入れるとすぐデカくなるってやつ、うちはビットマスクで解決したよ。
例えば、「Calendarオブジェクトから読み取りを許可する」っていうアクセスルールをエンコードしたいとするじゃん?よくあるCRUD操作は4ビットでエンコードできるんだ。全ビットがゼロならアクセスなし。最初のビットが1なら作成できる。二番目が1なら読み取りできる、みたいにね。
で、もしシステムに32種類のオブジェクトタイプがあったら、「13番目の位置がカレンダーをエンコードしてる」みたいにできる。だから、32*4 = 128ビット、つまりたったの16バイトで、32種類のオブジェクトに対するCRUDルールの情報をエンコードできちゃうんだ。
確かに複雑に聞こえるけど、ライブラリにしちゃえば、もう考える必要なくなるよ。
それは、オブジェクトのタイプ内で区別がない限りはうまくいくやり方だね。
「どの」カレンダーを編集できるの?全部?
アクセスできるリポジトリを全部トークンに入れる、なんて要望は時々あるんだけど、それは…難しいだろうね。
全ての場合に有効ってわけじゃないのは同意だよ。
うちは、サービス固有の複雑なアクセス制御ロジックがあって、トークンで分かりやすく表現しにくい場合もあるから、DBを見る必要もあるんだ。でも、それは普通問題ない(パフォーマンス的にもね)。だって、作業するエンティティを取得するためにどうせDBに繋がなきゃいけないからさ。
アクセストークンは、「ユーザーは原則としてカレンダーにアクセスできるかな?」みたいな一般的なチェックのDB負荷を減らしてくれるんだ。そして、アクセスできるユーザーに対しては、サービスが必要ならDBで追加のチェックをするんだよ。「このカレンダーの本当の所有者はユーザーかな?」とか、他の追加のアクセスロジックがあればね。
「ユーザーは原則としてカレンダーにアクセスできるかな?」っていう一般的なケースのチェックは、DBやキャッシュに全く負荷をかけずに、UIでメニュー項目を隠したり、即座に403エラーを返すのにも役立つんだ。
Biscuitをチェックしてみるといいかもよ。
「Biscuitは分散検証、オフラインでの権限縮小、そして論理言語に基づいた強力なセキュリティポリシー強制を備えた認可トークンです」だってさ。詳しくは https://www.biscuitsec.org を見てみて。
認可って、本当に一般的に解決できる問題じゃないんだよね。どんな大きなシステムだって、結局は独自の「こうしたい」って癖が出てきちゃうんだ。
それを一般化しようとすると、痛い目に遭って苦しむことになるし、最終的にはAWS IAMみたいな感じになっちゃうんだよ。
https://zanzibar.tech/ を見てみてよ。
ああ、トークンってほんと、あっという間に長くなるよね。俺たちも5年前に、クライアントにロールを渡すためにJWTを使ってみようとしたんだけど、結局色々な問題が出てきちゃったんだ。
HN(Hacker News)の伝説から、前知識として読んでおくと良いのがこれだよ。
https://fly.io/blog/api-tokens-a-tedious-survey/
Pasetoの方がいいよ(https://paseto.io/)。でも残念ながら、OAuthはJWTを使わせるんだよね。
俺が知る限り、OAuthはJWTを必須にしてないよ(実際にはよくJWTが使われるけど、不透明なトークンでいいはず)。でも、OAuthをベースにしたOpenID ConnectはJWTが必須なんだ。
OAuthのいくつかのオプション拡張RFCや新しいOAuth 2.1ドラフトなんかはJWTに依存したり推奨したりしてるよ。コアのOAuth 2.0はJWTを強制してないんだけど、OpenID Connectの影響とかもあって、JWTが必要なOAuthの使い方が増えてるんだ。
特にクライアント認証やProof-of-Possessionのところで、JWTが推奨されてるよ。例えばOAuth 2.1ではDPoPの実装方法としてJWTベースが推奨されてるんだ。
> OAuth forces the usage of JWT.
OAuthじゃなくてOIDCがIDトークンに要求してるんだ[0]。OAuthはJWTより前にできてるけど、拡張でJWTを使うことが多いね。
それにしても、Pasetoの需要ってあるのかな?
うちの会社でもPaseto[1]の課題が何年も前からあるけど、コミュニティのサポートはイマイチみたい。実装もライブラリばっかりっぽいしね[3]。他のコメントも参考になるよ[2]。
他の人も言ってるように、実際には拡張とかOIDCでJWTが事実上強制されてるよね、俺はちょっと短く言っただけなんだ。
これは需要の話じゃなくて、こうした設計がイマイチなAPIの危険性を解決して、ウェブ全体のセキュリティを向上させるための話であるべきなんだ。
他の選択肢もあるよ。
確認してみて:https://cozejson.com
仕様はこちら:https://github.com/Cyphrme/Coze
俺はあんまり好きじゃないな、JSONをそのまま送るのは、多くの認証フローで要求されるみたいにURLにエンコードするときに問題があるんだ。Pasetoはバージョン+ペイロード+署名全体をエンコードして、持ち運びやすくしてる。
もちろんCozeのJSON全体をbase64エンコードすることもできるけど、それは仕様の一部じゃないし、仕様が弱いってことだね。
ふーん、SAMLがぶち当たるような二重署名問題が、ここではどうなるんだろうね。誰かが余分なsigオブジェクトを追加したらどうなるの?
どうしてそれが優れてるのか、潜在的な暗号攻撃についての漠然とした話を除けば、俺には分からないな?
なんかNIH(Not Invented Here)症候群的な独自シリアライズ形式で、暗号スイートも固定されてるみたいだ。委譲とかクレームみたいなユースケースはサポートしてなさそうだし。
記事の下の方にある講演を見てみて。JWT/JOSEは危険な落とし穴だらけなんだ、それは理論だけじゃなくて、何度も設計が不十分で、書かれてる通りに正しく実装するのはリスクが高すぎることが示されてる。
より少なくて、既知の安全な暗号プリミティブを仕様の一部として使うことで、セキュリティを間違えることが不可能になって、悪用できなくなるんだ。