JWT(JSON Web Token)は、header.payload.signature というbase64urlの3つの部分からなる文字列で、サーバーがログイン時にユーザーに渡し、その後のリクエストのたびに確認します。署名は、サーバーがトークンを発行し誰も書き換えていないことを証明しますが、ペイロードはエンコードされているだけで暗号化されていないので、誰でも読めます。Node.jsでは、jose パッケージの new SignJWT(payload).sign(secret) で作り、jwtVerify(token, secret) で確認します。
3 parts、続いて user-42 admin が表示されます。jwtVerify が署名と有効期限を確認して、ペイロードを返しました。jwtVerify に渡す秘密鍵を1文字変えて実行し直すと、検証は signature verification failed で失敗します。
JWTの中身
ヘッダーとペイロードはJSONで、base64urlでエンコードされています。デコードに鍵はいりません。だからJWTにパスワードなどの秘密情報を入れてはいけないのです。
最初の2行は {"alg":"HS256"} と {"name":"Ada","role":"editor"} を表示し、トークンを持っている人なら誰でも読めます。書き換えを防いでいるのは署名(HS256なら43文字)です。editor を admin に変えると、署名が一致しなくなります。
| 部分 | 中身 | 秘密か? |
|---|---|---|
| ヘッダー | HS256 などのアルゴリズム | いいえ、読める |
| ペイロード | クレーム:sub(ユーザーid)、exp(有効期限)、iat(発行時刻)、独自のフィールド | いいえ、読める |
| 署名 | 最初の2つの部分に対するHMACまたはRSAの署名 | 作るには鍵が必要 |
有効期限と検証の失敗
setExpirationTime('15m') は exp クレームを書き込み、jwtVerify はそれを過ぎたトークンを拒否します。拒否はすべて code を持つエラーなので、期限切れのトークンと偽造されたトークンを区別できます。
1つ目のチェックは ERR_JWT_EXPIRED で、署名を変えた2つ目は ERR_JWS_SIGNATURE_VERIFICATION_FAILED で失敗します。APIにとってはどちらも同じ意味(401 を返す)ですが、コードをログに残しておくとデバッグに役立ちます。
BearerトークンでExpressのルートを守る
クライアントは一度ログインしてトークンを保持し、Authorization ヘッダーで Bearer <token> として送ります。ミドルウェアがそれを検証し、ルートのためにペイロードを req.user に入れます。
最初のリクエスト、トークンなしの GET /me は 401 と Missing token を受け取ります。ボディ {"username": "ada", "password": "lovelace"} で POST /login を送り、レスポンスの token をコピーしてから、ヘッダー Authorization: Bearer <token> を付けて GET /me を送り直すと、返事は {"id":"user-1","name":"ada"} になります。
jsonwebtokenパッケージ
jsonwebtoken(npm install jsonwebtoken)もよく使われる選択肢です。sign と verify はPromiseを返さず、同期的に動きます(コールバックも使えます)。
const jwt = require('jsonwebtoken');
const token = jwt.sign({ role: 'admin' }, process.env.JWT_SECRET, {
subject: 'user-42',
expiresIn: '1h',
});
const payload = jwt.verify(token, process.env.JWT_SECRET); // throws if invalid
新しいコードには jose を使いましょう。依存関係がなく、エッジランタイムやブラウザでも動きます。どちらのライブラリのトークンも標準のJWTなので、片方が署名したものをもう片方で検証できます。
よくある間違い
verify の代わりに decode を使う。 jwt.decode と decodeJwt は署名を確認せずにペイロードを読むので、偽造したトークンも通ってしまいます。信頼してよいのは verify が返したものだけです。
短い秘密鍵、またはコミットされた秘密鍵。 推測できるHS256の秘密鍵があると、攻撃者は自分でトークンに署名できます。ランダムな32バイト以上を使い、process.env から読み、決してコミットしないでください。
期限切れにならないトークン。 exp がないと、漏れたトークンは永遠に使えます。必ず有効期限を設定しましょう。
よくある質問
クライアントはJWTをどこに保存すべきですか?
ブラウザのアプリなら、ページのスクリプトから読めない HttpOnly かつ Secure のCookieがいちばん安全です。localStorage は手軽ですが、注入されたものも含め、ページ上のどのスクリプトでもトークンを読めてしまいます。モバイルアプリはプラットフォームの安全なストレージを使います。
JWTでユーザーをログアウトさせるには?
サーバーは、すでに署名したトークンを取り消せません。期限が切れるまで有効なままです。クライアント側で削除し、アクセストークンの有効期間を短くしておき、即時のログアウトが必要なら、取り消したトークンのid(jti)のリストをサーバーに持ってリクエストのたびに確認します。
HS256とRS256のどちらを使うべきですか?
HS256は署名と検証に1つの共有の秘密鍵を使うので、バックエンドが1つなら向いています。RS256は秘密鍵で署名して公開鍵で検証するので、ほかのサービスはトークンを作れないまま確認だけできます。
JWTの有効期間はどのくらいにすべきですか?
アクセストークンはふつう5分から60分です。長いセッションには、より慎重に保存するリフレッシュトークンを組み合わせ、クライアントがそれを新しいアクセストークンと交換します。