In this guide, we will implement Username and Password Authentication for a modern web application using a robust and secure stack of tools. This feature will complement existing Email and Password Authentication in the application. By the end of this tutorial, your app will allow users to log in with either their username or email, with optional OTP-based Two-Factor Authentication (2FA) for enhanced security.
Tech Stack
Here’s the technology stack we’ll be using:
- Better_Auth v1: A lightweight and extensible TypeScript authentication library.
- Next.js: A powerful React framework for building server-rendered applications.
- Prisma: A modern ORM for efficient and type-safe database interaction.
- ShadCN: A utility-first component library for rapid UI development.
- TailwindCSS: A popular CSS framework for building modern user interfaces.
- Resend: A reliable email service for sending OTPs.
Prerequisites
Before proceeding, ensure you have the following ready:
- Node.js (LTS version) installed.
- A package manager like npm, yarn, or pnpm (we'll use
pnpm
in this guide). - 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.
- Familiarity with TypeScript, Next.js, and Prisma.
Cloning the Starter Project:
This guide builds upon functionality such as Email-Password Authentication and Email Verification. You can:
- Start from scratch by following these guides:
- Or, clone the starter project:
git clone -b feat-two-factor https://github.com/Daanish2003/better_auth_nextjs.git
Navigate to the project directory and install dependencies:
pnpm install
Setup
1. Configure the .env
File
Create a .env
file in the root of your project and add these configurations:
# 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, start the container:
docker compose up -d
Note:
Before we move on to implementing username and password authentication in your full stack application. I would like to inform you that in this blog we will implementing username and password authentication with existing Email and password authentication. So that user could able login using either email or username.
Step 1: Update schema.prisma
file
Open schema.prisma
file in your project folder and then update the user
model in schema by adding the username
column with optional type string to it.
model User {
id String @id @default(cuid())
name String
email String
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
twoFactorEnabled Boolean @default(false)
// Add only username column with type of string and ignore other columns if you already implemented user model
username String?
isAnonymous Boolean?
Session Session[]
Account Account[]
TwoFactor TwoFactor[] // if you implemented 2FA add this column
@@unique([email])
@@map("user")
}
Then, Generate and migrate the prisma file using command below
pnpx prisma generate
pnpx prisma migrate dev --name username
Step 2: Update auth.ts
and auth-client.ts
file
Open auth.ts
file in your project repository and then add the username()
plugin into plugin array of betterAuth function.
// src/lib/auth.ts
import { betterAuth } from "better-auth"
import { username } from "better-auth/plugins"
const auth = betterAuth({
// other config options
plugins: [
// other plugins
username()
]
})
Then, Open auth-client.ts
file in your project repository and add usernameClient()
plugin function to the array of plugins
// src/lib/auth-client.ts
import { createAuthClient } from "better-auth/client"
import { usernameClient } from "better-auth/client/plugins"
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
plugins: [
usernameClient()
]
})
export const {
signIn,
signOut,
signUp,
useSession
} = authClient;
Step 3: Update login-schema.ts
and signup-schema.ts
file
Open login-schema.ts
file in your project repository and update the schema with following code below.
// src/helpers/zod/login-schema.ts
import { z } from "zod";
const LoginSchema = z
.object({
// checks if the input given by the user is email or username
emailOrUsername: z
.string()
.min(1, { message: "Email or username is required" }),
password: z
.string()
.min(8, { message: "Password must be at least 8 characters long" })
.max(20, { message: "Password must be at most 20 characters long" }),
})
.refine(
(data) =>
// checks if the email or username is valid
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.emailOrUsername) || /^[a-zA-Z0-9_.]+$/.test(data.emailOrUsername),
{
message: "Provide a valid email or username",
path: ["emailOrUsername"],
}
);
export default LoginSchema
Then, Open the signup-schema.ts
file in your project repository and added the username field and its type in zod schema.
// src/helpers/zod/signup-schema.ts
export const SignupSchema = z
.object({
name: z
.string()
.min(2, { message: "Minimum 2 characters are required" })
.max(20, { message: "Maximum of 20 characters are allowed" }),
email: z
.string()
.email({ message: "Invalid email address" })
.min(1, { message: "Email is required" }),
password: z
.string()
.min(8, { message: "Password must be at least 8 characters long" })
.max(20, { message: "Password must be at most 20 characters long" }),
// Added the username field to check its type
username: z
.string()
.min(3, { message: "Username must be at least 3 characters long" })
.max(25, { message: "Username must be at most 25 characters long" })
.regex(/^[a-zA-Z0-9_.]+$/, { message: "Username can only contain letters, numbers, underscores, and dots" }),
})
Step 4: Create generateUsername
file
Create a generate-username.ts
inside the helper/auth/
folder and paste the code from below
// helpers/auth/generate-username.ts
export const generateUsername = (name: string) => {
const randomNumbers = Math.floor(1000 + Math.random() * 9000); // Generate a random 4-digit number
return `${name.replace(/\s+/g, '').toLowerCase()}${randomNumbers}`;
};
GenerateUsername
function is used to generate random username from the given name of the user.
Step 5: Update SignIn component and Signup component
SignUp Component:
// src/components/auth/sign-in.tsx
"use client"
import React from 'react'
import CardWrapper from '../card-wrapper'
import FormError from '../form-error'
import { FormSuccess } from '../form-success'
import { FcGoogle } from 'react-icons/fc'
import SocialButton from './social-button'
import { FaGithub } from 'react-icons/fa'
import { useAuthState } from '@/hooks/useAuthState'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'
import { Input } from '../ui/input'
import { Button } from '../ui/button'
import { SignupSchema } from '@/helpers/zod/signup-schema'
import { signUp } from '@/lib/auth-client'
import { generateUsername } from '@/helpers/auth/generate-username'
const SignUp = () => {
const { error, success, loading, setLoading, setError, setSuccess, resetState } = useAuthState();
const form = useForm<z.infer<typeof SignupSchema>>({
resolver: zodResolver(SignupSchema),
defaultValues: {
name: '',
email: '',
password: '',
username: '', // Added username field
}
})
const onSubmit = async (values: z.infer<typeof SignupSchema>) => {
try {
await signUp.email({
name: values.name,
email: values.email,
password: values.password,
username: values.username, // Add the username fields
callbackURL: '/'
}, {
onResponse: () => {
setLoading(false)
},
onRequest: () => {
resetState()
setLoading(true)
},
onSuccess: () => {
setSuccess("Verification link has been sent to your mail")
},
onError: (ctx) => {
setError(ctx.error.message);
},
});
} catch (error) {
console.error(error)
setError("Something went wrong")
}
}
return (
<CardWrapper
cardTitle='SignUp'
cardDescription='Create an new account'
cardFooterLink='/signin'
cardFooterDescription='Already have an account?'
cardFooterLinkTitle='Signin'
>
<Form {...form}>
{/* Make changes to only name field of the form*/}
<form className='space-y-4' onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
disabled={loading}
type="text"
placeholder='john'
{...field}
onChange={(e) => {
field.onChange(e); // Update form state
const username = generateUsername(e.target.value);
form.setValue('username', username); // Auto-fill username
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* All other exisitng formfields like email and password */}
</form>
</Form>
</CardWrapper>
)
}
export default SignUp
Add username logic to SignUp
:
- Auto-generate usernames based on the user's name.
SignIn Component
"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 Link from "next/link";
import CardWrapper from "../card-wrapper";
import FormError from "../form-error";
import { FormSuccess } from "../form-success";
import { FcGoogle } from "react-icons/fc";
import { FaGithub } from "react-icons/fa";
import SocialButton from "./social-button";
import LoginSchema from "@/helpers/zod/login-schema";
import { useAuthState } from "@/hooks/useAuthState";
import { signIn } from "@/lib/auth-client";
import { requestOTP } from "@/helpers/auth/request-otp";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "../ui/form";
import { Input } from "../ui/input";
import { Button } from "../ui/button";
const SignIn = () => {
const router = useRouter();
// handles error, success, loading and resetState of a form
const { error, success, loading, setSuccess, setError, setLoading, resetState } = useAuthState();
const form = useForm<z.infer<typeof LoginSchema>>({
resolver: zodResolver(LoginSchema),
defaultValues: {
emailOrUsername: "", // update the field email to emailOrUsername
password: "",
},
});
// Check if the value is an email
const isEmail = (value: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
const { emailOrUsername, password } = values;
// Determine if the input is an email or username
const isEmailInput = isEmail(emailOrUsername);
try {
// resetState of a form after resubmit
resetState();
setLoading(true);
if (isEmailInput) {
// if user has given email and its true then signin using email and password
// also send 2FA code to the user's email, if enabled
await signIn.email(
{ email: emailOrUsername, password },
{
onRequest: () => setLoading(true),
onResponse: () => setLoading(false),
onSuccess: async (ctx) => {
if (ctx.data.twoFactorRedirect) {
const res = await requestOTP(); // request otp for 2fa
if (res?.data) {
setSuccess("OTP has been sent to your email");
router.push("two-factor");
} else if (res?.error) {
setError(res.error.message);
}
} else { // if didn't enable 2FA show loggedIn message
setSuccess("Logged in successfully");
router.replace("/");
}
},
onError: (ctx) => {
const errorMessage =
ctx.error.status === 403
? "Please verify your email address"
: ctx.error.message;
setError(errorMessage);
},
}
);
} else {
// if user has given username then signin using username and password
// also send 2FA code to the user's email, if enabled
await signIn.username(
{ username: emailOrUsername, password },
{
onRequest: () => setLoading(true),
onResponse: () => setLoading(false),
onSuccess: async (ctx) => {
if (ctx.data.twoFactorRedirect) {
const res = await requestOTP();
if (res?.data) {
setSuccess("OTP has been sent to your email");
router.push("two-factor");
} else if (res?.error) {
setError(res.error.message);
}
} else {
setSuccess("Logged in successfully");
router.replace("/");
}
},
onError: (ctx) => {
const errorMessage =
ctx.error.status === 403
? "Please verify your email address"
: ctx.error.message;
setError(errorMessage);
},
}
);
}
} catch (err) { // catch the error
console.error(err);
setError("Something went wrong. Please try again.");
} finally { // set the loading to false after submitting the form
setLoading(false);
}
};
return (
<CardWrapper
cardTitle="Sign In"
{/* change the cardDescription for email to email or password */}
cardDescription="Enter your email or username below to login to your account"
cardFooterDescription="Don't have an account?"
cardFooterLink="/signup"
cardFooterLinkTitle="Sign up"
>
<Form {...form}>
<form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
{/* Email or Username Field */}
<FormField
control={form.control}
name="emailOrUsername"
render={({ field }) => (
<FormItem>
<FormLabel>Email or Username</FormLabel>
<FormControl>
<Input
disabled={loading}
type="text" // update the type from email to text
placeholder="Enter email or username" // update the placeholder
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* All other exisitng formfields like password and form message */}
</form>
</Form>
</CardWrapper>
);
};
export default SignIn;
Enable login with username or email in SignIn
:
- Update the
email
field toemailOrUsername
. - Use a utility function to determine whether the input is an email or username.
Step 6: Run your application:
Start your development server:
pnpm dev
Navigate to your sign-up
route and sign-in
route and try sign-up the user first with email, name and password and then try using using email or username.
Conclusion
Congratulations! 🎉 You’ve successfully implemented a Username and Password Authentication system in your application. Your users can now log in with either their email or username, making the experience more flexible and user-friendly.
Blog Links:
Two Factor Authentication using BetterAuth: https://dev.to/daanish2003/two-factor-authentication-using-betterauth-nextjs-prisma-shadcn-and-resend-1b5p
Forgot and ResetPassword using BetterAuth: https://dev.to/daanish2003/forgot-and-reset-password-using-betterauth-nextjs-and-resend-ilj
Email Verification Blog: https://dev.to/daanish2003/email-verification-using-betterauth-nextjs-and-resend-37gn
Email And Password with Better_Auth: https://dev.to/daanish2003/email-and-password-auth-using-betterauth-nextjs-prisma-shadcn-and-tailwindcss-hgc
OAuth Blog: https://dev.to/daanish2003/oauth-using-betterauth-nextjs-prisma-shadcn-and-tailwindcss-45bp
Reference Links:
Better_Auth Docs: https://www.better-auth.com/
pnpm Docs: https://pnpm.io/
Docker Docs: https://docs.docker.com/
Prisma Docs: https://www.prisma.io/docs/getting-started
Shadcn Docs: https://ui.shadcn.com/
Next.js Docs: https://nextjs.org/
Tailwindcss Docs: https://tailwindcss.com/
Github repository: https://github.com/Daanish2003/better_auth_nextjs
Top comments (0)