paint-brush
Comment implémenter l'authentification dans Next.js 14 avec NextAuth.js, Shadcn/ui, React-hook-form et Zodpar@ljaviertovar
2,053 lectures
2,053 lectures

Comment implémenter l'authentification dans Next.js 14 avec NextAuth.js, Shadcn/ui, React-hook-form et Zod

par L Javier Tovar19m2024/03/01
Read on Terminal Reader

Trop long; Pour lire

Au sein de l'écosystème JavaScript et, plus particulièrement, dans les applications développées avec Next.js, l'une des bibliothèques les plus importantes pour gérer l'authentification est NextAuth.js. Cet outil offre une solution simple et facile à mettre en œuvre pour ajouter une authentification à nos applications. La meilleure chose est sa flexibilité ; il permet l'intégration de différents fournisseurs d'authentification tels que Google, Facebook et Twitter, en plus de l'authentification basée sur les identifiants, comme l'e-mail classique et le mot de passe.
featured image - Comment implémenter l'authentification dans Next.js 14 avec NextAuth.js, Shadcn/ui, React-hook-form et Zod
L Javier Tovar HackerNoon profile picture
0-item
1-item

Un guide complet : informations d'identification (e-mail et mot de passe)

Garantir la sécurité et la confidentialité des utilisateurs est plus important que jamais. L'authentification Web joue un rôle crucial à cet égard, servant de première ligne de défense pour protéger les informations et les données des utilisateurs.


Aujourd'hui, nous disposons d'outils comme NextAuth.js qui facilitent grandement notre travail, nous permettant d'implémenter facilement différents types d'authentification dans nos applications Next.js.


Dans cette série de tutoriels, nous allons construire un système d'authentification complet dans Next.js 14, en commençant par les bases : l'authentification par email et mot de passe.

Qu'est-ce que NextAuth.js (Auth.js) ?

Au sein de l'écosystème JavaScript et, plus particulièrement, dans les applications développées avec Next.js, l'une des bibliothèques les plus importantes pour gérer l'authentification est NextAuth.js.


Cet outil propose une solution simple et facile à mettre en œuvre pour ajouter une authentification à nos applications. La meilleure chose est sa flexibilité ; il permet l'intégration de différents fournisseurs d'authentification tels que Google, Facebook et Twitter, en plus de l'authentification basée sur les identifiants, comme l'e-mail classique et le mot de passe.

Implémentation de l'authentification des informations d'identification

L'authentification par informations d'identification est particulièrement utile dans les applications où vous avez besoin d'un contrôle total sur le processus d'authentification et le stockage des informations d'identification des utilisateurs, ou lorsque vous ne souhaitez pas dépendre de fournisseurs d'authentification externes.

Configuration

  1. Créez un nouveau projet Next.js avec la commande suivante et suivez les étapes indiquées. Nous utiliserons TypeScript et le dossier src/ .


 npx create-next-app@latest


2. Installez les dépendances nécessaires au projet. Cette fois, nous utiliserons pnpm ; vous pouvez utiliser le gestionnaire de paquets de votre choix.


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


Pour l'interface utilisateur, nous utiliserons Shadcn/ui.


 pnpm dlx shadcn-ui@latest init


  • prisme : est une boîte à outils de base de données open source. Nous l'utiliserons pour stocker les informations d'identification des utilisateurs.

  • authentification suivante : Authentification pour Next.js.

  • forme de crochet de réaction : une bibliothèque qui vous aide à valider les formulaires dans React.

  • zod : un validateur de données.

  • bcrypt : pour hacher les mots de passe.

  • shadcn/ui : une collection de composants réutilisables.


3. Créez la structure suivante pour le projet :


 ... ├── 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 │ ... ...


Configuration de Prisma

Nous utiliserons Prisma pour stocker et récupérer les utilisateurs dans la base de données. Prisma permet l'intégration de différents types de bases de données afin que vous puissiez utiliser n'importe quelle base de données dont vous avez besoin, nous utiliserons SQLite.


  1. Initialisation de Prisma .


 npx prisma init --datasource-provider sqlite


Cela crée le dossier prism avec ses schémas.


  1. Création des modèles.

Pour créer les modèles, nous utiliserons ceux fournis par @auth/prisma-adapter et les personnaliserons un peu comme suit :


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. Créer le premier migration .


 npx prisma migrate dev --name first-migration


Avec cette commande, davantage de fichiers ont été créés dans le dossier Prisma et la base de données a été synchronisée avec les modèles.


4. Client Prisma

Enfin, nous créons un client 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;


Configuration de NextAuth.js

  1. Création de variables 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. Création de la route d'authentification.

Ce chemin permet de gérer toutes les demandes d'authentification (telles que la connexion, la déconnexion et les rappels du fournisseur) sur un seul point de terminaison.


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


  1. Création des fournisseurs.


 ... // 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. Création du fournisseur d'authentification.


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> }


Ce composant agit en tant que fournisseur de session pour les applications Next.js qui utilisent NextAuth pour l'authentification.


L'encapsulation de composants ou de pages dans ce fournisseur leur donne accès au contexte de session, permettant aux composants enfants d'utiliser les hooks et les fonctionnalités NextAuth, tels que useSession , pour accéder ou modifier l'état de la session actuelle de l'utilisateur.


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> ) }


Configuration de l'interface utilisateur avec Shadcn/ui

  1. Installation de shadcn/ui après documentation (étape 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


Vous pouvez utiliser le thème de votre choix.


2. Mise en œuvre Mode sombre


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. Installation des composants shadcn/ui suivants :


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


Création de composants d'authentification

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> ) }


Ce composant affiche dynamiquement les options d'authentification en fonction de l'état de la session de l'utilisateur. Si l'utilisateur est connecté, la navigation spécifique à l'utilisateur s'affiche. Sinon, il propose des boutons pour se connecter ou s'inscrire, utilisant le routage de Next.js et les capacités d'authentification de NextAuth pour une expérience utilisateur fluide.


Modifiez la page d'accueil et ajoutez les boutons d'authentification. Voici à quoi ça ressemble :


Page d'accueil

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> ) }


Ce composant encapsule un formulaire d'inscription utilisateur, utilisant le formulaire de réaction-hook pour la gestion de l'état du formulaire et Zod pour la validation du schéma.


Bien qu'il ne s'agisse pas d'un tutoriel sur ces technologies, nous n'en utilisons que les bases, si vous avez d'autres questions, vous pouvez consulter leur documentation.


J'ai ajouté quelques styles supplémentaires à la page, et cela ressemble à ceci :


Page d'inscription



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.' } } }


La fonction registerUser est conçue pour enregistrer en toute sécurité un nouvel utilisateur en créant un enregistrement dans la base de données avec les informations utilisateur fournies, à l'exclusion des champs tels que id , phone , emailVerified et image .


Il utilise bcrypt pour hacher le mot de passe de l'utilisateur pour un stockage sécurisé.


Pour tester notre inscription et valider que l'utilisateur s'inscrit correctement, nous devons ajouter quelques rappels ; ce sont des fonctions qui permettent de personnaliser le comportement de l'authentification et de la gestion des sessions.


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 : ce rappel est exécuté chaque fois qu'un jeton Web JSON (JWT) est créé ou mis à jour pendant le cycle de vie d'authentification. Il permet de modifier le contenu du token avant qu'il ne soit signé et envoyé au client ou stocké sur le serveur.


Ceci est utile pour ajouter des informations supplémentaires au jeton, qui peuvent être pertinentes pour la logique de votre application.


session de rappel : ce rappel est appelé à chaque fois que les données de session sont lues, comme lors du rendu côté serveur ou dans les requêtes API protégées. Il permet de modifier les données de session avant leur envoi au client.


Ceci est particulièrement utile pour ajouter ou modifier des données de session en fonction des informations stockées dans le JWT ou d'autres critères.


Enfin, nous devons étendre les définitions de type NextAuth Session et JWT pour inclure des informations utilisateur supplémentaires.


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 } }


Maintenant, si nous remplissons le formulaire et le soumettons, nous pourrons voir le toast au succès. Pour vérifier que l'utilisateur a été enregistré dans la base de données, nous pouvons voir graphiquement les tables créées par Prisma avec la commande suivante :


 nxp prisma studio


Nous aurons à disposition l'itinéraire suivant http://localhost:5555


Studio Prisma


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> ) } 


Page d'accueil



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> ) } 


Page de connexion


🎉 C'est fait !


Nous avons fini d'implémenter une authentification de base avec NextAuth.js. Il reste encore beaucoup de choses à faire pour avoir un système d'authentification complet, et nous les aborderons dans les prochains tutoriels.


🔗 Repo ici


Conclusion

En résumé, nous avons exploré comment implémenter et personnaliser un système d'authentification dans Next.js à l'aide de NextAuth, comment étendre les sessions et les JWT pour enrichir la gestion des utilisateurs, et comment gérer les formulaires avec une validation efficace à l'aide de React-hook-form et Zod.


Vous souhaitez entrer en contact avec l'auteur ?


J'adore communiquer avec des amis du monde entier sur 𝕏 .


Également publié ici