Next.js のログイン。認証をどこで確かめるか
Next.js のログインは、Cookie には鍵だけを入れ、誰なのかはページと Server Function がそれぞれサーバー側で確かめる形で作ります。
決めることは3つだけ
ログインの作り方はライブラリによって見た目が違いますが、App Router で決めることは3つです。
| 決めること | 答え |
|---|---|
| Cookie に何を入れるか | 鍵だけ。名前や権限は入れない |
| Cookie をどこで書くか | 送信先(Server Function か Route Handler) |
| 誰なのかをどこで確かめるか | 見せるページと、呼ばれる関数の両方 |
このサイトの最小構成のログインは、この3つをそのまま実装して動いています。説明用に書き直したコードではなく、そのページを動かしている本物のファイルです。
Cookie には鍵だけを入れる
名前や権限を Cookie に入れると、利用者が自分で書き換えられます。Cookie はブラウザの持ち物だからです。
入れるのは、サーバー側の記録を引くための鍵だけにします。誰なのか、何ができるのかは、鍵を受け取ったサーバーが自分の記録を見て決めます。
越えられる: 鍵
越えられない: 名前・権限・メールアドレス
Cookie を書けるのは送信先だけ
ページの中で書こうとすると、こうなります。実機で確かめました。
import { cookies } from 'next/headers'
export default async function Page() {
const c = await cookies()
c.set('session', 'abc')
return <p>ログインしました</p>
}
ビルドは通ります。開いた瞬間に 500 になり、サーバーのログにこう出ます。
Error: Cookies can only be modified in a Server Action or Route Handler.
ページを描くのは表示のための処理です。開いただけで状態が変わると困るので、書き込みは送信を受け取る側に限られています。
- Browserフォームを送る
名前やメールアドレスを入れて送信する。
- Server送信先の Server Function
確かめて、鍵を作り、Cookie に入れる。そして別のページへ移す。
- Server移った先のページ
Cookie から鍵を読み、サーバー側の記録で誰かを確かめる。読むだけ。
守るのは、見せる側と呼ばれる側の両方
ここがいちばん抜けやすいところです。
ページは自分で確かめる
「ログインしていない人にはリンクを出さない」だけでは、URL を直接開かれたときに素通りします。見せたくないページは、そのページ自身が鍵を読んで確かめ、いなければ入口へ移します。
入口へ移す redirect を try で囲むと、移動せずに catch へ落ちます。取得だけを try で囲み、redirect は外で呼んでください(redirect が効かない)。
proxy で手前でも振り分けられますが、それは体験を良くするためのもので、守りではありません(proxy(旧 middleware)の書き方)。
Server Function も1つずつ確かめる
公式のドキュメントは、Server Action について画面を通らずに直接 POST で呼べると明記しています。そして、それぞれの中で認証と認可を確かめるように、と書いています。
'use server' export async function deleteItem(id: string) { await db.item.delete({ where: { id } }) }
ボタンを消しても、関数は外から呼べる。
'use server' export async function deleteItem(id: string) { const user = await currentUser() if (!user) throw new Error('forbidden') await db.item.delete({ where: { id, ownerId: user.id } }) }
誰が呼んだかを、関数の中で毎回確かめる。
ユーザー情報を丸ごと渡さない
Client Component に user をまるごと渡すと、画面に出していない項目もブラウザに届きます。実機で確かめると、名前しか表示していないのに passwordHash の値がブラウザに届く中身に入っていました(この問題)。
渡すのは、その部品が使う値だけにします。
<Greeting name={user.name} /> // user まるごとではなく
速くしようとして、ログインを壊さない
ログインしている人によって中身が変わるページに force-static を付けると、エラーも警告も出ないまま、ログインしていない状態の画面が全員に配られます。実機で確かめました(キャッシュの考え方)。
人によって変わるページは、アクセスのたびに描かれる側に置いたままにしてください。
まとめ
- Cookie には鍵だけ。中身はサーバーに置く
- Cookie を書けるのは送信先だけ。ページで書くと開いた時点で落ちる
- ページも Server Function も、それぞれ自分で確かめる。画面で隠すのは守りではない
- Client Component には使う値だけを渡す
- 人によって変わるページを静的にしない
出典
- nextjs.org/docs/app/guides/authentication
- nextjs.org/docs/app/guides/data-security
- nextjs.org/docs/app/api-reference/functions/cookies
このページの内容は上記の一次情報を基準に書いています。個人ブログや二次情報を根拠にしていません。