🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)
🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)

🔧 Programmierung 🕛 kürzlich 15 Min Lesezeit
0

Two-Factor Authentication Using Better_Auth, Next.js, Prisma, ShadCN, and Resend

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

In this guide, we will set up Two-Factor Authentication (2FA) for a modern web application. Using a powerful stack of tools and frameworks, you'll implement a robust and secure email-based 2FA system seamlessly integrated into your full-stack application.



By the end of this guide, you'll have a fully functional system that enables users to verify their identity using a one-time password (OTP) for enhanced security.






Tech Stack



Here’s the technology stack we’ll be using:





  • : A powerful React framework for building server-rendered applications.


  • : A utility-first component library for rapid UI development.


  • : A reliable email service for sending OTPs.






Prerequisites



Before proceeding, ensure you have the following ready:





  1. Node.js (LTS version) installed.

  2. A package manager like npm, yarn, or pnpm (we'll use pnpm in this guide).

  3. A PostgreSQL database instance (local or hosted, such as Supabase or PlanetScale).


    • If you're working locally, Docker is a great way to set this up.



  4. Familiarity with TypeScript, Next.js, and Prisma.






Cloning the Starter Project:



This guide builds upon existing functionality, including email-password authentication and email verification. You can either start from scratch by referring to the or clone the project with the following command:




CODE
git clone -b feat-reset-password https://github.com/Daanish2003/better_auth_nextjs.git  






Navigate to the project directory and install dependencies:




CODE
pnpm install  









Configure the .env File



Create a .env file in the root of your project and add the following configuration:




CODE
# Authentication settings
BETTER_AUTH_SECRET="your-secret-key" # Replace with a secure key
BETTER_AUTH_URL="http://localhost:3000"
NEXT_PUBLIC_APP_URL="http://localhost:3000"

# Database settings
POSTGRES_PASSWORD="your-password"
POSTGRES_USER="your-username"
DATABASE_URL="postgresql://your-username:your-password@localhost:5432/mydb?schema=public"

# Resend API Key
RESEND_API_KEY="your-resend-api-key"






If you're using Docker for PostgreSQL, run:




CODE
docker compose up -d









Step 1: Install ShadCN Components



ShadCN simplifies UI development with pre-built, customizable components. Install the required components:




CODE
pnpx shadcn@latest add dialog switch input-otp









Step 2: Update Prisma Schema



Extend the prisma/schema.prisma file to include a TwoFactor model for 2FA data:




CODE
model User {
// note just update it don't replace it
twoFactor TwoFactor[] // Add the twoFactor column to user table
}

model TwoFactor {
id String @id @default(cuid())
secret String
backupCodes String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@map("twoFactor")
}






Run the following commands to update the database schema:




CODE
pnpm prisma generate
pnpm prisma migrate dev --name add-two-factor









Step 3: Integrate 2FA Using Better_Auth






Backend Configuration



Update lib/auth.ts to include the twoFactor plugin:




CODE
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";

// Configure the authentication module
export const auth = betterAuth({
// Additional authentication options can go here

// Register plugins for enhanced functionality
plugins: [
// Two-Factor Authentication (2FA) plugin configuration
twoFactor({
// Configuration options for OTP (One-Time Password) handling
otpOptions: {
/**
* Defines how OTPs are sent to users.
* In this case, we are using the Resend service to send OTP emails.
*
* @param user - The user object containing user details like email.
* @param otp - The one-time password to be sent to the user.
*/

async sendOTP({ user, otp }) {
await resend.emails.send({
from: 'Acme <[email protected]>', // Sender email and display name
to: user.email, // Recipient's email address
subject: "Two-Factor Authentication (2FA)", // Email subject
html: `Your OTP is <b>${otp}</b>`, // Email content with the OTP
});
},
},

// Bypass OTP verification during 2FA setup (for a smoother user experience)
skipVerificationOnEnable: true,
}),
],
});










Frontend Configuration



In lib/auth-client.ts, add the 2FA client plugin:




CODE
import { createAuthClient } from "better-auth/client";
import { twoFactorClient } from "better-auth/plugins";

// Create an instance of the authentication client
export const authClient = createAuthClient({
// The base URL of the application
baseURL: process.env.NEXT_PUBLIC_APP_URL,
/**
* Explanation of `baseURL`:
* - Used to define the base API endpoint for authentication requests.
* - The value is fetched from an environment variable (`NEXT_PUBLIC_APP_URL`)
* to maintain flexibility across different environments (e.g., development, staging, production).
*/


// Register plugins to extend the functionality of the authentication client
plugins: [
twoFactorClient(),
/**
* `twoFactorClient` Plugin:
* - Enables support for Two-Factor Authentication (2FA) on the client side.
* - Handles 2FA-related tasks, such as requesting OTP verification.
*/

],
});










Step 4: Build the UI for 2FA






Create Zod Validation Schema



Create a password-schema.ts file under src/helpers/zod/:




CODE
import { z } from "zod";

// Define a schema for validating password input
export const PasswordSchema = z.object({
password: z
.string() // Ensures the input is a string
.min(8, { message: "Password must be at least 8 characters long" })
/**
* Validates that the password has a minimum length of 8 characters.
* Custom error message provided to guide the user.
*/

.max(20, { message: "Password must be at most 20 characters long" }),
/**
* Validates that the password does not exceed 20 characters.
* Ensures user-friendly error messaging for invalid input.
*/

});










Add Settings Component



Add the Settings component under components/auth to handle enabling/disabling 2FA. Use the code provided earlier.




CODE
"use client";

import React, { useState } from "react";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
import { Button } from "../ui/button";
import { Card, CardDescription, CardHeader, CardTitle } from "../ui/card";
import { Switch } from "../ui/switch";
import { authClient, useSession } from "@/lib/auth-client";
import { Input } from "../ui/input";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "../ui/form";
import { useForm } from "react-hook-form";
import { PasswordSchema } from "@/helpers/zod/password-schema";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { FormSuccess } from "../form-success";
import FormError from "../form-error";
import { useAuthState } from "@/hooks/useAuthState";
import { Settings as UserSettings } from "lucide-react";

const Settings = () => {
const { data } = useSession(); // Fetch session data to check if 2FA is enabled
const [open, setOpen] = useState<boolean>(false); // Manages the dialog open/close state
const { error, success, loading, setLoading, setSuccess, setError, resetState } = useAuthState();

// React Hook Form setup for password validation
const form = useForm<z.infer<typeof PasswordSchema>>({
resolver: zodResolver(PasswordSchema), // Resolves validation rules using Zod
defaultValues: {
password: "", // Default value for the password field
},
});

// If two-factor authentication status is unknown, do not render the component
if (data?.user.twoFactorEnabled === null) {
return null;
}

/**
* Handles form submission for enabling/disabling two-factor authentication.
* @param values - The form data, including the user's password
*/

const onSubmit = async (values: z.infer<typeof PasswordSchema>) => {
if (data?.user.twoFactorEnabled === false) {
// Enable 2FA
await authClient.twoFactor.enable(
{ password: values.password },
{
onRequest: () => {
resetState();
setLoading(true); // Show loading state during request
},
onResponse: () => {
setLoading(false); // Stop loading after response
},
onSuccess: () => {
setSuccess("Enabled two-factor authentication"); // Success message
setTimeout(() => {
setOpen(false); // Close dialog
resetState();
form.reset(); // Clear form fields
}, 1000); // Delay to allow user to see the success message
},
onError: (ctx) => {
setError(ctx.error.message); // Display error message
},
}
);
}

if (data?.user.twoFactorEnabled === true) {
// Disable 2FA
await authClient.twoFactor.disable(
{ password: values.password },
{
onRequest: () => {
resetState();
setLoading(true); // Show loading state during request
},
onResponse: () => {
setLoading(false); // Stop loading after response
},
onSuccess: () => {
setSuccess("Disabled two-factor authentication"); // Success message
setTimeout(() => {
setOpen(false); // Close dialog
resetState();
form.reset(); // Clear form fields
}, 1000);
},
onError: (ctx) => {
setError(ctx.error.message); // Display error message
},
}
);
}
};

return (
<>
{/* Main settings dialog */}
<Dialog>
<DialogTrigger asChild>
<Button variant={"default"}>
<UserSettings />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
<DialogDescription>Make changes to your settings here</DialogDescription>
</DialogHeader>
<Card>
<CardHeader className="p-4 flex flex-row justify-between">
<div>
<CardTitle className="text-sm">Enable 2FA</CardTitle>
<CardDescription className="text-xs">
Toggle to enable or disable two-factor authentication
</CardDescription>
</div>
{/* Toggle switch for enabling/disabling 2FA */}
<Switch
checked={data?.user.twoFactorEnabled}
onCheckedChange={() => {
setOpen(true);
}}
/>
</CardHeader>
</Card>
</DialogContent>
</Dialog>

{/* Dialog for password confirmation */}
<Dialog
open={open}
onOpenChange={() => {
setOpen(false);
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm Selection</DialogTitle>
<DialogDescription>Please enter your password to confirm the action</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
disabled={loading}
type="password"
placeholder="********"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Feedback messages */}
<FormSuccess message={success} />
<FormError message={error} />
<Button
type="submit"
className="w-full mt-4"
disabled={loading}
>
Submit
</Button>
</form>
</Form>
</DialogContent>
</Dialog>
</>
);
};

export default Settings;










Update app/page.tsx



To integrate the Settings and SignOut components into the homepage navigation, update the app/page.tsx file as shown below:




CODE
import Settings from "@/components/auth/settings";
import SignOut from "@/components/auth/sign-out";

export default function Home() {
return (
<nav className="flex items-center h-16 border-b px-12 justify-between">
{/* Application title */}
<span className="font-bold text-xl">Auth</span>
{/* Right section with actions */}
<div className="flex gap-x-2">
{/* Sign-out button */}
<SignOut />
{/* Settings dropdown */}
<Settings />
</div>
</nav>
);
}










Implement OTP Input Component



Customize input-otp.tsx for user-friendly OTP entry. Add your desired styles and logic for the OTP slots.




CODE
"use client"

import * as React from "react"
import { OTPInput, OTPInputContext } from "input-otp"
import { Dot } from "lucide-react"

import { cn } from "@/lib/utils"

const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50 justify-center", // Added justify-center to className
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
))
InputOTP.displayName = "InputOTP"

const InputOTPGroup = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ className, ...props }, ref) => (
// Added the gap-x-3 for space between the slots
<div ref={ref} className={cn("flex items-center gap-x-3", className)} {...props} />
))
InputOTPGroup.displayName = "InputOTPGroup"

const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div"> & { index: number }
>(({ index, className, ...props }, ref) => {
const inputOTPContext = React.useContext(OTPInputContext)
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]

return (
{/*Removed first:rounded-l-md last:rounded-r-md first:border-l*/}
{/*Added border rounded-md*/}
{/*Changed h-10 to h-12*/}
<div
ref={ref}
className={cn(
"relative flex h-12 w-10 items-center justify-center border border-y border-r border-input text-sm transition-all rounded-md",
isActive && "z-10 ring-2 ring-ring ring-offset-background",
className
)}
{...props}
>
{char}
{hasFakeCaret && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
</div>
)}
</div>
)
})
InputOTPSlot.displayName = "InputOTPSlot"

const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<Dot />
</div>
))
InputOTPSeparator.displayName = "InputOTPSeparator"

export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }










Create a requestOTP Helper Component



Create a requestOTP.tsfile inside the helpers/auth/ and paste the code inside it




CODE
import { authClient } from "@/lib/auth-client";

// Interface for the response returned by the OTP request
interface OTPResponse {
data?: { status: boolean } | null; // Holds response data indicating the OTP request status
error?: { message: string }; // Holds error details if the request fails
}

// Type alias for a successful response structure
type SuccessResponse = Pick<OTPResponse, "data">;

/**
* Sends a request to generate and send a one-time password (OTP).
*
* @returns {Promise<OTPResponse>} - A promise resolving to either the response data or an error object.
*/

export const requestOTP = async (): Promise<OTPResponse> => {
try {
// Send OTP request using the authClient's two-factor authentication module
const response: SuccessResponse = await authClient.twoFactor.sendOtp();

// Return the successful response object
return response;
} catch (error: unknown) {
console.error("Error requesting OTP:", error);

// Return an error object with a user-friendly message
return {
error: { message: "Failed to request OTP. Please try again." },
};
}
};










Create Two-Factor Verification Component



Create a TwoFactor component for OTP verification. Refer to the implementation provided earlier.




CODE
"use client";

import React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useRouter } from "next/navigation";

import CardWrapper from "../card-wrapper";
import { Form, FormField, FormItem, FormLabel, FormMessage } from "../ui/form";
import { InputOTP, InputOTPGroup, InputOTPSlot } from "../ui/input-otp";
import { REGEXP_ONLY_DIGITS } from "input-otp";
import FormError from "../form-error";
import { Button } from "../ui/button";
import { FormSuccess } from "../form-success";

import { authClient } from "@/lib/auth-client";
import { twoFactorSchema } from "@/helpers/zod/two-factor-schema";
import { useAuthState } from "@/hooks/useAuthState";
import { requestOTP } from "@/helpers/auth/request-otp";

const TwoFactor: React.FC = () => {
const router = useRouter();
const {
error, // Stores error message if any process fails
success, // Stores success message if a process succeeds
loading, // Indicates whether an operation is in progress
setSuccess, // Sets the success message
setError, // Sets the error message
setLoading, // Toggles the loading state
resetState, // Resets all success, error, and loading states
} = useAuthState();

const form = useForm<z.infer<typeof twoFactorSchema>>({
mode: "onBlur", // Triggers validation on blur
resolver: zodResolver(twoFactorSchema), // Uses Zod schema for validation
defaultValues: { code: "" }, // Initializes the OTP code as an empty string
});

/**
* Requests a new OTP to be sent to the user's email.
*/

const handleResendOTP = async () => {
resetState(); // Clear previous error/success messages
setLoading(true); // Start loading state

try {
const response = await requestOTP(); // Makes a request to send OTP

if (response?.data) {
setSuccess("OTP has been sent to your email."); // Notify user on success
} else if (response?.error) {
setError(response.error.message); // Show error message if request fails
}
} catch (err) {
console.error("Error requesting OTP:", err);
setError("Something went wrong. Please try again."); // Catch unexpected errors
} finally {
setLoading(false); // End loading state
}
};

/**
* Handles the submission of the OTP code for verification.
* @param values - The form values containing the OTP code.
*/

const handleSubmit = async (values: z.infer<typeof twoFactorSchema>) => {
resetState(); // Clear previous error/success messages
setLoading(true); // Start loading state

try {
// Verify OTP using Better_Auth client
await authClient.twoFactor.verifyOtp(
{ code: values.code }, // Pass the entered OTP code
{
onRequest: () => setLoading(true), // Start loading when request begins
onResponse: () => setLoading(false), // End loading after response
onSuccess: () => {
setSuccess("OTP validated successfully."); // Notify user of success
router.replace("/"); // Redirect to the homepage
},
onError: (ctx) => setError(ctx.error.message), // Show error message if verification fails
}
);
} catch (err) {
console.error("Error verifying OTP:", err);
setError("Unable to verify OTP. Please try again."); // Catch unexpected errors
} finally {
setLoading(false); // End loading state
}
};

return (
<CardWrapper
cardTitle="Two-Factor Authentication"
cardDescription="Verify your identity with a one-time password." // Instructional text
cardFooterDescription="Entered the wrong email?" // Text for footer message
cardFooterLink="/login" // Link to the login page
cardFooterLinkTitle="Login" // Footer link text
>
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
{/* Form field for entering the OTP */}
<FormField
control={form.control}
name="code"
render={({ field }) => (
<FormItem>
<FormLabel>One-Time Password</FormLabel>
{/* OTP Input field accepting only digits */}
<InputOTP
maxLength={6} // Maximum of 6 digits
pattern={REGEXP_ONLY_DIGITS} // Allows only numeric input
{...field}
disabled={loading} // Disables input during loading
>
<InputOTPGroup>
{Array.from({ length: 6 }).map((_, index) => (
<InputOTPSlot key={index} index={index} />
))}
</InputOTPGroup>
</InputOTP>
<FormMessage />
</FormItem>
)}
/>
{/* Button for resending the OTP */}
<Button
onClick={handleResendOTP}
variant="link"
className="text-xs underline ml-60" // Align to the right
disabled={loading} // Disabled during loading
>
Resend OTP
</Button>
<FormError message={error} /> {/* Displays error messages */}
<FormSuccess message={success} /> {/* Displays success messages */}
{/* Button to submit the OTP */}
<Button
type="submit"
disabled={loading} // Disabled during loading
className="w-full mt-4" // Full-width button with margin
>
Verify
</Button>
</form>
</Form>
</CardWrapper>
);
};

export default TwoFactor;










Create TwoFactorPage



Create a folder named two-factor inside /app/(auth)/ folder and then create page.tsx and import two-factor component inside page.tsx




CODE
import TwoFactor from '@/components/auth/two-factor'
import React from 'react'
const TwoFactorPage = () => {
return (
<TwoFactor />
)
}
export default TwoFactorPage









Step 5: Run Your Application



Now that your setup is complete, it's time to test your implementation. Follow these steps to verify the two-factor authentication system:





  1. Start the development server:


    CODE
    pnpm dev



  2. Access your application at http://localhost:3000/signin.



  3. Sign In:



    Log into your account using your email and password.




  4. Enable Two-Factor Authentication (2FA):




    • Navigate to Settings in your account dashboard.

    • Toggle the Enable 2FA option.

    • Enter your password to confirm and enable two-factor authentication.




  5. Test the Logout and Login Flow:




    • Log out of your account.

    • Try logging back in with your email and password.

    • Check your email for the OTP sent as part of the 2FA process.




  6. Validate OTP:




    • Enter the OTP in the provided input field on the 2FA page.

    • Verify that the system accepts the OTP and logs you into your account successfully.




  7. Disable Two-Factor Authentication (2FA):




    • Navigate to Settings in your account dashboard.

    • Toggle the Disable 2FA option.

    • Enter your password to confirm and enable two-factor authentication.








ScreenShots:










Conclusion



Congratulations! 🎉 You’ve successfully implemented Two-Factor Authentication using Better_Auth, Next.js, Prisma, ShadCN, and Resend. This setup provides an extra layer of security to your application, enhancing user trust and safeguarding their accounts.



Feel free to explore additional features, such as backup codes or app-based OTPs, to further strengthen your 2FA system.



Happy coding!






Reference Links:



Forgot and ResetPassword using BetterAuth:



Email And Password with Better_Auth:



Better_Auth Docs: 



Docker Docs: 



Shadcn Docs: 



Tailwindcss Docs: 

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Hackers Just Poisoned the Rust Supply Chain | Threat Wire
1 Quelle
Hackers Found a Way Into Humanoid Robots | Threat Wire
1 Quelle
Bits und so #1021 (Passwort für Laufwerk)
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Two-Factor Authentication Using Better_Auth, Next.js, Prisma, ShadCN, and Resend

Thematisch verwandte Begriffe: TwoFactor, Authentication, Using, BetterAuth · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...