JWTとは
Web開発において認証・認可の仕組みは避けて通れない重要な要素です。その中でもJWT(JSON Web Token)は、現代のWeb APIやSPA(Single Page Application)で広く使われている認証トークンの標準規格です。
JWTは「ジョット」と読むそうです。
EメールとパスワードでログインするとWebアプリからユーザーにJWTが発行されます。
そのJWTにより、Webアプリがログインユーザーとわかり、なかったらログイン画面へ遷移させる。という処理を行うことができます。
JWTはログインしたらもらえる、テーマパークのチケットみたいなものとイメージしていただけるとわかりやすいと思います。
なぜJWTが使われるのか
JWTって最近聞きますが、ログインといえばセッションというイメージがある方もいるかと思います。Reactなどで作られるSPAのサイトではJWTが利用されています。
従来のセッション認証とJWT認証の違いをまとめてみます。
従来のセッション認証との違い
従来のセッション認証
- サーバー側でセッション情報を保持
- クッキーにセッションIDを保存
- サーバーがセッションストレージを管理する必要がある
JWT認証
- トークン自体に必要な情報を含む
- サーバー側でセッション情報を保持しない(ステートレス)
- クライアント側でトークンを管理
JWT認証が普及した背景と理由
従来のセッション認証では、サーバー側でユーザーのログイン状態を管理する必要がありました。これはサーバーにメモリやデータベースでセッション情報を保持することを意味し、サーバーの負荷となっていました。
JWTでは、認証に必要な情報をトークン自体に含めるため、サーバー側でセッション情報を保持する必要がありません。この「ステートレス」な特性により、サーバーの運用が大幅に簡素化されます。
また、SPAではページ遷移がないため、従来のセッション管理では認証状態の維持が困難になります。各画面遷移でサーバーにセッション確認をする必要がなく、クライアント側で認証状態を自己完結的に管理できるJWTは、SPAのアーキテクチャに非常に適しています。
JWTは必要な認証情報をトークン内に含むため、「自己完結」しています。サーバーはトークンを受け取った時点で、データベースに問い合わせることなく、そのトークンが有効かどうか、どのユーザーのものかを判断できます。これにより、認証処理の高速化とデータベースへの負荷軽減を実現できます。
JWTは標準化された仕様に基づいているため、Web、モバイルアプリ、デスクトップアプリなど、異なるプラットフォーム間で同じ認証方式を使用できます。
そのため、最近はJWT認証が広く使われるようになりました。
JWTという技術自体は2015年ごろに標準化されたようですが、実際に一般的な技術として広く採用されるようになったのは、ここ5年程度の話です。
JWTの構造
JWTは3つの部分がピリオド(.
)で区切られた文字列です:
ヘッダー.ペイロード.署名
1. ヘッダー(Header)
署名アルゴリズムとトークンタイプを指定します。
{
"alg": "RS256",
"typ": "JWT"
}
2. ペイロード(Payload)
トークンに含める情報(クレーム)を定義します。
{
"jti": "1daf94f0-42c7-4338-a721-e232cceefc4a202505870026",
"iss": "test001.co.jp",
"sub": "user_12345",
"aud": "https://api.example.com",
"exp": 1748399594,
"iat": 1748395994,
"role": "admin",
"permissions": ["read", "write", "delete"]
}
標準クレーム(Registered Claims)
クレーム | 説明 | 例 |
---|---|---|
iss |
発行者(Issuer) | "myapp.co.jp" |
sub |
主体(Subject) | "user_12345" |
aud |
対象者(Audience) | "https://api.example.com" |
exp |
有効期限(Expiration Time) | 1748399594 |
nbf |
有効開始時刻(Not Before) | 1748395994 |
iat |
発行時刻(Issued At) | 1748395994 |
jti |
JWT ID | "unique-token-123" |
カスタムクレーム
アプリケーション固有の情報を含めることができます。
{
"user_id": "12345",
"username": "john_doe",
"role": "admin",
"permissions": ["read", "write", "delete"],
"organization_id": "org_456"
}
3. 署名(Signature)
トークンの真正性と完整性を保証します。
RSASHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
private_key
)
JWTは署名によって改ざん検出が可能な仕組みになっています。
ヘッダーやペイロードの内容が一文字でも変更されると、署名の検証が失敗するため、改ざんを確実に検出できます。
サーバーは署名を検証することで、トークンが改ざんされていないことを確認し、そのJWTを持つユーザーが正当にログインしたユーザーであることを安全に認識できます。
一致しない場合は、改ざんまたは不正なトークンとして判断することができます。
アルゴリズム
署名を生成・検証するアルゴリズムには種類があります。
いくつか代表的なものをご紹介します。
HMACアルゴリズム(共通鍵)
- 共通鍵。同じキーで署名と検証をする
- 高速で軽量
- シンプルに実装できる
const token = jwt.sign(payload, sharedSecret, { algorithm: 'HS256' });
jwt.verify(token, sharedSecret); // 同じ鍵
RSAアルゴリズム(公開鍵と秘密鍵)
- 秘密鍵で署名。公開鍵で検証をする
- 公開鍵を安全に配布できる
- HMACより処理が重い
- レガシーシステムとの互換性
// 署名(秘密鍵)
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// 検証(公開鍵)
jwt.verify(token, publicKey, { algorithm: 'RS256' });
ECDSAアルゴリズム(公開鍵と秘密鍵)
- 秘密鍵で署名。公開鍵で検証をする
- 公開鍵を安全に配布できる
- RSAより高速
- 楕円曲線暗号
- RSAより小さなキーサイズで同等のセキュリティ
// 署名(秘密鍵)
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// 検証(公開鍵)
jwt.verify(token, publicKey, { algorithm: 'RS256' });
アルゴリズムによって、処理に時間がかかったり、 セキュリティ面で弱くなったりとメリット・デメリットはあります。 アプリケーションによって適したアルゴリズムを選択することが必要です。
セキュリティ考慮事項
実はJWTはBASE64でエンコードされているだけで、暗号化されていません。
つまり、誰でもペイロード部分をみることできます。
RSA256などのアルゴリズムは改ざんされていないことを確認するための署名に使用されます。
ペイロードにパスワードを書いたりするのは危険です。
ペイロードのデコード
BASE64をデコードするにはatob()
関数を使えば簡単にできます。(ASCII文字の場合)
https://jwt.io/の初期設定にあるJWTをデコードしてみましょう。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWTはBASE64にエンコードされたテキストが.(ピリオド)でつながっています。
ペイロードは2つ目なので、それをデコードしてみます。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
const payload = atob("eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ");
console.log(payload);
// 結果
'{"sub":"1234567890","name":"John Doe","iat":1516239022}'
デコードされました。
JWTが暗号化されているわけではなく、簡単に情報を見ることができるとお分かりいただけたかと思います。
alg=none攻撃
JWTは秘密鍵を知らない攻撃者が、ヘッダーやペイロードを改ざんしても検証時にエラーになりますが、JWTのヘッダーに記載されたアルゴリズムをalg=none
とすることで、署名なしでトークンを受け入れさせる攻撃があります。
{
"typ": "JWT",
"alg": "none"
}
現在はライブラリなどでは、対応が取られていますが(と思われますが。。)古いバージョンの場合、攻撃が成功してしまう可能性があります。
他にも様々な攻撃手法があり、セキュリティへの攻撃は常に進化しているので、
- ライブラリのバージョンを更新する
- 検証時のオプションでアルゴリズムを明示して検証する
- 有効期限を長くしすぎない
- 強い鍵を使う
ことなども重要です。
jsonwebtoken
ここからはJWTのライブラリjsonwebtoken
の使い方を解説していきます。
jsonwebtokenは、JWT(JSON Web Token)の生成、署名、検証を簡単に行えるNode.js用のライブラリです。
JWTのライブラリはjsonwebtoken以外にもたくさんの種類があります。
https://jwt.io/libraries
インストール
npm install jsonwebtoken
基本的な使い方
jsonwebtokenでできることは、JWTの生成と検証、デコードです。
トークンの生成
jwt.sign(payload, secretOrPrivateKey, [options, callback]);
JWTを生成します。
署名のアルゴリズムはデフォルトでHS256が使用されます。
RSA256などのアルゴリズムを使用する場合はオプションに明記します。
const token = jwt.sign(payload, privateKey, { alogorithim: 'RSA256'});
アルゴリズム以外にもオプションを付与することで、セキュリティの向上が期待できます。
const token = jwt.sign(
{ userId: 123 },
'secret',
{ expiresIn: '1h' } // ← 1時間に指定
);
// ペイロード
// { userId: 123, iat: 1689675632, exp: 1689679232 }
オプション | 説明 |
---|---|
audience |
トークンの対象者を指定 |
issuer |
トークンの発行者を指定 |
jwtid |
トークンの一意識別子 |
subject |
トークンの主体(通常はユーザーID) |
expiresIn |
トークンの有効期限 |
notBefore |
トークンの有効開始時刻 |
algorithm |
署名アルゴリズム |
noTimestamp |
iatクレームを含めない |
header |
JWTヘッダーにカスタム情報追加 |
keyid |
ヘッダーのkid設定のショートカット |
mutatePayload |
元のペイロードオブジェクトを変更 |
allowInsecureKeySizes |
2048未満の係数を持つ小さな鍵サイズを許可 |
allowInvalidAsymmetricKeyTypes |
非対称鍵の型チェック無効化 |
トークンの検証
jwt.verify(token, secretOrPublicKey, [options, callback]);
JWTを検証します。
verify()の返り値は真偽値ではなく、デコードされたペイロードになります。
const decoded = jwt.verify(token, 'secret');
console.log(decoded);
// 出力: { userId: 123, role: 'user', iat: 1689675632 }
検証のアルゴリズムはデフォルトでは、そのJWTから読み取ったアルゴリズムが使用されます。
アルゴリズムを明示する場合は、オプションに明記します。
// 特定のアルゴリズムのみ許可
jwt.verify(token, secret, { algorithms: ['HS256'] });
// 複数のアルゴリズムを許可
jwt.verify(token, publicKey, { algorithms: ['RS256', 'RS384'] });
アルゴリズム以外にもオプションを付与することで、セキュリティの向上が期待できます。
jwt.verify(token, secret, {
algorithms: ['RS256'], //アルゴリズム
issuer: 'qiita', //発行者
audience: 'qiita-users', //対象者
maxAge: '1h' //有効期限
});
オプション | 説明 |
---|---|
algorithms |
許可するアルゴリズムのリスト` |
audience |
検証する対象者(aud クレーム) |
issuer |
検証する発行者(iss クレーム) |
subject |
検証する主体(sub クレーム) |
jwtid |
検証するJWT ID(jti クレーム) |
clockTolerance |
時刻のずれ許容範囲(秒) |
maxAge |
トークンの最大有効期間 |
clockTimestamp |
検証時の基準時刻を指定 |
ignoreExpiration |
有効期限を無視するか |
ignoreNotBefore |
notBefore クレームを無視するか |
complete |
ヘッダー情報も含めて返すか |
nonce |
OpenID Connect の nonce 検証 |
allowInvalidAsymmetricKeyTypes |
非対称鍵の型チェック無効化 |
トークンのデコード
jwt.decode(token [, options])
JWTトークンを検証せずにデコードします。
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIn0.signature';
const decoded = jwt.decode(token);
console.log(decoded);
// 結果
{ userId: 123, email: 'user@example.com', iat: 1689675632 }
ヘッダーや署名も含んだ情報を取得したい場合はcomplete
オプションをつけると取得できます。
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30";
jwt.decode(token, { complete: true }));
// 結果
{
header: { alg: 'HS256', typ: 'JWT' },
payload: { sub: '1234567890', name: 'John Doe', admin: true, iat: 1516239022 },
signature: 'KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30'
}
基本的に使用するのはsign()とverify()だけになるかと思います。
decode()の結果だけで認証するのは署名検証なしで認証してしまうため、危険です。
まとめ
- JWTは認証で使われるトークンの標準規格
- ステートレスのため、サーバー側で認証状態を保持する必要がない
- JWTは異なるプラットフォームでの同じ認証方式を使用できる
- JWTはヘッダーとペイロード、署名で構成される
- JWT自体は暗号化されていない
- JWTを生成するときはsign()、検証するときはvefify()、デコードするだけはdecord()
- オプションをつけてセキュリティを強くしよう
実際にログイン機能を実装する場合 bcrypt
などでハッシュ化したパスワードをデータベースに保存し、認証できたら、JWTをユーザーに渡し認証・認可を可能にすることになります。
JWTを正しく理解し、適切に実装することで、安全で効率的な認証システムを構築できます。
参考サイト