「FLAG: OFF」スイッチを持つプログラマーと、Bearer/トークン認証からCookieセッション認証への段階移行システム図。ビルド時フラグとクッキーの二段ゲートを経て、成功・旧経路不使用・APIエラーの三値結果設計が示されている。

はじめに

トークン認証(Bearer)から Cookie セッション認証へ移行するとき、サーバー側だけ先に新方式へ対応させると、フロントエンドは「ログイン済みに見えない」状態になります。
かといってフロントまで一気に切り替えると、問題が起きた際に本番の全ユーザーへ影響が及びます。

そこで、フロントエンドの切り替えをビルド時の環境変数フラグ(既定 OFF)で包み、マージしても本番の挙動を一切変えない段階移行のパターンを整理します。
あわせて「新経路の一時障害を、未ログイン扱いにも旧経路へのフォールバックにもしない」という判断方針を紹介します。

こんな人におすすめ

  • 既存のトークン認証から Cookie セッション認証への移行を計画している方
  • 認証まわりの変更を、本番に影響を出さずにマージしたい方
  • Next.js で NEXT_PUBLIC_* フラグを使った段階リリースを検討している方
  • CSRF トークンの持ち方に迷っている方

罠1: フラグを入れるだけでは足りない(二段ゲート)

最初の方針は、フラグが true の完全一致のときだけ新経路を試し、それ以外は従来のコードパスをそのまま通ることです。
ただし、フラグ ON の全ユーザーが新経路を叩く設計だと、サーバー側の準備状況に引きずられます。
そこで次の二段ゲートにします。

  1. ビルド時フラグ(NEXT_PUBLIC_*)が true であること
  2. 非 HttpOnly の「目印 Cookie」があるか、URL で明示的に指定されていること
// ビルド時に埋め込まれる。完全一致の 'true' のみ有効
export const AUTH_CORE_CLIENT_ENABLED =
  process.env.NEXT_PUBLIC_AUTH_CORE_CLIENT === 'true';

サーバー側に許可リストや停止スイッチを置いておけば、閉じている間は 401/404 が返り、フロントは自動的に従来経路へ戻れます。
フロントの停止には再デプロイが必要ですが、即時停止はサーバー側スイッチで行える、と役割を分けておくのがポイントです。

罠2: 失敗を「未ログイン」にも「旧経路」にも落とさない

セッション確認の結果を成功・失敗の2値にすると、一時障害のときに判断を誤ります。
結果を ok / none / unavailable の3値にして、フォールバックの条件を「結果の種類」で分けます。

export type CoreSessionResult =
  | { kind: 'ok'; user: User; csrfToken: string; accountLogoutUrl: string | null }
  | { kind: 'none' }          // 新経路のセッションなし → 従来経路を使う
  | { kind: 'unavailable' };  // 一時障害 → 従来経路に切り替えない

if (sessionResponse.status === 401 || sessionResponse.status === 404) {
  csrfToken = null;
  return { kind: 'none' };
}
if (!sessionResponse.ok) return { kind: 'unavailable' };
  • none(401/404):新経路のセッションが無いだけなので、従来経路へ進みます
  • unavailable(503・通信失敗):従来経路にも未ログインにも落とさず、エラー表示にします

一時障害でログアウト扱いになったり、意図せず別の認証方式へ切り替わったりするのを防げます。
クライアント層は状態を返すだけにし、「落とすか、エラーにするか」の判断はフック側に置くと責務が分かれて読みやすくなります。

罠3: API ラッパーでも旧経路へ再試行しない

リクエストを送る層でも同じ方針を貫きます。
新経路で確認済みなら、401 が返っても Bearer で再試行しません。
認証方式が混在すると、原因の追跡が難しくなるためです。

if (AUTH_CORE_CLIENT_ENABLED && getCoreCsrfToken()) {
  return fetch(url, coreRequestInit(options));
}
// 以降は従来の Bearer 経路

export function coreRequestInit(options: RequestInit = {}): RequestInit {
  const method = (options.method ?? 'GET').toUpperCase();
  const headers = new Headers(options.headers);
  if (method !== 'GET' && method !== 'HEAD' && csrfToken) {
    headers.set('x-csrf-token', csrfToken);
  }
  return { ...options, headers, credentials: 'same-origin' };
}

CSRF トークンはモジュールスコープの変数にだけ保持し、localStorage などには保存しません。
GET/HEAD 以外のリクエストにだけ x-csrf-token ヘッダを付け、ログアウト時と 401 時に破棄します。

罠4: トークンが消えると表示用の情報も取れなくなる

HttpOnly Cookie にすると、トークンがブラウザの JavaScript から読めなくなります。
セッション API が ID 類しか返さない場合、表示用のユーザー情報は別の読み取り専用エンドポイントで取得します。

export const GET = withRateLimit('auth-me', 'read', async function GET(request: NextRequest) {
  try {
    const session = await verifyRequestSession(request);
    const user = mapProfileToUser(toUserProfile(session));
    return NextResponse.json({ success: true, user }, { headers: { 'Cache-Control': 'no-store' } });
  } catch (error) {
    return handleApiError(error, 'get auth me');
  }
});

DB には書き込まず、既存の検証関数をそのまま再利用するので、本人確認ロジックを二重に持たずに済みます。
レート制限と Cache-Control: no-store を付けた薄いルートにしておくのがおすすめです。

フラグ OFF の完全互換を保つ

フック側では、フラグ OFF のとき新経路の状態が常に「確認中ではない」になるように組みます。

const isLoadingAll = isLoading || coreChecking; // フラグ OFF では coreChecking は常に false

「確認中」の初期値をビルド時定数(フラグ)だけで決めると、サーバー描画とクライアント初回描画のずれ(ハイドレーション不一致)も避けられます。

つまづきやすいポイント

  • フラグは再デプロイが必要:NEXT_PUBLIC_* はビルド時に埋め込まれます。即時停止の手段はサーバー側スイッチに用意しておきましょう
  • フラグ値の判定を緩くしない:'1' や 'True' で有効になると、意図しない環境で ON になります。完全一致で判定するのが安全です
  • 認証状態が複数に分かれる:移行中はフック内の状態が増えます。移行完了後に旧経路を削除して単純化する前提で進めましょう
  • 実機確認はフラグ ON の再デプロイ時:Cookie 属性、CSRF、リダイレクトの挙動は、ステージングと本番で確認する必要があります

まとめ

認証方式の移行は、ビルド時フラグ(既定 OFF)とヒント Cookie の二段ゲートで、マージと有効化を切り離せます。
セッション確認の結果を ok / none / unavailable の3値にすると、一時障害を未ログインや旧経路への誤フォールバックにせずに済みます。
CSRF トークンはメモリだけに持ち、表示用情報は読み取り専用の me エンドポイントで取得する構成が扱いやすいと感じます。
こうした小さな方針を積み重ねることで、本番の挙動を変えずに段階移行できます。