TOOLS BOX/ガイド/Cookie 有効期限・更新処理パターン
Concept

Cookie 有効期限・更新処理パターン

Session 維持に関わる Cookie の寿命設計と更新タイミングを整理するガイド。maxAge / expires の使い分け、固定期限とスライディング期限の違い、JWT と DB Session それぞれでの更新処理の考え方を比較する。

cookiemaxAgeexpiressessionsliding-sessionjwtnextjsauthentication

どういう場面で使うか

  • ·ログイン後のセッション維持期間を設計するとき
  • ·アクセスのたびにセッションを延長するスライディング期限を実装するとき
  • ·JWT と DB Session で Cookie の更新処理がどう違うかを判断したいとき
  • ·ログアウト・権限変更と期限設計の関係を整理したいとき

注意点 / Pitfalls

  • ·Cookie の maxAge だけ更新してサーバー側(DB / JWT の exp)の期限を更新しないと、Cookie は有効でもサーバーが期限切れと判断する
  • ·expires に Date オブジェクトを渡すとクライアント時計のずれに影響される。maxAge(秒数指定)を優先する
  • ·毎リクエスト JWT を再発行すると署名コストが増える。閾値(残り X 分以下)で更新する方が現実的
  • ·スライディング期限は「最後のアクセスから N 時間」で延び続ける。absolute 上限を設けないと永続ログイン状態になる
  • ·Cookie の期限を長くしても強制ログアウトできない方式(JWT など)では、セキュリティインシデント時の対処が難しくなる

何と混同しやすいか

補足

Cookie の寿命設計は「ユーザーの利便性」と「セキュリティリスク」のトレードオフ。期限を短くすれば再ログインが増え、長くすれば窃取リスクが残る。更新処理はサーバー側(DB / JWT)と Cookie 側を同期させることが前提。

maxAge と expires の違い

Cookie の有効期限は maxAge(秒数指定)または expires(日時指定)で設定します。

maxAgeexpires
指定方法秒数(相対)Date オブジェクト(絶対)
基準時刻ブラウザが受信した時刻から計算クライアントの時計に依存
推奨✅ こちらを使うクライアント時計のずれに影響される
// ✅ maxAge を使う(相対指定。クライアント時計の影響を受けない)
cookieStore.set("session_id", token, {
  maxAge: 60 * 60 * 24 * 7, // 7日(秒単位)
});

// ⚠️ expires を使う場合(絶対日時。サーバーとクライアントの時計差に注意)
cookieStore.set("session_id", token, {
  expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
});

maxAgeexpires が両方指定された場合、maxAge が優先されます。基本は maxAge のみで十分です。

固定期限とスライディング期限

固定期限(Fixed Expiry)

ログイン時に設定した期限がそのまま続き、更新されません。

10:00 ログイン → Cookie の期限: 翌 10:00(24 時間後)
10:30 アクセス → 期限変わらず翌 10:00
23:50 アクセス → 翌 10:00 まであと 10 分。操作中にセッション切れになりうる
翌 10:01 アクセス → セッション切れ → ログイン画面へ

シンプルで実装コストが低い反面、「作業中にセッションが切れる」体験が発生しやすい。

スライディング期限(Sliding Expiry)

アクセスのたびに期限を延長します。最後のアクセスから N 時間操作がなかった場合にセッション切れになります。

10:00 ログイン → Cookie の期限: 翌 10:00(24 時間後)
14:00 アクセス → Cookie の期限: 翌 14:00 に延長
23:00 アクセス → Cookie の期限: 翌々日 23:00 に延長
  ↓(以降 24 時間アクセスなし)
翌々日 23:01 → セッション切れ

「使っている限りログインが切れない」体験になるが、absolute 上限がないと永続ログイン状態になる。実務では「最大○日」の上限を別途設ける。

スライディング延長: アクセスのたびに +24 時間
Absolute 上限: ログインから 30 日後に強制セッション切れ

どのタイミングで Cookie を更新するか

毎リクエスト更新

// middleware.ts — 全リクエストで Cookie を延長する
export function middleware(request: NextRequest) {
  const token = request.cookies.get("session_id")?.value;
  if (!token) return NextResponse.next();

  const response = NextResponse.next();
  // アクセスのたびに期限を延長(スライディング)
  response.cookies.set("session_id", token, {
    httpOnly: true,
    sameSite: "lax",
    secure: process.env.NODE_ENV === "production",
    maxAge: 60 * 60 * 24, // 24 時間リセット
  });
  return response;
}

常に最新の期限になる反面、すべてのリクエストで Set-Cookie ヘッダーが付く。

閾値更新(残り期限が X 以下になったら更新)

// middleware.ts — 残り 1 時間を切ったら延長する
export function middleware(request: NextRequest) {
  const sessionExpires = request.cookies.get("session_expires")?.value;
  if (!sessionExpires) return NextResponse.next();

  const expiresAt = new Date(sessionExpires);
  const remainingMs = expiresAt.getTime() - Date.now();
  const oneHour = 60 * 60 * 1000;

  const response = NextResponse.next();

  // 残り 1 時間以下のときだけ更新する
  if (remainingMs < oneHour) {
    const newExpires = new Date(Date.now() + 24 * 60 * 60 * 1000);
    response.cookies.set("session_id", request.cookies.get("session_id")!.value, {
      httpOnly: true,
      sameSite: "lax",
      secure: process.env.NODE_ENV === "production",
      maxAge: 60 * 60 * 24,
    });
    response.cookies.set("session_expires", newExpires.toISOString(), {
      sameSite: "lax",
      secure: process.env.NODE_ENV === "production",
      maxAge: 60 * 60 * 24,
    });
  }

  return response;
}

更新頻度を抑えられるが、判定用に期限情報を別 Cookie に持たせる必要がある。

JWT と DB Session で更新処理はどう変わるか

JWT の場合

JWT の期限は ペイロードの exp フィールドに埋め込まれます。期限を延長するには新しい JWT を再発行する必要があります。

[期限延長の流れ(JWT)]
1. リクエスト時に JWT を検証
2. 残り期限が閾値以下なら新しい JWT を署名して発行
3. 新しい JWT を Cookie にセット(古い JWT は捨てる)
// lib/session.ts
import { SignJWT, jwtVerify } from "jose";

const SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);

export async function refreshJwtIfNeeded(token: string): Promise<string | null> {
  try {
    const { payload } = await jwtVerify(token, SECRET);
    const exp = payload.exp as number;
    const remainingSec = exp - Math.floor(Date.now() / 1000);

    // 残り 1 時間以下なら再発行
    if (remainingSec < 3600) {
      return await new SignJWT({ userId: payload.userId, role: payload.role })
        .setProtectedHeader({ alg: "HS256" })
        .setExpirationTime("24h")
        .sign(SECRET);
    }
    return null; // 更新不要
  } catch {
    return null;
  }
}

DB Session の場合

Session データは DB 側に expiresAt カラムとして持ちます。Cookie と DB の両方を更新します。

[期限延長の流れ(DB Session)]
1. Cookie の Session ID で DB を検索
2. expiresAt が閾値以下なら DB の expiresAt を更新
3. Cookie の maxAge も同じ期限に延長
// lib/session.ts
export async function refreshSessionIfNeeded(sessionId: string): Promise<boolean> {
  const session = await db.session.findUnique({ where: { id: sessionId } });
  if (!session) return false;

  const remainingMs = session.expiresAt.getTime() - Date.now();
  const oneHour = 60 * 60 * 1000;

  if (remainingMs < oneHour) {
    await db.session.update({
      where: { id: sessionId },
      data: { expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) },
    });
    return true; // Cookie の maxAge も更新が必要
  }
  return false;
}

DB Session では Cookie と DB の期限を必ず同期させます。どちらか一方だけ更新するとずれが生じます。

よくある誤用

Cookie の期限だけ更新してサーバー側を更新しない

❌ Cookie: maxAge を延長した(ブラウザ上では有効に見える)
   DB Session: expiresAt は古いまま → 次のリクエストでサーバーが期限切れと判断
   → ログインしているのに操作できない状態になる

✅ Cookie と DB Session(または JWT の再発行)を必ずセットで更新する

JWT を毎リクエスト再発行する

JWT の署名には CPU コストがかかります。毎リクエスト再発行すると不要な処理が増えます。「残り期限が X 分以下になったら更新」の閾値方式が現実的です。

スライディング期限に absolute 上限を設けない

スライディング期限のみ: 毎日アクセスしていると永続ログイン状態になる

✅ 「最後のアクセスから 24 時間」+「ログインから最大 30 日」の 2 層構造にする
   → どちらかの期限に達したらセッション切れ

期限を長くしすぎる

Cookie の期限を長くするほど、トークンが窃取された場合に有効期間が長く残ります。即時無効化できない JWT では特にリスクが高まります。要件に必要な最短の期限を設定することが基本です。


Cookie のセキュリティ属性(httpOnly / sameSite / secure) → Cookie セキュリティオプション 3点セット

Session 取得 API の責務分離 → 型安全な Session 管理パターン

JWT と DB Session の保存方式比較 → JWT vs DB Session 比較

関連ドキュメント

関連サンプル

同じテーマや技術スタックを使った実装例