paint-brush
NextAuth.js、Shadcn/ui、react-hook-form、Zod を使用して Next.js 14 に認証を実装する方法@ljaviertovar
1,376 測定値
1,376 測定値

NextAuth.js、Shadcn/ui、react-hook-form、Zod を使用して Next.js 14 に認証を実装する方法

L Javier Tovar19m2024/03/01
Read on Terminal Reader

長すぎる; 読むには

JavaScript エコシステム内、特に Next.js で開発されたアプリケーション内で、認証を処理するための最も著名なライブラリの 1 つは NextAuth.js です。 このツールは、アプリケーションに認証を追加するためのシンプルで実装が簡単なソリューションを提供します。最も優れている点はその柔軟性です。従来の電子メールやパスワードなどの資格情報ベースの認証に加えて、Google、Facebook、Twitter などのさまざまな認証プロバイダーの統合が可能になります。
featured image - NextAuth.js、Shadcn/ui、react-hook-form、Zod を使用して Next.js 14 に認証を実装する方法
L Javier Tovar HackerNoon profile picture
0-item
1-item

包括的なガイド: 資格情報 (電子メールとパスワード)

ユーザーのセキュリティとプライバシーを確保することがこれまで以上に重要になっています。 Web 認証はこの面で重要な役割を果たし、ユーザーの情報とデータを保護するための防御の最前線として機能します。


現在、NextAuth.js のようなツールがあり、作業がはるかに簡単になり、さまざまな種類の認証を Next.js アプリケーションに簡単に実装できるようになりました。


この一連のチュートリアルでは、基本的な電子メールとパスワードによる認証から始めて、Next.js 14 で完全な認証システムを構築します。

NextAuth.js (Auth.js) とは何ですか?

JavaScript エコシステム内、特に Next.js で開発されたアプリケーション内で、認証を処理するための最も著名なライブラリの 1 つは NextAuth.js です。


このツールは、アプリケーションに認証を追加するためのシンプルで実装が簡単なソリューションを提供します。最も優れている点はその柔軟性です。従来の電子メールやパスワードなどの資格情報ベースの認証に加えて、Google、Facebook、Twitter などのさまざまな認証プロバイダーの統合が可能になります。

資格情報認証の実装

資格情報認証は、認証プロセスとユーザー資格情報の保存を完全に制御する必要があるアプリケーション、または外部認証プロバイダーに依存したくない場合に特に役立ちます。

セットアップ

  1. 次のコマンドを使用して新しい Next.js プロジェクトを作成し、示された手順に従います。 TypeScript とsrc/フォルダーを使用します。


 npx create-next-app@latest


2. プロジェクトに必要な依存関係をインストールします。今回はpnpm使用します。選択したパッケージ マネージャーを使用できます。


 pnpm install next-auth prisma react-hook-form zod, bcrypt


UI には Shadcn/ui を使用します。


 pnpm dlx shadcn-ui@latest init


  • プリズマ:はオープンソースのデータベース ツールキットです。ユーザーの認証情報を保存するために使用します。

  • 次の認証: Next.js の認証。

  • 反応フックフォーム: React でフォームを検証するのに役立つライブラリ。

  • ゾッド:データバリデーター。

  • bcrypt :パスワードをハッシュ化します。

  • シャドン/UI : 再利用可能なコンポーネントのコレクション。


3. プロジェクトに次の構造を作成します。


 ... ├── prisma/ ... ├── src/ │ ├── actions/ │ │ └── auth-actions.tsx │ ├── app/ │ │ ├── api/auth/[...nextauth] │ │ │ └── route.ts │ │ ├── auth/ │ │ │ ├── signin │ │ │ │ └── page.tsx │ │ │ └── signup │ │ │ └── page.tsx │ │ │ ... │ ├── components/ │ │ ├── auth/ │ │ │ ├── auth-buttons.tsx │ │ │ ├── signin-form.tsx │ │ │ ├── signup-form.tsx │ │ │ └── user-nav.ts │ │ ├── ui/ │ │ │ ... │ │ ├── auth-provider.tsx │ │ ├── icons.tsx │ │ └── theme-provider.tsx │ ├── lib/ │ │ ├── prisma.ts │ │ ├── types.d.ts │ │ └── utils.ts │ ... ...


プリズマのセットアップ

Prisma を使用して、データベースにユーザーを保存および取得します。 Prisma では、さまざまな種類のデータベースを統合できるため、必要なデータベースを使用できます。ここでは SQLite を使用します。


  1. Prisma を初期化しています


 npx prisma init --datasource-provider sqlite


これにより、スキーマを含む prism フォルダーが作成されます。


  1. モデルの作成。

モデルを作成するには、 @auth/prisma-adapterによって提供されるモデルを使用し、次のように少しカスタマイズします。


prisma/schema.prisma :


 generator client { provider = "prisma-client-js" output = "../../node_modules/.prisma/client" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } ... model User { id String @id @default(cuid()) username String password String email String @unique emailVerified DateTime? phone String? image String? } ...


3. 最初の作成移住


 npx prisma migrate dev --name first-migration


このコマンドにより、Prisma フォルダーにさらに多くのファイルが作成され、データベースがモデルと同期されました。


4. プリズマクライアント

最後に、Prisma クライアントを作成します。


lib/prisma.ts :


 import { PrismaClient } from "@prisma/client"; const globalForPrisma = global as unknown as { prisma: PrismaClient; }; export const prisma = globalForPrisma.prisma || new PrismaClient(); if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; export default prisma;


NextAuth.js のセットアップ

  1. env変数を作成します。


 # Secret key for NextAuth.js, used for encryption and session security. It should be a long, # random string unique to your application. NEXTAUTH_SECRET=XXX3B2CC28F123456C6934531CXXXXX # Base URL for your Next.js app, used by NextAuth.js for redirects and callbacks. NEXTAUTH_URL=http://localhost:3000/


  1. 認証ルートを作成します。

このパスにより、単一のエンドポイントですべての認証リクエスト (ログイン、ログアウト、ベンダー コールバックなど) を処理できるようになります。


src/app/api/auth/[...nextauth]


  1. プロバイダーを作成します。


 ... // Imports the Prisma User type for typing. import { User } from '@prisma/client' // Configuration of authentication options for NextAuth. export const authOptions: AuthOptions = { ... // Defines authentication providers, in this case, only credentials. providers: [ CredentialsProvider({ name: 'Credentials', // Defines the required fields for authentication. credentials: { email: { label: 'Email', type: 'text' }, password: { label: 'Password', type: 'password' }, }, // Function to authenticate the user with the provided credentials. async authorize(credentials) { // Searches for the user in the database by email. const user = await prisma.user.findUnique({ where: { email: credentials?.email, }, }) // Checks if the user exists and if the password is correct. if (!user) throw new Error('User name or password is not correct') if (!credentials?.password) throw new Error('Please Provide Your Password') const isPasswordCorrect = await bcrypt.compare(credentials.password, user.password) if (!isPasswordCorrect) throw new Error('User name or password is not correct') // Returns the user without including the password. const { password, ...userWithoutPass } = user return userWithoutPass }, }), ], } // Exports the configured NextAuth handler to handle GET and POST requests. const handler = NextAuth(authOptions) export { handler as GET, handler as POST }


4. 認証プロバイダーを作成します。


src/components/auth-provider.tsx :


 'use client' import { SessionProvider } from 'next-auth/react' export default function AuthProvider({ children }: { children: React.ReactNode }) { return <SessionProvider>{children}</SessionProvider> }


このコンポーネントは、認証に NextAuth を使用する Next.js アプリケーションのセッション プロバイダーとして機能します。


このプロバイダーでコンポーネントまたはページをラップすると、セッション コンテキストへのアクセスが許可され、子コンポーネントが NextAuth フックやuseSessionなどの機能を使用して、ユーザーの現在のセッションの状態にアクセスまたは変更できるようになります。


src/app/layout.tsx :


 /* All imports */ export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang='en' suppressHydrationWarning > <body className={`${inter.className} relative`}> <AuthProvider> <main>{children}</main> </AuthProvider> </body> </html> ) }


Shadcn/ui を使用した UI のセットアップ

  1. 次の手順に従って shadcn/ui をインストールします。文書化 (ステップ 2)


 Would you like to use TypeScript (recommended)? yes Which style would you like to use? › Default Which color would you like to use as base color? › Slate Where is your global CSS file? › src/app/globals.css Do you want to use CSS variables for colors? › yes Are you using a custom tailwind prefix eg. tw-? Leave blank Where is your tailwind.config.js located? › tailwind.config.js Configure the import alias for components: › @/components Configure the import alias for utils: › @/lib/utils Are you using React Server Components? › yes


お好みのテーマを使用できます。


2. 実装ダークモード


src/app/layout.tsx :


 /* All imports */ export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang='en' suppressHydrationWarning > <body className={`${inter.className} relative`}> <AuthProvider> <ThemeProvider attribute='class' defaultTheme='dark' enableSystem disableTransitionOnChange > <main>{children}</main> <Toaster /> </ThemeProvider> </AuthProvider> </body> </html> ) }


2. 次の shadcn/ui コンポーネントをインストールします。


 pnpm dlx shadcn-ui@latest add avatar button dropdown-menu form input label tabs toast


認証コンポーネントの作成

src/components/auth-buttons.tsx :


 'use client' import Link from 'next/link' import { signIn, useSession } from 'next-auth/react' import { Button } from '../ui/button' import { UserNav } from './user-nav' export default function AuthButtons() { // Use the useSession hook to access session data const { data: session } = useSession() return ( <div className='flex justify-end gap-4'> {session && session.user ? ( <UserNav user={session.user} /> ) : ( <> <Button size={'sm'} variant={'secondary'} onClick={() => signIn()} > Sign In </Button> <Button size={'sm'} asChild className='text-foreground' > <Link href='/auth/signup'>Sign Up</Link> </Button> </> )} </div> ) }


このコンポーネントは、ユーザーのセッション ステータスに基づいて認証オプションを動的に表示します。ユーザーがログインしている場合は、ユーザー固有のナビゲーションが表示されます。それ以外の場合は、スムーズなユーザー エクスペリエンスのために Next.js のルーティングと NextAuth の認証機能を利用して、サインインまたはサインアップするためのボタンが提供されます。


ホームページを変更し、認証ボタンを追加します。見た目はこんな感じです。


ホームページ

src/components/auth/signup-form.tsx :


 'use client' /* all imports */ // Function to register a new user import { registerUser } from '@/actions/auth-actions' // Define the validation schema for the signup form using Zod const formSchema = z .object({ username: z .string({ required_error: 'Username is required', }) .min(2, 'User name must have at least 2 characters') .max(12, 'Username must be up to 12 characters') .regex(new RegExp('^[a-zA-Z0-9]+$'), 'No special characters allowed!'), email: z.string({ required_error: 'Email is required' }).email('Please enter a valid email address'), password: z .string({ required_error: 'Password is required' }) .min(6, 'Password must have at least 6 characters') .max(20, 'Password must be up to 20 characters'), confirmPassword: z .string({ required_error: 'Confirm your password is required' }) .min(6, 'Password must have at least 6 characters') .max(20, 'Password must be up to 20 characters'), }) .refine(values => values.password === values.confirmPassword, { message: "Password and Confirm Password doesn't match!", path: ['confirmPassword'], }) // Type inference for form inputs based on the Zod schema type InputType = z.infer<typeof formSchema> export function SignUpForm() { const [isLoading, setIsLoading] = useState(false) const { toast } = useToast() // Hook to show toast notifications // Initialize form handling with React Hook Form and Zod for validation const form = useForm<InputType>({ resolver: zodResolver(formSchema), }) // Handles form submission async function onSubmit(values: InputType) { try { setIsLoading(true) const { confirmPassword, ...user } = values // Exclude confirmPassword from data to be sent const response = await registerUser(user) // Register the user if ('error' in response) { toast({ title: 'Something went wrong!', description: response.error, variant: 'success', }) } else { toast({ title: 'Account Created!', description: 'Your account has been created successfully! You can now login.', }) } } catch (error) { console.error(error) toast({ title: 'Something went wrong!', description: "We couldn't create your account. Please try again later!", variant: 'destructive', }) } finally { setIsLoading(false) } } return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <div className='grid gap-2'> // Each FormField validates and displays an input <FormField control={form.control} name='username' render={({ field }) => ( <FormItem> <FormControl> <div className='flex items-center gap-2'> <Icons.user className={`${form.formState.errors.username ? 'text-destructive' : 'text-muted-foreground'} `} /> <Input placeholder='Your Username' className={`${form.formState.errors.username && 'border-destructive bg-destructive/30'}`} {...field} /> </div> </FormControl> <FormMessage /> </FormItem> )} /> // Repeated structure for email, password, and confirmPassword with respective validations and icons <Button className='text-foreground mt-4' disabled={isLoading} // Disable button during form submission > {isLoading && <Icons.spinner className='mr-2 h-4 w-4 animate-spin' />} // Show loading icon if isLoading is true Sign Up </Button> </div> </form> </Form> ) }


このコンポーネントはユーザー登録フォームをカプセル化し、フォームの状態管理に反応フックフォームを、スキーマ検証に Zod を利用します。


これはこれらのテクノロジに関するチュートリアルではありませんが、それらの基本を使用しているだけです。さらに質問がある場合は、ドキュメントを参照してください。


ページにさらにいくつかのスタイルを追加すると、次のようになります。


サインアップページ



src/actions/auth-action/ts :


 'use server' /* all imports */ export async function registerUser(user: Omit<User, 'id' | 'phone' | 'emailVerified' | 'image'>) { try { // Attempt to create a new user record in the database const result = await prisma.user.create({ data: { ...user, // Hash the password before storing it password: await bcrypt.hash(user.password, 10), }, }) return result } catch (error) { console.log(error) // Handle known request errors from Prisma if (error instanceof Prisma.PrismaClientKnownRequestError) { // Check for unique constraint failure (eg, email already exists) if (error.code === 'P2002') { return { error: 'Email already exists.' } } } // Return a generic error message for any other errors return { error: 'An unexpected error occurred.' } } }


registerUser関数は、 idphoneemailVerifiedimageなどのフィールドを除く、提供されたユーザー情報を使用してデータベースにレコードを作成することにより、新しいユーザーを安全に登録するように設計されています。


bcrypt を使用してユーザーのパスワードをハッシュし、安全に保管します。


サインアップをテストし、ユーザーが正しく登録していることを検証するには、いくつかのコールバックを追加する必要があります。これらは、認証とセッション管理の動作をカスタマイズできる機能です。


src/app/api/auth/[...nextauth] :


 ... export const authOptions: AuthOptions = { // Define custom pages for authentication flow pages: { signIn: '/auth/signin', // Custom sign-in page }, // Configure session management to use JSON Web Tokens (JWT) session: { strategy: 'jwt', }, // JWT configuration, including secret for token signing jwt: { secret: process.env.NEXTAUTH_SECRET, // Secret used to sign the JWT, stored in environment variables }, ... // Callbacks for customizing JWT and session behaviors callbacks: { // Callback to modify the JWT content. Adds user information if available. async jwt({ token, user }) { if (user) token.user = user as User // Cast user object to User type and assign to token return token }, // Callback to modify session content. Adds user information to the session. async session({ token, session }) { session.user = token.user // Assign user information from token to session return session }, }, } ...


Callback jwt : このコールバックは、認証ライフサイクル中に JSON Web Token (JWT) が作成または更新されるたびに実行されます。これにより、トークンが署名されてクライアントに送信されたり、サーバーに保存されたりする前に、トークンの内容を変更できます。


これは、アプリケーション ロジックに関連する可能性のある追加情報をトークンに追加する場合に役立ちます。


コールバックsession : このコールバックは、サーバー側のレンダリング中や保護された API リクエストなど、セッション データが読み取られるたびに呼び出されます。これにより、セッション データをクライアントに送信する前に変更できます。


これは、JWT に保存されている情報やその他の基準に基づいてセッション データを追加または変更する場合に特に役立ちます。


最後に、NextAuth SessionJWTタイプの定義を拡張して、追加のユーザー情報を含める必要があります。


src/lib/types.d.ts :


 import { User } from '@prisma/client' declare module 'next-auth' { interface Session { user: User } } declare module 'next-auth/jwt' { interface JWT { user: User } }


ここで、フォームに記入して送信すると、成功のトーストが表示されるようになります。ユーザーがデータベースに保存されたことを確認するには、次のコマンドを使用して、Prisma によって作成されたテーブルをグラフィカルに確認できます。


 nxp prisma studio


次のルートを利用できるようになりますhttp://localhost:5555


プリズマスタジオ


src/components/auth/user-nav.tsx :


 /* all imports */ interface Props { user: User // Expect a user object of type User from Prisma client } export function UserNav({ user }: Props) { return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant='ghost' className='relative h-8 w-8 rounded-full' > <Avatar className='h-9 w-9'> <AvatarImage src='/img/avatars/01.png' alt='' /> <AvatarFallback>UU</AvatarFallback> </Avatar> </Button> </DropdownMenuTrigger> <DropdownMenuContent className='w-56' align='end' forceMount > <DropdownMenuLabel className='font-normal'> <div className='flex flex-col space-y-1'> <p className='text-sm font-medium leading-none'>{user.username}</p> <p className='text-xs leading-none text-muted-foreground'>{user.email}</p> </div> </DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem> <Link href={'/api/auth/signout'} // Link to the signout API route className='w-full' > Sign Out </Link> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ) } 


ホームページ



src/components/auth/signin-form.tsx :


 /* all imports */ // Schema definition for form validation using Zod const formSchema = z.object({ email: z.string({ required_error: 'Please enter your email' }).email('Please enter a valid email address'), password: z.string({ required_error: 'Please enter your password', }), }) // Type inference for form inputs based on the Zod schema type InputType = z.infer<typeof formSchema> // Props definition, optionally including a callback URL interface Props { callbackUrl?: string } export function SignInForm({ callbackUrl }: Props) { const [isLoading, setIsLoading] = useState(false) const { toast } = useToast() const router = useRouter() // Hook to control routing const form = useForm<InputType>({ resolver: zodResolver(formSchema), // Set up Zod as the form validation resolver }) // Function to handle form submission async function onSubmit(values: InputType) { try { setIsLoading(true) // Attempt to sign in using the 'credentials' provider const response = await signIn('credentials', { redirect: false, // Prevent automatic redirection email: values.email, password: values.password, }) // Handle unsuccessful sign in attempts if (!response?.ok) { toast({ title: 'Something went wrong!', description: response?.error, variant: 'destructive', }) return } toast({ title: 'Welcome back! ', description: 'Redirecting you to your dashboard!', }) router.push(callbackUrl ? callbackUrl : '/') // Redirect to the callback URL or home page } catch (error) { toast({ title: 'Something went wrong!', description: "We couldn't create your account. Please try again later!", variant: 'destructive', }) } finally { setIsLoading(false) } } return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <div className='grid gap-2'> <div className='grid gap-1'> <FormField control={form.control} name='email' render={({ field }) => ( <FormItem> <FormControl> <div className='flex items-center gap-2'> <Icons.email className={`${form.formState.errors.email ? 'text-destructive' : 'text-muted-foreground'} `}/> <Input type='email' placeholder='Your Email' className={`${form.formState.errors.email && 'border-destructive bg-destructive/30'}`} {...field} /> </div> </FormControl> <FormMessage /> </FormItem> )} /> {/* Password field */} {/* Similar structure to email field, customized for password input */} </div> <Button className='text-foreground mt-4' disabled={isLoading} // Disable button while loading > {isLoading && <Icons.spinner className='mr-2 h-4 w-4 animate-spin' />} // Show loading spinner when processing Sign In </Button> </div> </form> </Form> ) } 


サインインページ


🎉 完成しました!


NextAuth.js を使用した基本認証の実装が完了しました。完全な認証システムを構築するにはやるべきことがまだたくさんありますが、それらについては次のチュートリアルで説明します。


🔗レポはこちら


結論

要約すると、NextAuth を使用して Next.js で認証システムを実装およびカスタマイズする方法、ユーザー管理を強化するためにセッションと JWT を拡張する方法、react-hook-form と Zod を使用して効果的な検証でフォームを処理する方法を検討しました。


著者とつながりたいですか?


世界中の友達とつながるのが大好きです𝕏


ここでも公開されています