プロジェクトの出発点にしているボイラープレートでは、最初の認証プロバイダを入れたコミットと、それを二つ目に置き換えたコミットの間隔が二日です。二日です。ひとつ目は保護ルート用のミドルウェアとログイン画面を入れ、ふたつ目はそれをまるごと引き抜いて、独自の sign-in と sign-up ページを持つ別の SDK を置きました。それ以来、同じテンプレートはさらに二つのプロバイダを経て、約一年後に手書きのミドルウェアに落ち着きました。
運が悪かったのではありません。パターンであり、少なくとも四つのリポジトリで繰り返しました。
必ずマネージドのプロバイダで始めるし、理由もいつも同じ
プロジェクトの初日に、パスワードのハッシュもメール認証も Google の OAuth フローも書きたくありません。SDK を入れ、環境変数を二つ置けば、その午後にはログインができます。その日としては正しい判断です。プロジェクトが一か月後に存在しているかどうかもまだ分からないのですから。
問題は、その SDK がアプリケーションの縁でおとなしくしていないことです。レイアウトに入り、ユーザーのコンテキストに入り、呼び出し元を知る必要のあるサーバーアクション全部に入り、そしてデータベースのスキーマにも入ります。そのテーブルの user_id が、自分のすべてのテーブルの外部キーになるからです。
四回の移行と、それぞれの引き金
- プロバイダ一から二へ、二日で。 ひとつ目は動いていましたが、価格モデルと専用ダッシュボードへの結合が合いませんでした。認証のコードは一行も生き残りませんでした。
- プロバイダ二と六か月の同居。 ここではいろいろありました。Suspense との相性の悪さについての README のメモ、保護ドメインを設定ファイルから読むためのミドルウェアのリファクタリング、そして相手の上に自前の層を維持するのをやめるための
useProfileからuseUserへの変更。 - プロバイダ二から三へ。 PR の名前がそのまま "migrate from" 前のプロバイダ名でした。三十ファイルに変更。
stack.tsとキャッチオールのルートapp/handler/[...stack]/page.tsxが消え、クライアント側のauth.tsとサーバー側のapp/api/auth/[...path]/route.tsが現れます。 - プロバイダ三から自前のミドルウェアへ。 コミットには文字通り "replace neonAuth with custom auth middleware and update session handling" とあります。マネージドの SDK からはクライアントだけ残し、あとは自分で制御します。
いつも壊れるもの: URL
二回目の移行では、同じ日に連続した二つのコミットがあります。ひとつ目は設定ファイルをこうしています。
export const stackServerApp = new StackServerApp({
tokenStore: "nextjs-cookie",
urls: {
signIn: "/ass",
signUp: "/asdds",
},
});
何がどこへリダイレクトするかを手で試していて、適当に打った値を残したまま出てくるのがこれです。ふたつ目のコミットで両方 /auth に戻ります。ばかばかしい話ですが、認証の移行で必ず食らう不具合そのものです。プロバイダはあなたのログイン画面がどこにあるか知りません。だからその設定に宣言された URL が、ルーターの実在のルートと一致しなければならず、それを検査する型はありません。404 にリダイレクトされ、症状は「URL が違う」ではなく「ログインが動かない」になります。
壊れるものの残り半分は、セッションの形です。自前のミドルウェアに移った際、getSession は SDK のオブジェクトを返すのをやめ、明示的なものを返すようになりました。
type SessionData = NonNullable<Awaited<ReturnType<typeof auth.getSession>>["data"]>;
export type Session = SessionData | { session: null; user: null };
この | { session: null; user: null } が払うべき代償です。以前の SDK は素の nullable を返していて、コードの半分は session?.user と書いていました。正規化した途端、セッションが undefined になりうると仮定していた場所がすべてコンパイルしなくなりました。移行で高くつくのはそこであって、ログインではありません。
自分で書いて得るもの、失うもの
得るものは具体的です。セッションオブジェクトの制御、Cookie の保存場所の制御、セッションがないときに何が起きるかの正確な制御。そして必要になるまで評価していなかったもの、認可のルールを同じ場所に置けることです。
大企業向けの BI ツールでは、認可が最終的にミドルウェア内のメールアドレスのリストになりました。
const authorizedEmails = [ /* ... */ ];
const hasFullAccess = userEmail && authorizedEmails.includes(userEmail);
リダイレクトは二通り。未認証はログインへ、認証済みだが権限のない人はトップへ。マネージドのプロバイダでこれをやるには、クレームやその管理画面と格闘することになります。ここでは十行で書けました。ついでに console.log("User email:", userEmail) を置きっぱなしにして、あとのコミットで消す羽目にもなりました。そのアプリは一か月で認証を三回書き直しています。
失うのは、プロバイダがやっていて見えていなかったもの全部です。トークンのローテーション、セッションの無効化、パスワード再設定のメール、ログインのレート制限。どれも不可能ではありませんが、自分のコードになります。自分で書いていないテストは付いてきません。
いまの判断基準
「認証は自分で書け」ではありません。もっと地味です。初日から、プロバイダを乗り換えると想定しておく。 何かをインストールする前にいまやっていること。
- 自分のコードには
getSession()を一つだけ。 SDK を import するコンポーネントはゼロ。戻り値の型は自分で定義し、プロバイダから推論しません。三十ファイル移行するか一ファイルで済むかの差です。 - 自分のデータベースの
user_idは自分のもの。 プロバイダのものは別のカラムに入れます。さもないと、移行はライブラリの入れ替えではなくデータ移行になります。 - ログインの URL は定数に置き、プロバイダの設定に置かない。
/assが隠れられる場所をなくすためです。 - 認可は絶対にプロバイダの中に置かない。 誰が入れるかはプロバイダの仕事、何ができるかは自分のコードの仕事です。
最近、新しいプロジェクトを始めて、またマネージドのプロバイダから始めました。初日はログインできませんでした。認証のベース URL が、ボイラープレートの穴埋め用の値を指していたのです。サイクルはまた始まりました。今回は、どこが痛むのか正確に分かっています。
