JWTの課題とPASETOという選択肢について

はじめに

この記事は、TVer Advent Calendar 2025の4日目です。 3日目は、@takanamitoさんの「2025年AIにやらせてみてよかった開発関連の作業」でした。

TVerでは利用していない方式ですが、年末は業務であまり触れない事を勉強しようと決めているので、最近学んだPASETOについてJWTの課題感と共に自分が感じている事を書きたいと思います。

JWTは認証・認可の文脈で広く使われます。漠然と「JWTは運用が難しい」「PASETOの方が安全」というのは耳にしますが、実際何が難しくて、何故PASETOが安全と言われるのかよく分かっていなかったので調べることにしました。

JWTの何が難しいのか

JWTの難しさは「仕様が柔軟すぎる」ことに尽きるのではないかと思います。柔軟性は本来メリットですが、セキュリティを考える上では攻撃者が悪用できる余地が増えることも意味しているのかなと思います。

アルゴリズム選択の問題

JWTのヘッダにはalgフィールドがあり、署名アルゴリズムを指定します。

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

これが問題になるケースがいくつかあります。

alg: none という仕様がある

RFC 7519では署名なしトークン(alg: none)が仕様として許容されています。信頼された環境内での用途を想定しているはずですが、実際には攻撃に悪用されるケースもあったようです。 トークン内のalgフィールドをみて検証方法を決めます。alg: noneをみて「署名検証は不要」と解釈し、署名がなくてもトークンを受け入れてしまいます。

algの値をトークンから読み取って検証方法を決めるという設計自体がリスクの元になっているのだろうなと思います。

JOSEヘッダの拡張性

JWTが基づくJOSE仕様は、さまざまなヘッダフィールドを許容します。

  • kid(Key ID): 使用する鍵の識別子
  • jku(JWK Set URL): 鍵を取得するURL
  • jwk(JSON Web Key): 鍵そのものを埋め込み

便利ではありますが、実装次第では攻撃経路になります。たとえばkidの値をファイルパスやSQLクエリにそのまま使ってしまう実装があるとしたら、パストラバーサルやSQLインジェクションの入り口になります。 JWT仕様自体に問題があるというより、仕様が許容するオプションが多くて安全に実装するのが難しいという問題に繋がってると思いました。

JWSは暗号化ではない

JWTのペイロードは暗号化されておらず、Base64URLエンコードされているだけなのでデコードすれば当然中身が読めます。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

中央の部分をデコードすると:

{"sub":"1234567890","name":"John Doe"}

JWTは改ざん防止であって秘匿ではないため、これを理解せずに機密情報をペイロードに入れてしまえばインシデントに繋がります。暗号化したい場合はJWEを使う必要があると思いますが、複雑性は増していくと思いました。

トークンの失効が難しい

JWTのメリットとして「ステートレスである」が挙げられますが、同時に「発行後にサーバー側から無効化できない(しづらい)」ということでもあると思います。 トークンが漏洩した場合の対処は限られます。

  • expを短くする → リフレッシュ頻度が上がる
  • ブラックリストを管理する → 結局ステートフルになる
  • 鍵をローテートする → 全ユーザーが再ログインになる

改めてPASETOとは

PASETO(Platform-Agnostic Security Token)は、JWTの課題を踏まえて設計されたトークン仕様です。「暗号の選択を開発者に委ねない」ことが特徴です。

バージョンと用途でアルゴリズムが固定される

PASETOのトークンはこのような形式です

v4.public.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0...

先頭のv4.publicでアルゴリズムが一意に決まります。

バージョン 用途 アルゴリズム
v4.public 署名 Ed25519
v4.local 暗号化 XChaCha20 + keyed BLAKE2b

JWTのalgフィールドに相当するものがなく、バージョン番号がアルゴリズムを決定します。そのため開発者がどのアルゴリズムを使うかを選ぶ必要がありません。PASETOではトークンのプレフィックスをみた時点で処理が確定するため、攻撃者が入る余地が十分に減らされているなと思いました。

JOSEヘッダがない

PASETOには前述のJOSEヘッダがありません。そのため、それらを悪用する攻撃もそもそも成立しません。鍵のローテーションでKey IDが必要な場合は、footerという別の領域を使います。footerは署名の対象に含まれるので、改ざんはできません。

localモードで暗号化がビルトインされる

PASETOには2つのモードがあります:

  • public: 非対称鍵による署名。秘密鍵で署名し、公開鍵で検証。ペイロードは平文
  • local: 対称鍵による認証付き暗号化。ペイロードが暗号化され、改ざん検知もされる

JWTでペイロードを暗号化するにはJWEを別途組み合わせる必要がありますが、PASETOは単一の仕様で両方カバーしています。

PASETOが解決すること、解決しないこと

解決すること

JWTの問題 PASETOでの対応
アルゴリズム選択の複雑さ バージョンで固定、選択不要
alg: none 攻撃 そもそも存在しない
対称/非対称鍵の混同 local/publicで用途が明確に分離
JOSEヘッダ経由の攻撃 ヘッダが存在しない
暗号化にJWEが必要 localモードでビルトイン

解決しないこと

失効(revocation)の問題

PASETOもステートレストークンなので、発行後の強制失効はJWTと同様に難しいです。短い有効期限、ブラックリスト、鍵ローテーションといった対策が必要な点は変わりません。

外部サービスとの相互運用性

OIDC(OpenID Connect)はIDトークンとしてJWTを前提としているので、PASETOは使えません。Google、Apple、Auth0などの外部IdPと連携する場合はJWTを使う必要があります。

PASETOはどこで使えるのか

内部システムでの認証・認可

  • マイクロサービス間の認証
  • BFF(Backend for Frontend)と内部APIの間
  • バッチジョブやワーカー間の認可

こういった用途では、トークン形式を自由に選べます。PASETOの「設定不要で安全」という特性が活きます。

OIDCでは使えない

OAuth 2.0 / OIDCの文脈では、IDトークンはJWTでなければなりません。これは仕様上の制約で、PASETOで代替することはできません。

外部IdPを使う場合、そこから発行されるトークンはJWTです。PASETOを採用するとしても、「外部認証はJWT、内部認証はPASETO」という二層構成になります。

外部サービスでのサポート状況

主要なIdPやAPIゲートウェイでPASETOをネイティブサポートしているものは限定的です。

  • Auth0, Okta, Azure AD, AWS Cognito: 未サポート(JWTのみ)

PASETOは2018年策定と比較的新しく、JWTほどのエコシステムはまだありません。

Goでの実装例

Go言語でPASETO v4を扱う場合のサンプルをaidanwoods.dev/go-pasetoを利用して紹介します。

v4.public(署名)

package main

import (
    "fmt"
    "time"

    "aidanwoods.dev/go-paseto"
)

func main() {
    // 鍵ペア生成
    secretKey := paseto.NewV4AsymmetricSecretKey()
    publicKey := secretKey.Public()

    // トークン作成
    token := paseto.NewToken()
    token.SetIssuedAt(time.Now())
    token.SetExpiration(time.Now().Add(15 * time.Minute))
    token.SetSubject("user_123")
    token.SetString("role", "admin")

    // 署名
    signed := token.V4Sign(secretKey, nil)
    fmt.Println("Token:", signed)

    // 検証
    parser := paseto.NewParser()
    parsedToken, err := parser.ParseV4Public(publicKey, signed, nil)
    if err != nil {
        panic(err)
    }

    sub, _ := parsedToken.GetSubject()
    role, _ := parsedToken.GetString("role")
    fmt.Println("Subject:", sub, "Role:", role)
}

実行結果:

Token: v4.public.eyJleHAiOiIyMDI1LTExLTI3VDIxOjI3OjQyKzA5OjAwIiwiaWF0IjoiMjAyNS0xMS0yN1QyMToxMjo0MiswOTowMCIsInJvbGUiOiJhZG1pbiIsInN1YiI6InVzZXJfMTIzIn0wyJdZ4EpvGECd-X6FlN2xaPQf8ASp7mogQQSDRGyiSVGh9jHhiKgloeROURyq74X1z46uSH14nvMC6Rhm6TcK
Subject: user_123 Role: admin

v4.public. で始まるトークンが生成され、検証後にペイロードの値が取り出せています。

v4.local(暗号化)

package main

import (
    "fmt"
    "time"

    "aidanwoods.dev/go-paseto"
)

func main() {
    // 対称鍵生成
    key := paseto.NewV4SymmetricKey()

    // トークン作成
    token := paseto.NewToken()
    token.SetExpiration(time.Now().Add(15 * time.Minute))
    token.SetString("user_id", "12345")
    token.SetString("internal_permission", "read:secrets")

    // 暗号化
    encrypted := token.V4Encrypt(key, nil)
    fmt.Println("Encrypted:", encrypted)

    // 復号
    parser := paseto.NewParser()
    decrypted, err := parser.ParseV4Local(key, encrypted, nil)
    if err != nil {
        panic(err)
    }

    userID, _ := decrypted.GetString("user_id")
    fmt.Println("User ID:", userID)
}

実行結果:

Encrypted: v4.local.V5XpMGl4p6CR-XkQxFiuQu3Ftwsf8EcrD_sZEC2ARhsWW0HHXTzkQ8iAyvu9oJO5sz9qpT4wX9_kgG4Wz2aBDEmAgddXzSuRUKwJC3aghIjSJUyrxiGTlJjJ-u2cpzVxlBYt3yFaZjpuM0NLT-uaymP6i1SQdlDWLb7QrBkBNSpodV_rTnmyU4GAxA57ns_RemLynuftzaI_NQ
User ID: 12345

v4.local. で始まるトークンが生成されています。v4.public と違い、ペイロード部分も暗号化されているため、トークンを見ても中身はわかりません。

JWTのライブラリと比べると、アルゴリズムを指定するコードがありません。NewV4AsymmetricSecretKey() を呼んだ時点でEd25519が確定するので、開発者が暗号の詳細を意識する必要がありません。


結局どうすればいいのか

PASETOが向いているケース:

  • 内部サービス間の認証を新規に設計する場合
  • 暗号化トークンが必要だがJWEの複雑さを避けたい場合
  • 「アルゴリズム選択」という判断を減らしたい場合

JWTを使い続けるべきケース:

  • 外部IdP(OIDC)との連携が主な用途の場合
  • 既存システムがJWTで安定稼働している場合
  • エコシステム(ライブラリ、ツール)の充実度を重視する場合

「外部連携はJWT、内部はPASETO」という構成は合理的ですが、2種類のトークン形式を運用するコストも考慮が必要です。

まとめ

PASETOは様々なリスクを「選択肢を減らす」ことで解決しています。アルゴリズムはバージョンで固定、JOSEヘッダは存在せず、暗号化もビルトイン。

ただし、PASETOはJWTの完全な代替ではありません。OIDCのエコシステムではJWTが必要ですし、外部サービスのPASETOサポートもまだ限定的です。

採用を検討するなら、「どこで使うか」を明確にすることが大事だと思います。個人的にはこのメリットを大きく活かせるパターンはあまり思いつきませんでした。

最後に

PASETOが今後どこまで普及するかは分かりませんが、「選択肢を減らすことで安全性を高める」という設計思想は、認証トークンに限らず参考になりそうだなと感じました。 今回はトークン仕様の比較に留まりましたが、実際の運用では鍵管理やトークンの失効戦略など、まだまだ深掘りできるテーマがあります。引き続きそのあたりも勉強していきたいと思います。

明日の記事は、@m-yoshizawaさんのCODE BLUE 2025 AIエージェント・セキュリティ・マスタークラス 参加してきた です!

「はじめて学ぶバイナリ解析 不正なコードからコンピュータを守るサイバーセキュリティ技術」 を読んだ

この本では、基本的なコンピュータアーキテクチャの話から始まって、アセンブリを読むための基礎知識、アセンブリの書き方を学習し、最終的には古典的なバッファオーバーフロー攻撃をお試しできるような構成になっていて非常にわかりやすかったです。 一年くらい前に、策謀本を読み始めてよく分からなくて積んでしまったんですが改めて理解したくてその基礎づくりとして読みました。

www.amazon.co.jp

特に良かったのは、4章から6章でメモリとスタック領域、レジスタ、アセンブリ基礎を手を動かしながら学習できるのでこの辺の座学っぽい所で眠くなってしまう自分にとっては良かったです。 それと、最終章の発展的なバッファオーバーフローの手法は単純に読んでいてワクワクしました。この本ではあえて攻撃を成立させるためにセキュリティ機能をオフにして実行したりしますがオンの場合でもそれを回避して攻撃を成立させるような手法が現在でも研究されていて、今後はそういう所にアプローチしていけると面白いなと思えました。

「Auth0」で作る!認証付きシングルページアプリケーションを読んだ

この本では、SPA構成のアプリケーションにおいてAuth0から取得したTokenでバックエンドサーバーの保護されたAPIを叩いてみるみたいな全体の流れをハンズオン形式で体験できてとても良かった。 夢中になってやったので仕事終わりの時間を使って二日くらいで読み終えたのでAuth0でPoCしてるみたいな人にはおすすめです。

www.amazon.co.jp

特に良かったのは、前半で改めて認証やJWTについて説明してくれているところが一つ

ハンズオンは言うまでもなく良い体験ですが、最後の方でAuth0で実行できるサーバーレスコードのサンプルが勉強になりました。Auth0では、ログイン時やユーザー登録時にフックしてカスタムコードを書けるので、例えば「ユーザー登録時に自前で立てているDBへも情報を同期する」ような事が可能です。本で紹介されているのはAuth0のRulesという機能でしたが、現在(2021-10-28時点)では、Auth0 Actionsという新しい機能もあって似たような事が実現できるので今後はおそらくAuth0 Actionsを使っていく事になりそうという事も知れて良かったです。

色々学べたのでまた違った構成で認証周りの実装を書いてみたいと思います。
個人的には、このスピード感で難しい認証周りを実装できるのはAuth0すごいなあと感じていて、もしユーザー数が数年は1000人いくかいかないだろうなみたいな社内アプリや個人開発のアプリを作るかとなったらもう個人的にはAuth0一択だなと思いました。

Autifyで作ったテストプランをデプロイ時に自動でスケジューリングさせたい

Autifyめちゃくちゃ便利なので、作成したテストプランをCI/CDに組み込んでデプロイのタイミングで自動でスケジュールして欲しくて、調べてみると簡単に出来たのでメモ。 アカウントページでアクセストークンを払い出せるので、作成し、スケジュールさせたいテストプランのIDを控えたらあとはcurlで叩くだけだった

curl -XPOST -H "Authorization: Bearer ${AUTIFY_PERSONAL_ACCESS_TOKEN}" https://app.autify.com/api/v1/schedules/${AUTIFY_TEST_PLAN_ID}

後はCDのスクリプトに組み込んであげればやりたいことはできそうだった

テストの結果取得もAPIでやろうと思えばやれると思いますが、テストが終わるまで定期的にcurlで見に行かないといけないので、そうするよりSlack連携させて、テストの完了通知を受け取るようにしたほうが容易でした。

APIドキュメント slack連携

職歴書管理はじめました

リモートワークも一年が過ぎて、日々の過ごし方が以前とガラッと変わったのもあったり最近モチベーションも低下してきたので副業でもやってみて、普段とは違う緊張感も味わうかと思って副業を探していました。 大体の企業さんが、「職歴書ください!」っていうところが多いのですが職歴書なんてもう数年書いてないし内容も古いしでこの時点で面倒になってしまうんですよね。 職歴書のフォーマットも色々ありすぎてどれがいいかとかテンプレダウンロードしてきてもなんか表示崩れとかしちゃったりでなんとかならんかな〜と思ってた所、プログラマーの仲間にマークダウンで書いてgithubにリポジトリ置いてそこで管理しているという人を見つけました。

ということで、マークダウンで書いてgithubで管理してPDF化もできちゃえば楽だなと思ってPythonで実装されたmd2pdfを使ってPDF化できるように職歴書のテンプレを公開しました。

github.com

環境依存なく誰の環境でも動くようにPDF化の部分をDockerコンテナ内で処理させるようにしてて、レポジトリをcloneなりforkなりしてもらってdocs/README.mdに自身の職歴書を書いてもらいmake build_resume を実行してもらえればPDFファイルが出力されるようになってます。

docs/main.cssでスタイリングも可能なので、自身のキャラクターにあった感じにスタイリングしてもらえるとPDFにした時に見た目も綺麗だと思います。

テンプレから作られるPDFはこんな感じ。 f:id:keisuke_t:20210315021807p:plain

何でも管理しておくとフットワーク軽く生きていけると最近しみじみ感じたので誰かの役にたったら嬉しいです。 良い日を〜

夏休みにHackTheBoxを延々とやってた

世間は大変だし、どうせ家にいるなら何か興味のあることを勉強しようと思ってHackTheBoxのRetired Machineを延々とやりました。

www.hackthebox.eu

サーバーサイドのプログラムを書いて生活をしてますが、セキュリティに興味があって細々と勉強していました。そうすると、必然的に手を動かして攻撃にチャレンジしてみたいなと思ってくるのですが、当然そこら辺のサイトに対して練習するなんてことはできません(ポートスキャンですら危ないですね) ということで、HackTheBoxのVIP会員になってみて夏休みに12台のマシーンのroot権限まで取っていきました

チャレンジしたものは、Retired Machineに分類されているので、多くの人が攻略のwriteupを書いてくれていて、詰まったマシーンはwriteupを見て勉強させてもらいました。 今のところは難易度Mediumに満たないくらいの簡単なものをこなしてるだけなので、基礎的なCSの知識、ググる力、コード読んで何してるかさえある程度分かればそこまで困難ではないなという感じでした。 まだまだ難易度がこれから上がっていくのでツールに頼った知識だけでなく、自らペイロードを書ける力や攻撃の想像性が必要とされるんだろうなという感じで当分はHackTheBoxをやることと、Active Machineにもチャレンジしてみてランクもあげてみたいなと思いました。次はランクあがったら記事書こうかな。。。

やる上で意識してることは、終わったら次みたいにならないように例えば、何らかのWebサービスが80番に立っててsearchsploitで脆弱性探したらLFIやRCEがあって突いてみるとなんか成功したみたいな場合でも他の脆弱性のレポートは読んでみたり実行してみたりすると色々勉強になるので意識して取り組んでいます。この類のマシンだとXSSとかあってもpwnには使えないから触れないみたいな事はよくあるので、あえて触れてみたりする事で学習の幅が広がっていいなと思いました。せっかく攻撃し放題(?)なので、色々試してみるといいと思います。 例えば、Beepマシーンは様々なサービスがたっているかつ、色んな攻撃経路があったのでeasyの中でも非常に勉強になりました。

HackTheBoxやるにあたって参考にしたもの

最初にこれ読みました。HTBだとIPアドレスだけ表示されていて、あとはお好きに。。。というスタイルなので少なくともアプローチの仕方が分からないと手がでません。この本ではペネトレーションテストにおけるアプローチの仕方(Recon, Exploitation, Post Exploitation)が網羅されているのと、それぞれのフェーズでのツールが紹介されてるので最初に読む一冊としておすすめです。

www.amazon.co.jp

IppSecさんはHTBのRetired Machineのwriteupをあげてくださっています。Metasploitだけでなくマニュアルにexploitationする手法や色んな攻撃経路にチャレンジしてくれるので、非常に参考になります。

www.youtube.com

日本語記事が読みたい時は、Qiitaのタグを頼りにwriteupを探りました

qiita.com

Torからのアクセスをnginxでブロックする

はじめに

Torは、プライバシー保護という観点で自身は決して悪いものだとは思っていません。サービスを運用する上で個人の特定ができない事で起こりうる問題の予防のためにこのような対応の選択をせざるを得ないというだけで、Torそのものを否定したいわけではない事だけ始めに書いておきます。

本題

Torは、ざっくり言うと複数のサーバーを経由することで秘匿性を実現しています。一番最後に経由したノードからインターネットに接続し、接続先へ通信します。一番最後に経由したノードはTor出口ノードと呼ばれています。接続先のサーバーのログに残るIPアドレスはこの出口ノードのIPアドレスであるので、現在出口ノードになっているサーバーのIPアドレスさえ分かればそれらをnginxでブロックするのは容易です。

Torの出口ノードはtorprojectで管理されているので、このエンドポイントをcurlで叩いてIPを取得、nginxのdenyディレクティブにIPを追加するようなファイルを動的に生成するスクリプトをcronで定期的に実行させれば、出口ノードに変動があったとしてもなるべくリアルタイムに更新させることができます。

以下で、動的にIPを取得し、block_tor_ip.confに追加、nginxをリロードさせます。

#!/bin/bash

result=`curl -s https://check.torproject.org/torbulkexitlist?ip=1.1.1.1`

if [ $? -ne 0 ]; then
    echo "Cannot get address."
    exit 1
fi

echo "${result}" | sed '/^#/ !s/^/deny  /g; s/$/;/g' >> block_tor_ip.conf

nginx -t
nginx -s reload

ファイルは以下のように生成され、/etc/nginx/conf.d/*.confはデフォルトでincludeされるので、リロードされれば適用されます。 実際は、数百行ありますが、長いので割愛してます。

$ head conf.d/block_tor_ip.conf
deny  54.37.16.241;
deny  46.182.106.190;
deny  109.70.100.32;
deny  51.15.43.205;
deny  51.75.64.23;
deny  185.205.210.245;
deny  94.32.66.48;
deny  82.221.128.191;
deny  109.70.100.35;

検証

AWSでインスタンスを立てて、試しに上記のスクリプトを動作させTorからアクセスしてみます。 nginxをstartさせると、デフォルト画面がレンダリングされます。

f:id:keisuke_t:20200817185940p:plain
nginx_default

スクリプトを発火させ、denyのIPが追加されているnginxを起動します。

Tor起動し、アクセスすると

f:id:keisuke_t:20200817190055p:plain
tor

403を返し、ブロックされました。

f:id:keisuke_t:20200817190250p:plain
block_nginx

最後に

本番環境で運用するならば、locationディレクティブを使って特定のURLの時だけ(例えば、決済画面とかユーザーの問合せ画面とか)denyを適用させるとかもありかなと思いました。