はじめに
家計簿アプリを監視している中で、JWTの有効期限が切れたにも関わらず管理画面では「ログイン中」と表示されたままになる問題が発生しました。
今回はその原因と解決方法、そしてPlaywrightを用いたE2Eテストまで実装した内容をまとめます。
システム構成
React
│
▼
Express
│
JWT(HttpOnly Cookie)
│
MongoDB
ログイン状態は
- JWT
- MongoDBの
is_logged_in
の2つで管理していました。
発生した問題
JWTが期限切れになっても画面上ではログイン中と表示されていました。
JWT期限切れ
│
▼
画面はログイン中
│
▼
APIアクセス
│
▼
401 Unauthorized
管理画面
ユーザー名 ログイン状態
山田太郎 ログイン中
しかし実際にはJWTは失効しているため、
GET /api/home/me
401 Unauthorized
となります。
原因
Express側では
jwt.verify(token)
で期限切れを検知していました。
しかし
User.is_logged_in
は更新していませんでした。
つまり
JWT
↓
期限切れ
DB
↓
ログイン中
となり、
認証状態と管理画面の表示が一致していませんでした。
修正内容
① TokenExpiredErrorを検知
try {
const decoded = jwt.verify(token, JWT_SECRET);
} catch (err) {
if (err.name === "TokenExpiredError") {
}
}
② Cookieを削除
clearAuthCookie(res);
③ DBのログイン状態を更新
const decoded = jwt.decode(token);
await updateLoginStatus(decoded.id, false);
最終的な処理
JWT期限切れ
│
▼
TokenExpiredError
│
▼
Cookie削除
│
▼
DB更新
(is_logged_in=false)
│
▼
401返却
フロントエンド
AuthContextでは
const res = await fetch("/api/home/me", {
credentials: "include",
});
401の場合は
if (res.status === 401) {
setUser(null);
return;
}
としました。
これでJWT期限切れ後はログイン状態を保持しません。
E2Eテスト
今回はPlaywrightを使って
JWT期限切れを人工的に発生させました。
流れは
ログイン
↓
JWT取得
↓
期限切れJWT生成
↓
Cookie差し替え
↓
/me
↓
401(TOKEN_EXPIRED)
↓
DB更新
↓
管理画面確認
期限切れJWT生成
const expiredToken = jwt.sign(
{
id: decoded.id,
email: decoded.email,
role: decoded.role,
},
process.env.JWT_SECRET!,
{
expiresIn: "-1s",
}
);
Cookieを書き換える
await userContext.addCookies([
{
name: "user_token",
value: expiredToken,
url: "http://localhost:5001",
httpOnly: true,
},
]);
レスポンス確認
expect(meResponse.status()).toBe(401);
expect(meBody.code).toBe("TOKEN_EXPIRED");
E2E環境
開発DBへ影響しないように
専用環境を構築しました。
Playwright
│
Frontend(Container)
│
Backend(Container)
│
MongoDB(integration_test)
追加したもの
- docker-compose.test.yml
- Dockerfile.test
- .env.test
- seed.e2e.js
苦労した点
Cookie
Playwrightでは
context.addCookies()
で
url
を指定しないとCookieを書き換えられません。
さらに
Frontend
3000
Backend
5001
だったため、
API側URLへCookieを設定する必要がありました。
ブラウザ差異
Chromium
✅
Firefox
✅
WebKit
✅
ただし状態更新系のテストでは
ブラウザ毎に別ユーザーを用意しました。
test-chromium@...
test-firefox@...
test-webkit@...
これにより並列実行でも競合しませんでした。
まとめ
JWT認証では
「JWTが有効か」
だけではなく
- Cookie
- DB
- UI
すべての状態を同期させることが重要です。
今回の修正によって
JWT期限切れ
↓
Cookie削除
↓
DB更新
↓
管理画面更新
↓
401返却
まで一連の流れを保証できるようになりました。
またPlaywrightを利用することで、
認証周りのE2Eテストも自動化できました。
おわりに
認証処理は実装できても、状態管理まで考慮すると意外と落とし穴が多いと感じました。
同じような問題で悩んでいる方の参考になれば幸いです。