JWT(JSON Web Token)とは、2者のあいだでやり取りする「クレーム」を表す、コンパクトで URL に載せても安全な形式です(RFC 7519)。クレームとは、ユーザーなどの「主体」について述べた1つの情報のことで、名前と値の組で表します。JWT は、クレームをまとめた JSON を、署名(改ざんを見つける印)や暗号化をして、.(ピリオド)でつないだ文字列にしたものです。
大事な点が1つあります。署名がついた JWT は、改ざんを見つけられますが、中身は暗号化されていません。トークンを手に入れた人は、鍵がなくても、すべてのクレームを読めます。そして、中身を読める(デコードできる)ことと、署名が正しいと確かめる(検証する)ことは別のことです。この記事の中心は、この2つの違いです。
この記事は、ログインの仕組みで JWT という名前を聞いて、初めて調べる人に向けて、JWT の3つの部分、中身が読めること、検証しないと起きること、有効期限、セッションとの違い、保存場所について書いたものです。書いてあることは、RFC(IETF が出す標準の文書)、OWASP(ウェブの安全に取り組む団体)の Cheat Sheet、MDN の文書を、2026年10月10日に開いて確かめたものです。コードは、Node v21.7.1(macOS 14.4.1)で流したものを載せています。JWT のライブラリは使っておらず、ブラウザーでの保存の実験もしていません。
OWASP の Cheat Sheet は頻繁に書き換わります。この記事の「2026年10月10日時点」の書き方は、その日に読んだ内容です。ログインや保存場所を決めるときは、最新の文書をもう一度開いてください。
- JWT とは何か。ヘッダー・ペイロード・署名の3つの部分と、
expなどのクレームの名前 - 署名つき JWT の中身は暗号化されておらず、誰でも読めること(Node のコードで確認)
- 「読む」と「検証する」の違い。署名を確かめないと起きること(
algのnoneとアルゴリズムの取り違え) - セッション(サーバーが状態を持つ)と JWT(トークンが状態を持つ)の違い
- 保存場所について、OWASP・MDN・RFC 10017 が 2026年10月10日時点で書いていること(言い切れない理由つき)
JWT とは?3つの部分と署名
JWT の標準は、2015年5月に出た RFC 7519 です。標準化過程の文書(Proposed Standard)で、その後に RFC 7797 と RFC 8725 で更新されました。使われる場面の例として、OWASP は、OpenID Connect の ID トークン(ログインした人を表すトークン)は JWT であり、OAuth 2 のアクセストークン(保護されたリソースに入るための許可証)も JWT のことがある、と書いています。
署名つき JWT は「ヘッダー・ペイロード・署名」の3つ
署名つきの JWT は、3つの部分を . でつないだ形です。OWASP は、次の形で書いています。
{base64url(json(header))}.{base64url(json(claims))}.{base64url(signature)}
各部分は base64url で符号化されています。base64url とは、URL やファイル名に使える文字だけで書く base64 のことです(RFC 4648 の §5)。JWS(署名つきの形式)では、末尾の = を省き、改行や空白を入れません。ふつうの base64 と違うのは、値 62 と 63 の文字(- と _)で、RFC 4648 は、これを単に「base64」と呼んではいけないと書いています。
| 部分 | 入るもの |
|---|---|
| ヘッダー | トークンの種類や、守るのに使った暗号のアルゴリズムなど |
| ペイロード(クレーム) | クレームの一覧(ふつうは主体についてのもの) |
| 署名 | 公開鍵による電子署名(秘密鍵と公開鍵の組を使う)か、共有の秘密を使う MAC(改ざんを見つける印)。署名はヘッダーとクレームの両方を守る |
「ペイロード」は JWS 側の呼び方で、RFC 7519 は、JWT のクレームの集まりを "JWT Claims Set" と呼びます。この記事は、JWS の呼び方に合わせて「ペイロード」と書きます。
ヘッダーの alg は、署名に使ったアルゴリズムを示す項目です。必ず入れる項目で、実装は必ず理解して処理することになっています(RFC 7515)。typ は、入れるなら値は JWT(大文字)が推奨で、使うかどうかは任意です(RFC 7519)。
RFC 7519 の例を見る
RFC 7519 の §3.1 に載っている例です。ヘッダーは次のとおりです。
{"typ":"JWT","alg":"HS256"}
ペイロードのクレームは次のとおりです。
{"iss":"joe","exp":1300819380,"http://example.com/is_root":true}
署名をつけてできあがった JWT は、次のとおりです。表示のために3行に分けましたが、本来は改行のない1本の文字列です。
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
これは RFC に載っている例で、本物のトークンではありません。この記事の例は、すべて RFC に載っている値だけを使います。RFC の例の JSON には、改行(CR LF)と空白が入っています。
クレームの名前
RFC 7519 は、よく使うクレームの名前を登録しています。
| 名前 | 意味 |
|---|---|
iss | 発行者(issuer) |
sub | 主体(subject) |
aud | 受け手(audience) |
exp | 有効期限(expiration time) |
nbf | この時刻より前は受け付けない(not before) |
iat | 発行した時刻(issued at) |
jti | JWT の ID |
RFC 7519 によると、このどれも必須ではありません。どのクレームを使い、いつ必須にするかは、JWT を使うアプリが決めます。名前が短いのは、JWT をコンパクトにするためです。登録された名前のほかに、衝突しにくい名前(Public)や、当事者どうしで決めた名前(Private)も使えます。Private な名前は衝突することがあるので、注意して使います。
暗号化した JWT(JWE)は5つの部分
JWT は「いつも3つの部分」ではありません。クレームを暗号化する形式の JWE では、ヘッダー・暗号化した鍵・初期化ベクトル・暗号文・認証タグの、5つの部分になります(RFC 7516)。JWT は、クレームを JWS の中身にするか、JWE の平文にするかで、署名または暗号化ができます。部分の数は、JWS か JWE かで決まります。この記事で扱うのは、おもに署名つきの JWS の形です。
JWT の中身は暗号化されていない?誰でも読める?
署名つきの JWT(JWS)の中身は、暗号化されていません。OWASP は、署名つきの JWT は改ざんの防止と、発行元が本物であること(真正性)は守るが、秘密は守らない、と書いています。ペイロードは base64url で符号化しただけなので、トークンを手に入れた人は、すべてのクレームを読めます。
base64 などの符号化は、見た目で読みにくくするだけです。RFC 4648 のセキュリティの節は、符号化は、パスワードのような見つけやすい情報を見た目では隠すが、計算で秘密を守る力はない、と書いています。
鍵なしで読んでみる
RFC 7519 の例の JWT を、鍵を使わずに読むコードです。Node の標準の Buffer だけを使います。
// RFC 7519 3.1 の例の JWT(RFC に載っている例の値。本物のトークンではない)
const jwt =
"eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" +
".eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" +
".dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
const [h, p, s] = jwt.split(".");
console.log("部分の数:", jwt.split(".").length);
console.log("ヘッダー:", JSON.stringify(Buffer.from(h, "base64url").toString("utf8")));
console.log("ペイロード:", JSON.stringify(Buffer.from(p, "base64url").toString("utf8")));
console.log("ペイロード(JSON.parse):", JSON.parse(Buffer.from(p, "base64url").toString("utf8")));
console.log("署名(バイト数):", Buffer.from(s, "base64url").length);
decode.mjs というファイルに保存して流すと、Node v21.7.1 では次のように出ました。
$ node decode.mjs
部分の数: 3
ヘッダー: "{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}"
ペイロード: "{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n \"http://example.com/is_root\":true}"
ペイロード(JSON.parse): { iss: 'joe', exp: 1300819380, 'http://example.com/is_root': true }
署名(バイト数): 32
鍵を一切使わずに、ヘッダーとクレームが読めました。\r\n は、RFC の例の JSON に CR LF と空白が入っているためです。Python の標準の base64 と json でも、同じ内容が読めました。ただし、JWT は末尾の = を省くので、Python の base64.urlsafe_b64decode にそのまま渡すと Incorrect padding で止まります。長さが 4 の倍数になるまで = を足すと読めました。
HTTPS なら中身は見られない?
HTTPS(TLS)は、通信の途中でトークンが読まれるのを防ぎます。しかし OWASP は、クレームはほかの場所で見えてしまう、と書いています。アプリのログ、ブラウザーの保存先、Referer ヘッダー、TLS を終わらせる中継(途中で暗号を解く機器)です。
秘密にしたい情報は入れない
RFC 7519 は、JWT に個人情報のような機微な情報が入るなら、意図しない相手に漏れないための手を打たなければならない、と書いています。手は2つのどちらかで、どちらも相手の認証が付きます。暗号化した JWT(JWE)を使って受け手を認証するか、相手を認証できる暗号化した通信(TLS など)だけで送ることです。そして、いちばん簡単なのは、そもそも入れないことだ、とも書いています。
OWASP も同じです。機微なデータはサーバー側に置き、中身のない参照用のトークンを使うほうがよく、JWE は、クレームをトークンと一緒に運ぶ必要があるときだけにする、という書き方です。クレームを秘密にする必要があるときは JWE を使うと書いていますが、JWE の詳しいやり方は、この記事の範囲の外です。
読めることと「検証」は別:署名を確かめないと何が起きる?
JWT の中身を読めても、その中身を信じてよいわけではありません。RFC 7519 は、JWT の中身は、暗号で守られ、判断に必要な文脈に結びついていない限り、信用の判断に使えない、と書いています。署名や暗号化の鍵は、ふつう、発行者が持っていると確かめられる必要があります。また、検証の手順のどれかが失敗したら、その JWT は拒否しなければなりません。
alg を書き換えられる攻撃(RFC 8725)
RFC 8725 は、JWT を安全に使うための現行のベストプラクティス(BCP 225、2020年2月)です。署名つきの JWT は、ヘッダーの alg でアルゴリズムを示します。RFC 8725 は、これがライブラリやアプリの設計の欠陥と合わさって、複数の攻撃につながった、と書いていて、その例として次の2つを挙げています。
algをnoneに変える。 攻撃者がalgをnoneに変えると、一部のライブラリはその値を信じて、署名を調べずに JWT を「検証済み」にした- アルゴリズムを取り違えさせる。
RS256(RSA)をHS256(HMAC)に書き換えると、一部のライブラリは、RSA の公開鍵を HMAC の共有の秘密として使って、署名を検証しようとした
none の JWT(Unsecured JWT)は、alg が none で、署名の部分が空文字の JWT です。RFC 7519 の例は、次のように最後が . で終わります。
eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.
対策は「受け付けるアルゴリズムを決めておく」
RFC 8725 の §3.1 は、ライブラリは、受け付けるアルゴリズムの組を呼び出す側が指定できなければならず、それ以外のアルゴリズムを使ってはならない、と書いています。さらに、alg が、暗号の処理に使うアルゴリズムと同じかを確かめること、1つの鍵は1つのアルゴリズムだけに使うことも求めています。OWASP も、アルゴリズムの取り違えの対策の一つとして、受け付けるアルゴリズムを固定し、公開鍵署名と MAC を混ぜないことを挙げています。この攻撃は、OWASP では "key confusion" や "algorithm confusion" とも呼ばれます。
MDN も、JWT の形式は署名のないトークンも許していて、過去に一部の検証ライブラリがそれを受け入れた、と書いています。対策として、リソースサーバーは、使う JWT ライブラリが必ず署名を調べるようにする、という書き方です。
そのほかに、RFC 8725 が書いていることです。
- HS256 などの MAC の鍵に、人が覚えられるパスワードをそのまま使ってはならない(総当たりや辞書攻撃で破られる)
issがあれば、検証に使う鍵がその発行者のものか確かめる。同じ発行者が複数の受け手向けに JWT を出すなら、audを必ず入れる。受け手は自分あてか確かめ、audが無いか、自分あてでなければ拒否する(RFC 8725。RFC 7519 も、audがあるのに自分の値が見つからない JWT は拒否しなければならない、と書いています)- 受け取ったヘッダーをそのまま信じない。
kidは SQL や LDAP のインジェクションにならないよう検証する。jkuやx5uの URL をそのまま取りに行くと、SSRF(サーバーに任意の URL へ通信させる攻撃)になりうる
none は「禁止」?RFC ごとの言い方
「RFC は none を禁止している」とは書けません。文書によって言い方が分かれているからです。
| 出どころ | 書いてあること |
|---|---|
| RFC 7519 §8 | none は、準拠する実装が実装しなければならない(MUST be implemented) |
| RFC 7518 §3.6 | 署名のない JWS(Unsecured JWS)を、既定で受け付けてはならない(MUST NOT)。受け付けるのは、アプリがそのオブジェクトについて「守られていなくてよい」と指定したときだけ |
| RFC 8725 §3.2 | none は、JWT がほかの手段で暗号的に守られているときだけ使う。同じ節に、TLS で端から端まで守られているなら none でも問題ないことがある、とも書いてある。ライブラリは、呼び出す側が明示しない限り、none の JWT を作るべきでなく、受け付けるべきでもない(どちらも SHOULD NOT で、RFC 7518 の MUST NOT より弱い言い方) |
| OWASP | JWT のパーサーが "alg":"none" を受け付けないようにする。最近の実装では既定で無効になっているはず(should) |
つまり、仕様が書いているのは、「既定では受け付けない」(RFC 7518。MUST NOT)、「呼び出す側が明示しない限り受け付けるべきでない」(RFC 8725。SHOULD NOT)です。OWASP の「最近の実装では既定で無効のはず」は、「どのライブラリでも大丈夫」という意味ではありません。受け付けるアルゴリズムを、自分で決めておきます。
説明用のコードで試す
「読むだけ」のコードと、署名を検証するコードの違いを、Node の標準の node:crypto だけで試します。鍵は、RFC 7515 の付録 A.1 に載っている例の鍵です(公開されている例で、本物の秘密鍵ではありません)。トークンは、先ほどの RFC 7519 の例です。
import { createHmac, timingSafeEqual } from "node:crypto";
// RFC 7515 付録 A.1 の例の鍵(RFC に載っている公開の例。本物の秘密鍵ではない)
const key = Buffer.from("AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow", "base64url");
const jwt = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
function verifyHS256(token) {
const [h, p, s] = token.split(".");
const expected = createHmac("sha256", key).update(`${h}.${p}`).digest();
const actual = Buffer.from(s, "base64url");
return actual.length === expected.length && timingSafeEqual(actual, expected);
}
console.log("1 元のまま:", verifyHS256(jwt));
// ペイロードを書き換えて、署名は元のまま
const [h, , s] = jwt.split(".");
const forged = Buffer.from(JSON.stringify({ iss: "joe", exp: 1300819380, "http://example.com/is_root": true, role: "admin" })).toString("base64url");
console.log("2 ペイロードを書き換え:", verifyHS256(`${h}.${forged}.${s}`));
// exp(NumericDate = 1970-01-01T00:00:00Z からの秒)
const exp = 1300819380;
console.log("3 exp を日時に:", new Date(exp * 1000).toISOString());
console.log("3 いまは exp より前か:", Date.now() / 1000 < exp, "(いま:", new Date().toISOString() + ")");
// RFC 7519 6.1 の Unsecured JWT(alg: none、署名は空)
const none = "eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.";
const parts = none.split(".");
console.log("4 alg none の部分の数:", parts.length, "署名の長さ:", parts[2].length);
console.log("4 ヘッダー:", Buffer.from(parts[0], "base64url").toString());
// 「読むだけ」のコードは、署名が無くても中身をそのまま返してしまう
const decodeOnly = (t) => JSON.parse(Buffer.from(t.split(".")[1], "base64url").toString());
console.log("4 読むだけのコードの結果:", decodeOnly(none));
// 受け付けるアルゴリズムを先に決めて照合する形
const allowed = ["HS256"];
const alg = JSON.parse(Buffer.from(parts[0], "base64url").toString()).alg;
console.log("4 許可リスト", JSON.stringify(allowed), "に", JSON.stringify(alg), "が入っているか:", allowed.includes(alg));
verify.mjs というファイルに保存して流すと、Node v21.7.1 では次のように出ました(「いま」の時刻は、流した時刻です)。
$ node verify.mjs
1 元のまま: true
2 ペイロードを書き換え: false
3 exp を日時に: 2011-03-22T18:43:00.000Z
3 いまは exp より前か: false (いま: 2026-10-10T09:04:03.371Z)
4 alg none の部分の数: 3 署名の長さ: 0
4 ヘッダー: {"alg":"none"}
4 読むだけのコードの結果: { iss: 'joe', exp: 1300819380, 'http://example.com/is_root': true }
4 許可リスト ["HS256"] に "none" が入っているか: false
- 1: 例の鍵で、RFC 7519 の例の JWT の HS256 署名が合いました
- 2: ペイロードに
role: "admin"を足して、署名は元のままにすると、検証はfalseになりました。クレームを書き換えると、署名で見つかります - 4:
noneの JWT は、部分が3つで、署名の長さが 0 でした。「読むだけ」の関数は、署名がなくても中身を返しました。これが、デコードは検証ではない、ということです。受け付けるアルゴリズムを先に["HS256"]と決めておけば、noneは弾けます
このコードは、仕組みを見せるための説明用です。本番の検証には使わないでください。 鍵の管理、exp・nbf・iss・aud の確認、時計のずれの猶予などは入っていません。本番では、ライブラリの検証機能を使い、受け付けるアルゴリズムを固定します。MDN も、セッション管理の細部を自分で実装せず、定評のあるフレームワークかライブラリを使うことを勧めています。この記事は、特定のライブラリを勧めません。ライブラリを選ぶときは、アルゴリズムを呼び出す側が指定できるか(RFC 8725 の §3.1)を確かめてください。
有効期限(exp)はどう読む?
exp は、その時刻以降は JWT を受け付けてはならない、という期限です。RFC 7519 によると、処理するときは、いまの日時が exp より前でなければなりません。時計のずれに備えて、少しの猶予(ふつうは数分まで)を置いてもよいとされています。そして、exp を使うかどうかは任意(OPTIONAL)です。入っていない JWT もあります。
exp の値は NumericDate で、1970-01-01T00:00:00Z(UTC)からの秒の数です(うるう秒は数えません)。ミリ秒ではありません。JavaScript の Date.now() はミリ秒なので、比べるときは 1000 で割ります。前の節のコードの3は、そのとおりに書いています。RFC 7519 の例の exp は 1300819380 で、日時にすると 2011-03-22T18:43:00Z です。2026年10月10日の時点では、期限切れです。
ほかの時刻のクレームは、nbf(この時刻より前は受け付けない)と、iat(発行した時刻。JWT の古さを測るのに使える)です。
期限の短さについては、MDN は、クライアントがリソースサーバーに入るために使うトークン(アクセストークン)の有効期間を短くすると、盗まれても長く使えない、と書いています。長く使えるリフレッシュトークン(新しいアクセストークンをもらうためのトークン)で新しいアクセストークンをもらう形が、いちばん一般的な解決だとされています。リフレッシュの窓口が、セッションを無効にするかを決める1か所になります。OWASP も、トークンの再利用は、有効期限を短くすることで和らげられる、と書いています。なお、MDN 日本語版は、リフレッシュトークンを「更新トークン」と訳しています。
「何分にすればよいか」という数字は、書けません。 確かめた RFC 7519・RFC 8725・OWASP の JWT Cheat Sheet・MDN のどれにも、JWT 一般についての有効期限のおすすめの長さは書かれていませんでした。
セッションと JWT の違いは?
HTTP は状態を持たないプロトコルなので、ログインのような状態を続けるには、セッションの仕組みが要ります。MDN は、ログインを保つ方法を2つ挙げています。秘密のセッション ID を入れた Cookie を使うか、JWT のように暗号で署名したオブジェクトを使うか、です。MDN は、この2つを次のように呼び分けています。
| 集中型のセッション管理 | 分散型のセッション管理 | |
|---|---|---|
| セッションの状態 | サーバーに保存する | クライアントが、サーバーに署名されたオブジェクトとして持つ(ふつうは JWT) |
| クライアントが持つもの | サーバーが作ったセッション ID | 署名つきのトークン |
| リクエストのとき | ID を送る。サーバーが ID で状態を引く | トークンを出す。サーバーが署名を検証して、中身で判断する |
| 無効にするとき(ログアウトなど) | サーバーの状態を消せば無効にできる | トークンだけで完結しているので、一度出したトークンを取り消すのが難しい |
分散型は、クライアントが複数のサーバーに頼む分散したアプリで好まれます。署名を検証できれば、各サーバーは、トークンの発行元に問い合わせなくてよいからです。
ただし MDN は、分散型は集中型より複雑になりやすく、弱点も持ち込む、と書いています。アプリの構成上いらないなら、ふつうは集中型のほうがよい、というのが MDN の勧めです。
OWASP も近い立場です。JWT は「状態を持たない(stateless)セッション」によく勧められるが、その使い方は好まれていない(frowned upon)、と書いています。JWT をユーザーのセッションに使うなら、セッションの無効化を管理する仕組みが要ります。取り消したトークンの拒否リスト(deny list)を作る方法がありますが、入れると、セッションは完全には stateless でなくなり、stateless の利点が消えるかもしれません。OWASP は、ふつうのセッションの仕組みを検討するよう書いています。拒否リストのキーは、jti と iss で作れます。トークンそのものや、そのハッシュをキーにするのは、安全ではないとされています。
2つの文書は、言い方の強さが違います。OWASP は「好まれない」と書き、MDN は「分散型はある種の構成で役に立つ。ただし、いらないなら集中型がよい」と書いています。どちらも、必要がないならふつうのセッションを勧める、というところは共通です。
また、OAuth の文書である RFC 10017(2026年8月発行)にも、近い話があります。単純なアプリが、OAuth を使ってセッション管理の代わりにすることで、必要以上に複雑になっていることが多い、という指摘です。フロントとサーバーが同じドメインのような単純なアプリでは、アクセストークンの代わりに、サーバー側に状態を持つ Cookie のセッションで足りることが多い、と書いています。これは OAuth を使うかどうかの話で、「JWT を使うな」という意味ではありません。
「Cookie がセッション、JWT がトークン」という分け方にはなりません。 JWT も Cookie に入れて送れます(MDN は、多くのサイトがトークンを HttpOnly の Cookie に入れることを選ぶ、と書いています)。違いは、状態をサーバーに持つか、署名したトークンに持たせるかです。
ログインのとき、JWT をどう送る?(Authorization ヘッダー)
HTTP の Authorization リクエストヘッダーは、サーバーに対して利用者を認証する資格情報を送るためのものです(MDN)。OAuth 2.0 のアクセストークンは、Authorization: Bearer <トークン> の形で送ります。RFC 6750 の例は次のとおりです。
GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM
RFC 6750 は、クライアントはこの形で送るべきだ(SHOULD)と書いています。ただし、これは OAuth のアクセストークン一般についての文書で、トークンが JWT とは限りません。「JWT は Bearer で送ると RFC で決まっている」わけではありません。
Bearer トークンとは、持っている人なら誰でも使えるトークンのことです。鍵を持っていることの証明は要りません。そのため RFC 6750 は、意図しない相手に漏らさないことを求めています。必ず TLS(https)で送ること、ページの URL(クエリ文字列)でトークンを渡さないこと(SHOULD NOT)も書いています。
ブラウザーから別のオリジンの API を呼ぶときは、CORS の設定も関わります。資格情報(Cookie など)を付けるには credentials: "include" などが要ること、Authorization ヘッダーを付けるとプリフライト(本番のリクエストの前に、ブラウザーが先に確かめるリクエスト)が起きることは、CORS エラーとは?「has been blocked by CORS policy」の原因と直し方【メッセージ別】で説明しています。fetch と axios でリクエストを送る書き方は、fetch と axios違いのわかりやすい話【JS初心者向け完全入門】で説明しています。
トークンの送り方は、保存先で変わります。MDN によると、localStorage に入れた値は、クライアントが自分で読み出して、リクエストに入れる必要があります。Cookie に入れた値は、リクエストのときに自動で送られます。この違いが、次の節の話につながります。
JWT はどこに保存する?一次資料が書いていること(2026年10月10日時点)
先に書いておくと、この記事は「ここに保存すればよい」と言い切りません。保存場所について書いている一次資料は、確かめた範囲で、おもに次の4つでした(Cookie に入れる話は、RFC 6750 にもあります)。出どころによって、対象も、言い方の強さも違います。そして、どれも「この保存先なら完全に安全」とは書いていません。
| 出どころ | 対象 | 書いていること |
|---|---|---|
| OWASP の Session Management Cheat Sheet | 認証トークン・セッション ID・JWT・リフレッシュトークンなど | localStorage にも sessionStorage にも入れない(Do not store)。HttpOnly; Secure; SameSite=Strict の Cookie(推奨)か BFF の形を使う |
| OWASP の HTML5 Security Cheat Sheet | セッション識別子 | local storage に入れない。Cookie なら httpOnly でこの危険を和らげられる。JWT の名指しはない |
| MDN の Session management | セッション ID とトークン | セッション ID は Cookie を勧める。トークンは、多くのサイトが HttpOnly の Cookie に入れることを選ぶ(記述であり、禁止ではない) |
| RFC 10017(OAuth のブラウザー向けの BCP) | OAuth のアクセストークン・リフレッシュトークン | 保存先ごとの性質を比べている。どの保存先も、悪いコードがアプリの中で動けば、トークンの持ち出しを完全には防げない。業務・機微・個人情報を扱うアプリでは、ブラウザーの JavaScript だけで OAuth のクライアントになり、トークンもすべてブラウザーが持つ構成(§6.3)は勧めず、BFF(トークンはサーバー側の BFF が Cookie のセッションで扱い、ブラウザーの JavaScript には渡さない構成)を強く勧める |
OWASP:localStorage・sessionStorage に入れない
OWASP の Session Management Cheat Sheet は、認証トークン・セッション ID・JWT・リフレッシュトークンなどの資格情報を、localStorage にも sessionStorage にも入れない、と書いています。理由は、その origin(ページのプロトコル・ホスト・ポートの組)で動くどの JavaScript からも読めるので、XSS が1つあれば、すべてのトークンが漏れるからです。XSS(クロスサイトスクリプティング)とは、攻撃者が、目的のサイトに悪いコードを、そのサイトの一部であるかのように実行させる攻撃です。かわりに、HttpOnly; Secure; SameSite=Strict の Cookie(推奨)か、BFF(Backend-for-Frontend)の構成を使う、と書かれています。
同じページには、localStorage と sessionStorage の中身は、Cookie と違って、ブラウザーが自動でリクエストに付けないこと、標準では localStorage のディスク上での暗号化は求められていないことも書かれています。MDN によると、localStorage の値には期限がなく、sessionStorage の値は、ページのセッションが終わる(ページを閉じる)と消えます。
この警告は、2026年5月7日の更新で入った新しい文です。OWASP の Cheat Sheet は頻繁に書き換わるので、読むときは最新の文面も見てください。警告の中の「OAuth 2.0 for Browser-Based Apps」のリンク先は、下書きの段階の URL のままですが、その文書は 2026年8月に RFC 10017 になりました。
OWASP の HTML5 Security Cheat Sheet も、セッション識別子を local storage に入れない(いつも JavaScript から読めるため)と書いています。XSS が1つあれば、これらの入れ物の中身はすべて盗まれうる、という理由も同じです。こちらは JWT を名指ししていません。
MDN:Cookie を勧める。ただし完全ではない
MDN は、セッション識別子の主な保存先は、local storage と Cookie の2つだとして、Cookie を勧めています。HttpOnly 属性をつけると JavaScript から Cookie を読めないので、XSS が成功しても、セッション ID そのものは盗めないからです。トークンについても、注意点はほぼ同じで、盗まれればセッションを乗っ取られるので、多くのサイトはトークンを HttpOnly の Cookie に入れることを選ぶ、と書いています。
ただし、HttpOnly も完全な守りではありません。 MDN は、悪いコードが、利用者のブラウザーから、セッション ID が付いたリクエストを出せるので、その利用者の権限で操作できてしまう、と書いています。OWASP も、HttpOnly の Cookie が守るのは Cookie の秘密(機密性)だけで、攻撃者は、XSS の攻撃の外で、それをオフラインで使えない、という趣旨を書いています。
MDN は、Cookie の制約も挙げています。トークンはセッション ID よりずっと大きく、Cookie は最大 4KB です。また、別の登録可能ドメイン(たとえば example.com と example.org)のサービスを使うアプリでは、ブラウザーは、Cookie を設定したサイトと別のサイトへ Cookie を送らないので、Cookie にトークンを入れられません。
RFC 10017:OAuth のトークンの保存先の性質
RFC 10017 は、2026年8月に出た、ブラウザーで動く OAuth のクライアント向けの文書(BCP 212)です。JWT 一般の文書ではなく、対象は OAuth のアクセストークンとリフレッシュトークンです。 §8 で、保存先ごとの性質を比べています。
localStorageは、ページを読み直しても残り、origin 全体から読めます。同じ origin で悪い JavaScript が動けば、中身を読めるので、不正なアクセスを防げませんsessionStorageは、タブの寿命に結びつき、同じ origin の別のタブとは共有されないので、さらす範囲が少し減ります- メモリーにだけ持つと、さらすのは今の実行の中だけになりますが、ページを読み直すとトークンは消えます
- JavaScript から Cookie にトークンを入れる使い方(あとで
Authorizationヘッダーに載せるため)は、NOT RECOMMENDED とされています。同じドメインへのリクエストに、Cookie が勝手に付く副作用があるためです。サーバーが付ける、JavaScript から読めない Cookie(BFF の構成)とは別の話です
そのうえで RFC 10017 は、これらの保存先の主な違いは、データをさらす範囲であり、攻撃者がアプリの実行環境で悪いコードを動かせるなら、どの保存先でも、トークンの持ち出しを完全には防げない、と書いています。
また RFC 10017 は、業務のアプリ・機微なアプリ・個人情報を扱うアプリについて、2つの構成に触れています(§6.1.4 と §6.3.4)。BFF(トークンはサーバー側の BFF が Cookie のセッションで扱い、ブラウザーの JavaScript には渡さない構成)は、強く勧める(strongly recommended)としています。ブラウザーの JavaScript だけで OAuth のクライアントになり、トークンもすべてブラウザーが持つ構成(§6.3)は、勧めない(not recommended)としています。つまり、こうした種類のアプリでは、どの保存先を選ぶかの前に、BFF が強く勧められています。両者の間の形(アクセストークンだけをブラウザーの JavaScript に渡す構成、§6.2)は「勧めない」とは書かれておらず、まず BFF にできないかを検討する、という扱いです。この勧めは OAuth のクライアントについての文書の範囲で、JWT 一般の決まりではありません。なお、RFC 10017 に、localStorage を一律に禁じる文は、見つかりませんでした。
Cookie に入れるなら、CSRF への対策も要る
Cookie に入れる場合は、CSRF への対策が必要です。CSRF(クロスサイトリクエストフォージェリ)とは、攻撃者が、悪いサイトから目的のサイトへ、利用者のブラウザーに HTTP リクエストを出させる攻撃です。リクエストには、利用者の資格情報(Cookie など)が付くので、サーバーは、本人の操作と思って実行してしまいます。
MDN は、Cookie でセッション識別子を送るサイトは、CSRF への対策を実装しなければならない、と書いています。SameSite 属性は、完全な対策の一部でしかない、とも書いています。RFC 6750 も、Bearer トークンを、平文で送られうる Cookie(Cookie の既定)に入れてはならず、Cookie に入れるなら CSRF への対策が必須(MUST)と書いています。Cookie の Secure 属性は HTTPS のときだけ送る印で、HttpOnly 属性は JavaScript から読めなくする印です。MDN は、ユーザーのセッションを保つ Cookie には HttpOnly を付けるべきだと書いています。
どの保存先にも、弱点がある
ここまでをまとめると、次のようになります。
| 保存先 | 書かれている特徴 | 書かれている弱点・制約 |
|---|---|---|
localStorage・sessionStorage | ブラウザーが自動ではリクエストに付けない。クライアントが読み出して送る | その origin の JavaScript から読めるので、XSS で漏れる。OWASP は入れないと書く |
HttpOnly の Cookie | JavaScript から読めないので、XSS でも値そのものは盗めない | 悪いコードが、利用者の権限でリクエストを出せる。CSRF への対策が要る。大きさと、別ドメインへ送れない制約がある |
| メモリー | さらすのは今の実行の中だけ | ページを読み直すと消える |
OWASP と MDN は、どちらも Cookie(HttpOnly)の側を挙げています。しかし、その Cookie も完全ではありません。アプリの構成(フロントとサーバーが同じドメインか、別のドメインのサービスを使うか)によって、選べるものも変わります。保存先を決めるときは、上の一次資料の原文を読んで、自分のアプリの構成と照らしてください。
よくある質問
JWT の読み方は?
RFC 7519 は、JWT の読み方として、英単語の "jot" と同じ読み方を勧めています。カタカナで書くなら「ジョット」のような音です(カタカナは、この記事での書き方で、RFC の表記ではありません)。
JWT の中身は暗号化されている?
署名つきの JWT(JWS)は、暗号化されていません。ペイロードは base64url で符号化しただけなので、トークンを手に入れた人は誰でも読めます。クレームを秘密にする必要があるときは、暗号化する JWE を使います。ただし OWASP も RFC 7519 も、いちばん簡単なのは、秘密にしたい情報をそもそも JWT に入れないことだ、と書いています。
JWT でログアウトするには?すぐに無効にできる?
セッションを無効にするのは、集中型(サーバーに状態を持つ)なら、サーバーの状態を消せばできます。分散型(JWT)は、トークンがそれだけで完結しているので、一度出したトークンを取り消すのが難しい、と MDN は書いています。
対策として、MDN は、アクセストークンの有効期間を短くして、リフレッシュの窓口を、セッションを無効にするかを決める1か所にする方法を挙げています。OWASP は、取り消したトークンの拒否リストを作る方法を挙げています。ただし、拒否リストを入れると、完全には stateless ではなくなります。
有効期限(exp)はどのくらいにする?
JWT 一般について、「この長さがよい」という数字は、確かめた RFC 7519・RFC 8725・OWASP の JWT Cheat Sheet・MDN のどれにも、見つかりませんでした。書かれているのは、アクセストークンの有効期間は短くする(MDN)、トークンの再利用は有効期限を短くすると和らげられる(OWASP)、という方向までです。exp を使うかどうか自体も任意です(RFC 7519)。期限の長さは、守りたいものとログインを保つ形に合わせて、自分で決めることになります。
SPA のログインに JWT を使うのは良い方法?
SPA は、ページを切り替えずに、画面を書き換えて動くタイプのウェブアプリのことです。「良い」「悪い」とは、言い切れません。一次資料が書いていることを並べると、次のとおりです。
- MDN:分散型(JWT)は、集中型より複雑になりやすく、弱点も持ち込む。アプリの構成上いらないなら、ふつうは集中型がよい
- OWASP:JWT を stateless なセッションに使うことは好まれていない。使うなら、無効化の仕組みが要り、その場合はふつうのセッションの仕組みも検討する
- RFC 10017(OAuth の文書):フロントと API が同じドメインの単純なアプリでは、アクセストークンの代わりに、サーバー側に状態を持つ Cookie のセッションで足りることが多い
- RFC 10017(OAuth の文書。範囲は OAuth のクライアントについて):業務のアプリ・機微なアプリ・個人情報を扱うアプリでは、ブラウザーの JavaScript だけで OAuth のクライアントになり、トークンもすべてブラウザーが持つ構成(§6.3)は勧めず、BFF(トークンはサーバー側の BFF が Cookie のセッションで扱い、ブラウザーの JavaScript には渡さない構成)を強く勧める
どれも「必要がないなら、ふつうのセッションを」という向きですが、分散した構成で JWT が役に立つ場合もある、とも MDN は書いています。
まとめ
- JWT は、クレームを JSON でまとめ、署名や暗号化をして、文字列にした形式。署名つき(JWS)は「ヘッダー・ペイロード・署名」の3つの部分を
.でつなぎ、暗号化したもの(JWE)は5つの部分 - 署名つき JWT の中身は、暗号化されていない。鍵がなくても、誰でも読める。秘密にしたい情報は、そもそも入れない
- 読む(デコード)ことは、検証ではない。
algをnoneにされる、アルゴリズムを取り違えさせられる、といった攻撃があるので、本番はライブラリの検証で、受け付けるアルゴリズムを固定する - RFC は
noneをひとことで禁止しているわけではない。RFC 7518 は「既定で受け付けない」、RFC 8725 は「呼び出す側が明示しない限り、受け付けるべきでない(SHOULD NOT)」と、少し弱い言い方で書いている expは秒単位の期限で、使うかどうかは任意。JWT 一般の「おすすめの長さ」は、確かめた資料に書かれていない- MDN も OWASP も、必要がなければふつうのセッション(サーバーが状態を持つ)を勧めている。JWT は、取り消しが難しい
- 保存場所は言い切れない。OWASP は
localStorageとsessionStorageに入れないと書き、MDN は Cookie を勧める。ただし、どの保存先も完全ではなく、Cookie に入れるなら CSRF への対策も要る
参考資料
この記事の内容は、2026年10月10日に開いた文書をもとにしています。RFC 8725 は、改訂版が作業中ですが、この日の時点の現行は RFC 8725 です。OWASP の Cheat Sheet は頻繁に書き換わります。
- RFC 7519: JSON Web Token (JWT)
- RFC 7515: JSON Web Signature (JWS)・RFC 7516: JSON Web Encryption (JWE)・RFC 7518: JSON Web Algorithms (JWA)
- RFC 8725: JSON Web Token Best Current Practices(
algの攻撃と対策) - RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage(OAuth の Bearer トークン)
- RFC 10017: OAuth 2.0 for Browser-Based Applications(OAuth のトークンの文書)
- RFC 4648: The Base16, Base32, and Base64 Data Encodings(base64url)
- OWASP: JSON Web Token Cheat Sheet
- OWASP: Session Management Cheat Sheet・OWASP: HTML5 Security Cheat Sheet
- MDN: セッション管理(日本語版)(英語版はこちら)
- MDN: ウェブの認証(Authentication)の概要
- MDN: HTTP Cookie のガイド
- MDN: CSRF の解説・MDN: XSS の解説
- MDN: Authorization ヘッダーのリファレンス・MDN: localStorage のリファレンス
※この記事は、公式の資料をもとに AI(Claude)で下書きし、2026年10月10日時点の公式の資料と照らして確かめてから公開しています。誤りに気づいたらお問い合わせからお知らせください。



