0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

概要

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など残っているので順次削除して参ります...

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?