概要
Next.jsによるWebアプリ開発の練習のため、本家チュートリアルに従ってAuth.jsを用いた認証機能の実装を試みましたが、想定外に難しかった部分をまとめます。
-
現在のCanaryバージョン(15)以上でないと、Next.jsチュートリアル通りの認証関連のコードがエラーになりました
→ チュートリアルだしstable版だろうと思い込んでいました😂
-
認証機能の要になる
auth.ts
,auth.config.ts
,middleware.ts
の置き場所を間違っていました
→create-next-app
した時の設定にもよりますが、私の場合はsrc/
直下でした
-
上記を解決して認証機能は部分的に動きましたが、middlewareのmatcher設定を誤って画像類が表示されなくなりました
→ Next.jsプロジェクトのpublic/
フォルダに入れた画像がmatcherによって認証対象にならないように、matcher: ['/']
という最小限の設定をしました
-
Auth.jsのUserオブジェクトの扱いを誤って、
auth()
関数が空のuserオブジェクトを返していました
本来のUserオブジェクトに無いキーを持ったオブジェクトにすると、セッションにユーザ情報が保存されません......
https://github.com/nextauthjs/next-auth/discussions/2762
同じように悩んでいる方の参考になれば幸いです。
もう少し詳しく
開発中のToDoアプリのソースコードを引っ張ってきます。
以下src/auth.config.ts
には、ログイン用のページの指定や
ログイン後のリダイレクトの設定などがあります。
src/auth.config.ts
import type { NextAuthConfig, User } from "next-auth";
export const authConfig = {
pages: {
// ログイン用に表示するページを
// src/app/login/pages.tsx に置いています
// ログインが必要な際にはリダイレクトされるのでなく
// 表示内容がログイン用ページで置き換わるような動作をします。
signIn: '/login',
},
callbacks: {
// Next.jsチュートリアル通りの認証後のリダイレクトなどの設定
async authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnRoot = nextUrl.pathname.startsWith('/')
if (isOnRoot) {
return isLoggedIn;
} else if (isLoggedIn) {
return Response.redirect(new URL('/', nextUrl));
}
return true;
}
},
providers: [],
} satisfies NextAuthConfig;
以下src/middleware.ts
には、認証を行うURLを指定するmatcherがあります。
認証機能が動いた後、画像類が一切表示されずに悩みました...
src/middleware.ts
import NextAuth from "next-auth";
import { authConfig } from './auth.config';
export default NextAuth(authConfig).auth;
export const config = {
// 元々はチュートリアルに従って matcher に
// ['/((?!api|_next/static|_next/image|.*\\.png$).*)']
// を設定していましたが、
// public/*.svg に入れた画像がmatcherに引っかかって表示されなくなり、
// 本当に必要な部分だけに絞ることにしました
matcher: ['/'],
};
以下src/auth.ts
内には、Auth.jsのUserオブジェクトの扱いについて注意するべきポイントがあります。
src/auth.ts
import NextAuth, { DefaultSession, User } from "next-auth";
import Credentials from 'next-auth/providers/credentials';
import { authConfig } from './auth.config';
import { z } from 'zod';
import { db } from "@/db";
import { eq } from 'drizzle-orm';
import { users } from "@/db/schema";
import { hashWithSalt } from '@/lib/crypto';
// データベースから指定したユーザ名 id を持つユーザ情報を取得するための関数です
// Drizzle ORM を使用していますが、見たままなので詳細は省略します
async function getUser(id: string) {
try {
const user = await db.select().from(users).where(eq(users.id, id));
return { username: user[0].id, passWithSalt: user[0].passWithSalt };
} catch (error) {
console.error('Failed to fetch user:', error);
throw new Error('Failed to fetch user.');
}
}
export const { handlers, signIn, signOut, auth } = NextAuth({
...authConfig,
providers: [
Credentials({
credentials: {
username: {},
password: {},
},
// ほぼNext.jsチュートリアル通りの認証用関数
async authorize(credentials): Promise<User|null> {
const parsedCredentials = z
.object({ username: z.string(), password: z.string() })
.safeParse(credentials);
if (!parsedCredentials.success)
throw Error('Invalid credentials format.');
const { username, password } = parsedCredentials.data;
const hashedPassword = await hashWithSalt(
password, process.env.HASH_SALT!
);
const user = await getUser(username);
if (!user) return null;
const passwordsMatch = (
hashedPassword === user.passWithSalt
);
// !!
// !! ここは注意が必要 !!
// !! Auth.js内のUserの定義は
// !! {
// !! id?: string;
// !! name?: string | null;
// !! email?: string | null;
// !! image?: string | null;
// !! }
// !! となっており、それ以外のフィールドがあると
// !! セッション内にユーザ情報が記録されないです......
// !! うっかりハッシュ化されたパスワードのデータ等を明らかにしないためでしょうか
// !! これ以外のフィールドを足す方法もあるようです
// !! https://authjs.dev/getting-started/typescript
if (passwordsMatch) return { id: username };
return null;
},
})
],
});
Auth.jsで定義されているUser型のキーに気を付けていれば、
https://authjs.dev/getting-started/session-management/get-session
の様にユーザ情報を取得できます。
試行錯誤中のリポジトリ
上記コードの元になっている開発中のWebアプリのリポジトリへのリンクを貼っておきます。
この記事を書いている時点では、ユーザ情報を出力する不穏なconsoole.log
など残っているので順次削除して参ります...