Create a login screen in Next.js to enable login with email address/password
Introduction
Up to the previous article, we used Auth.js to determine whether the session for an incoming request was logged in or not, and used Next.js middleware to configure which screens could be accessed in each state.
This time, we will implement the login process and verify that we can access screens that are only available when logged in.
Goal for this article
We will create a login input form and, when login succeeds, redirect to the settings screen and display the session information. When the user logs out, they will be taken back to the login screen.
We will also implement validation at login time so that if the password is not entered, or if the entered email address/password is incorrect, the user will be notified.
Login with email address/password
This can be implemented using the Auth.js Credentials Provider.
Implementation flow
There are more parts to implement than before, and the amount of code is accordingly larger. However, since no new libraries are introduced, the code should be relatively easy to understand.
- ① Create a Zod schema for login
- ② Create processing to access the database using the email address as the key
- ③ Create authentication processing
- ④ Create a server action for login and integrate the authentication processing
- ⑤ Create the login form (including validation)
- ⑥ Integrate the login form into the login screen
- ⑦ Implement logout processing on the settings screen
① Create a Zod schema for login
First, we will create a Zod schema for login.
It is similar to the schema for account creation, but we change two points:
- No validation for name input
- Password must be at least 1 character (because password strength is enforced at account creation time)
Add the following to index.ts under /schema.
+ export const LoginSchema = z.object({
+ email: z.string().email({
+ message: 'Please enter your email address',
+ }),
+ password: z.string().min(1, {
+ message: 'Please enter your password',
+ }),
+ })
② Create processing to access the database using the email address as the key
We will create processing that queries the database for user information based on the email address.
Since this will be used in other places later as common processing, we will implement it as a standalone function.
Create a user.ts file under /data.
import db from '@/lib/db'
export const getUserByEmail = async (email: string) => {
try {
const user = await db.user.findUnique({ where: { email } })
return user
} catch {
return null
}
}
③ Create authentication processing
We will perform authentication using the schema definition and database query processing created in ① and ②.
+ import bcryptjs from 'bcryptjs'
import type { NextAuthConfig } from 'next-auth'
+ import Credentials from 'next-auth/providers/credentials'
import github from 'next-auth/providers/github'
+ import { getUserByEmail } from './data/user'
+ import { LoginSchema } from './schema'
- export default { providers: [github] } satisfies NextAuthConfig
+ export default {
+ providers: [
+ github,
+ Credentials({
+ async authorize(credentials) {
+ const validatedFields = LoginSchema.safeParse(credentials)
+
+ if (!validatedFields.success) {
+ return null
+ }
+ const { email, password } = validatedFields.data
+ const user = await getUserByEmail(email)
+ if (!user || !user.password) {
+ return null
+ }
+ const passwordMatch = await bcryptjs.compare(password, user.password)
+ if (passwordMatch) {
+ return user
+ }
+ return null
+ },
+ }),
+ ],
+ } satisfies NextAuthConfig
Explanation of the code:
+ Credentials({
+ async authorize(credentials) {
+ const validatedFields = LoginSchema.safeParse(credentials)
+
+ if (!validatedFields.success) {
+ return null
+ }
The credentials object contains the email address and password entered by the user at login. We run schema validation, and if there is a problem, we terminate the process.
+ const { email, password } = validatedFields.data
+ const user = await getUserByEmail(email)
+ if (!user || !user.password) {
+ return null
+ }
We access the database using the email address entered by the user, and if the account is not registered or no password is set, we terminate the process.
+ const passwordMatch = await bcryptjs.compare(password, user.password)
+ if (passwordMatch) {
+ return user
+ }
After retrieving the hashed password from the database, we hash the password entered by the user and compare them; if they match, we return the user information and treat it as a successful completion.
④ Create a server action for login and integrate the authentication processing
We will integrate the authentication processing created above into a server action.
Create a login.ts file under /actions.
'use server'
import { AuthError } from 'next-auth'
import { z } from 'zod'
import { signIn } from '@/auth'
import { DEFAULT_LOGIN_REDIRECT } from '@/route'
import { LoginSchema } from '@/schema'
export const login = async (values: z.infer<typeof LoginSchema>) => {
const validatedFields = LoginSchema.safeParse(values)
if (!validatedFields.success) {
return { error: 'Please correct the input' }
}
const { email, password } = validatedFields.data
try {
await signIn('credentials', {
email,
password,
redirectTo: DEFAULT_LOGIN_REDIRECT,
})
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case 'CredentialsSignin':
return {
error: 'Email address or password is incorrect.',
}
default:
return { error: 'An error occurred。' }
}
}
throw error
}
}
Explanation of the code:
await signIn('credentials', {
email,
password,
redirectTo: DEFAULT_LOGIN_REDIRECT,
})
When the signIn function is executed, the authorize we defined earlier is called.
We set DEFAULT_LOGIN_REDIRECT (/settings) as the redirect destination when the process completes successfully.
⑤ Create the login form (including validation)
Now that the login processing has been implemented, we will prepare an input form for logging in and wire it up to the login processing.
Create a login-form.tsx file under /component/auth.
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState, useTransition } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { login } from '@/actions/login'
import { LoginSchema } from '@/schema'
import { FormError } from '../form-error'
import { Button } from '../ui/button'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '../ui/form'
import { Input } from '../ui/input'
import { CardWrapper } from './card-wrapper'
export const LoginForm = () => {
const [error, setError] = useState<string | undefined>('')
const [isPending, setTransition] = useTransition()
const form = useForm<z.infer<typeof LoginSchema>>({
resolver: zodResolver(LoginSchema),
defaultValues: {
email: '',
password: '',
},
})
const onSubmit = async (values: z.infer<typeof LoginSchema>) => {
setError('')
setTransition(async () => {
try {
const response = await login(values)
if (response && response.error) {
setError(response.error)
}
} catch (e) {
setError('An error occurred')
}
})
}
return (
<CardWrapper
headerLabel="Login with Email and Password"
buttonLabel="Don't have an account? Click here"
buttonHref="/auth/register"
showSocial
>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email Address</FormLabel>
<FormControl>
<Input
{...field}
disabled={isPending}
placeholder="nextjs@example.com"
type="email"
autoComplete="off"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
{...field}
placeholder="******"
type="password"
disabled={isPending}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormError message={error} />
<Button type="submit" className="w-full" disabled={isPending}>
Login
</Button>
</form>
</Form>
</CardWrapper>
)
}
This is almost the same structure as the account creation form, so the detailed explanation is omitted.
One difference is that, while the account creation form returned a message on success, in the case of login we redirect to the settings screen on success, so there is no need to display a success message.
Therefore, we removed the component that displays a success message and the state management for that message.
⑥ Integrate the login form into the login screen
Integrate the created login form into the page.
import { LoginForm } from '@/components/auth/login-form'
const LoginPage = () => {
return <LoginForm />
}
export default LoginPage
⑦ Implement logout processing on the settings screen
For operation verification, we will implement logout processing on the settings screen.
+ import { auth, signOut } from '@/auth'
+ import { Button } from '@/components/ui/button'
- const SettingsPage = () => {
- return <div>Settings Screen</div>
+ const SettingsPage = async () => {
+ const session = await auth()
+ if (!session) return null
+ const onSubmit = async () => {
+ 'use server'
+ await signOut()
+ }
+ return (
+ <div>
+ Settings Screen
+ <pre>{JSON.stringify(session, null, 2)}</pre>
+ <form action={onSubmit}>
+ <Button type="submit" className="w-full" variant="secondary">
+ Logout
+ </Button>
+ </form>
+ </div>
+ )
}
export default SettingsPage
Explanation of the code:
+ const session = await auth()
+ if (!session) return null
((※ Partially omitted))
+ <pre>{JSON.stringify(session, null, 2)}</pre>
With await auth(), Auth.js allows you to obtain the session on the server side. We then display the obtained session information on the settings screen.
+ const onSubmit = async () => {
+ 'use server'
+ await signOut()
+ }
This is the processing for logging out.
Operation check
After implementing all the code, we will verify that the implemented parts behave as expected.
① When the password is not entered, a validation error is returned
② When the password is incorrect, the login server action returns an error
③ When the correct email address/password is entered, you are redirected to the settings screen, can log in, and can confirm the session information
④ When you log out, you are redirected to the login screen (Next.js middleware feature)
⑤ When you access the settings screen while not logged in, you are redirected to the login screen (Next.js middleware feature)
Items ④ and ⑤ are features implemented in the previous article.
Conclusion
With this article, we have completed the series of processes from account creation to login and logout.
However, there are still several points to consider regarding authentication.
- Checking whether the email address exists
- Password reset
- Two-factor authentication
These features are essential when implementing authentication with email address/password. In future articles, we will also cover how to implement these.
Even when using libraries like Auth.js, implementing email/password authentication involves a fair number of considerations and can be quite involved. For systems in the PoC phase where you want to spend as little time as possible on authentication, it may be better to use OAuth-based authentication with providers such as Google or Facebook.
Auth.js also supports integrating OAuth-based authentication, and we will cover that in future articles as well.
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Chat App (with Image/PDF Sending and Video Call Features)
2024/07/15Practical Component Design Guide with React × Tailwind CSS × Emotion: The Optimal Approach to Design Systems, State Management, and Reusability
2024/11/22Management Dashboard Features (Graph Display, Data Import)
2024/06/02Tutorial for Implementing Authentication with Next.js and Auth.js
2024/09/13Thorough Comparison of the Best ORMs for the Next.js App Router: How to Choose Between Prisma / Drizzle / Kysely / TypeORM [Part 1]
2025/03/13Test Strategy in the Next.js App Router Era: Development Confidence Backed by Jest, RTL, and Playwright
2025/04/22Done in 10 minutes. Easy deployment procedure for a Next.js app using the official AWS Amplify template
2024/11/05Building an Integrated Next.js × AWS CDK Environment: From Local Development with Docker to Production Deployment
2024/05/11
