0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【個人開発】JWT認証で「ログイン中」の表示が更新されない問題を解決した話

0
Posted at

はじめに

家計簿アプリを監視している中で、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テストも自動化できました。

おわりに

認証処理は実装できても、状態管理まで考慮すると意外と落とし穴が多いと感じました。

同じような問題で悩んでいる方の参考になれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?