
はじめに
トークン認証(Bearer)から Cookie セッション認証へ移行するとき、サーバー側だけ先に新方式へ対応させると、フロントエンドは「ログイン済みに見えない」状態になります。
かといってフロントまで一気に切り替えると、問題が起きた際に本番の全ユーザーへ影響が及びます。
そこで、フロントエンドの切り替えをビルド時の環境変数フラグ(既定 OFF)で包み、マージしても本番の挙動を一切変えない段階移行のパターンを整理します。
あわせて「新経路の一時障害を、未ログイン扱いにも旧経路へのフォールバックにもしない」という判断方針を紹介します。
こんな人におすすめ
- 既存のトークン認証から Cookie セッション認証への移行を計画している方
- 認証まわりの変更を、本番に影響を出さずにマージしたい方
- Next.js で
NEXT_PUBLIC_*フラグを使った段階リリースを検討している方 - CSRF トークンの持ち方に迷っている方
罠1: フラグを入れるだけでは足りない(二段ゲート)
最初の方針は、フラグが true の完全一致のときだけ新経路を試し、それ以外は従来のコードパスをそのまま通ることです。
ただし、フラグ ON の全ユーザーが新経路を叩く設計だと、サーバー側の準備状況に引きずられます。
そこで次の二段ゲートにします。
- ビルド時フラグ(
NEXT_PUBLIC_*)がtrueであること - 非 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 エンドポイントで取得する構成が扱いやすいと感じます。
こうした小さな方針を積み重ねることで、本番の挙動を変えずに段階移行できます。

