Next.jsのmiddlewareで公開リンクの除外を書き忘れるとログイン必須になる
※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。
結論
Next.js の middleware(Proxy)は全ルートを既定で認証チェックの対象にするため、新しく追加したログイン不要のルートは個別に除外条件へ加えない限り、そのまま /login へリダイレクトされる。
結論
Next.js の middleware(Next.js 16 では Proxy に改称)は、書いた条件式の対象を全ルートに既定でかける。 そのため、後から「ログイン不要のルート」を追加しても、middleware 側の認証チェック条件にその除外を書き足さない限り、未ログインのままアクセスした瞬間に一律で /login へリダイレクトされる。今回は取引先向けの公開共有リンク(/p/[token]、請求書・見積書を閲覧・ダウンロードするための窓口)がこの除外から漏れていた。
症状
公開共有リンク /p/[token] は、ログインアカウントを持たない取引先に請求書・見積書を見せるためのリンクで、本来ログイン不要で開けるはずだった。ところが実際にこのリンクを開くと、内容が表示される前に問答無用で /login へリダイレクトされ、先方はログイン画面を求められて書類を見られない状態になっていた。
原因
Next.js のリクエストは全て src/proxy.ts の proxy() 関数を通る。ここで Supabase のセッションを確認し、未ログインなら /login へ飛ばす判定をしている。
// src/proxy.ts:44-52(修正前)
const path = request.nextUrl.pathname;
const isLogin = path === "/login";
if (!isAuthed && !isLogin) {
const url = request.nextUrl.clone();
url.pathname = "/login";
url.search = "";
return NextResponse.redirect(url);
}
この条件式は「未ログイン」かつ「/login ページ自身ではない」場合に、それ以外の全パスをリダイレクト対象にする。公開共有リンクを扱う /p/[token] は特別扱いされておらず、isLogin が false になる以上、この条件にそのまま該当していた。
/p/[token] を開く画面自体(トークンを検証して書類を表示する処理)はすでに実装済みだったが、それより手前でリクエストを検査する middleware 側の条件式に、この公開ルートを対象外にする記述が無かった。機能の実装場所と、認証を一律でかける場所が別ファイルに分かれているため、機能ができていても認証側の除外を個別に足さない限り、そのまま /login に巻き込まれる。
直し方
path が /p/ から始まるかどうかを判定する isPublic を追加し、認証チェックの条件式にそのまま組み込んだ。
// src/proxy.ts:44-53(修正後)
const path = request.nextUrl.pathname;
const isLogin = path === "/login";
// 外部公開リンク(/p/[token])はログイン不要。請求書/見積書を先方が閲覧・DL。
const isPublic = path.startsWith("/p/");
if (!isAuthed && !isLogin && !isPublic) {
const url = request.nextUrl.clone();
url.pathname = "/login";
url.search = "";
return NextResponse.redirect(url);
}
除外は path.startsWith("/p/") という1行で足りている。ポイントは、この判定を /p/[token] を表示する画面側ではなく、全リクエストが通る middleware 側の条件式そのものに書いたことで、/p/ 配下に今後別のページが増えても個別対応が要らない形にした。
再発防止
このコミット自体は該当コミット1件のみで直っており、根拠となるコミット履歴にも修正後のルールやチェックリストの追加は残っていない。直せた理由は「気づいたから」であり、仕組みとして次の抜けを防ぐようにはなっていない。
構造として見ると、この種の漏れは認証を一律でかける場所(middleware)と、ログイン不要な機能を実装する場所(各ページ)が別ファイルに分かれているときに起きやすい。新しいログイン不要のルートを追加する作業は「そのページを作る」ことで完結して見えるため、proxy.ts 側の条件式まで戻って除外を足す必要があることが、実装している最中には見えにくい。今回のケースでも、公開共有リンクの画面自体はすでに動いていたにもかかわらず、認証側の条件式だけが追いついていなかった。
よくある質問
Q1Next.js の middleware(Proxy)で、特定のパスだけ認証を除外するにはどう書きますか?
request.nextUrl.pathname を見て、対象パスなら未認証でも通す条件を認証チェックの if 文自体に加えます。今回は `!isAuthed && !isLogin` という条件に `&& !isPublic`(`isPublic = path.startsWith("/p/")`)を足し、`/p/` 配下の公開共有リンクだけを対象外にしました。除外条件は認証チェックの if 文に書かないと効きません。
Q2この不具合はどんな影響がありましたか?
未ログインの取引先が公開共有リンク(/p/[token])を開いても、内容を見る前に一律 /login へリダイレクトされていました。ログイン不要を前提にしたリンクとして請求書・見積書を渡していたのに、リンク先で先にログインを求められる状態です。
Q3なぜ機能自体はあるのに除外だけ漏れたのですか?
公開共有リンク(/p/[token])を表示する画面自体は実装済みで、問題はその手前で全ルートに認証を要求する middleware 側の条件式でした。機能の実装場所と、認証を一律でかける場所が別ファイルに分かれているため、新しいログイン不要ルートを増やすたびに middleware 側の除外条件を個別に見直さない限り、片方だけ直っていても気づけません。
確認した環境
- Next.js 16.2.7(App Router、旧 Middleware は Next.js 16 で Proxy に改称)
- 2026-06-05 に発生・同日中に修正
この記事の根拠
- TypeScriptファイル 44〜52行目コミット 3731348
- TypeScriptファイル 44〜53行目コミット 4fb9321
本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。