Menu

Node.jsでJWT認証:署名、検証、Expressのルート保護

JWTは、base64urlの3つの部分からなる署名付きのトークンで、サーバーがログイン時に発行し、リクエストのたびに確認します。Node.jsでは、joseかjsonwebtokenのパッケージと秘密鍵で署名と検証を行います。

このページのコードはエディタで実行できます。編集してすぐに結果を確認できます。

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分です。長いセッションには、より慎重に保存するリフレッシュトークンを組み合わせ、クライアントがそれを新しいアクセストークンと交換します。

Coddyのプログラミング言語のイラスト

Coddyでコードを学ぼう

始める