Creating the Sign In Flow
Course: Next.js Authentication and Authorization
Introduction
With the signup flow complete, the next step is to implement the sign in logic for the user. The good news is that the sign in logic is very similar to the signup. You'll notice that the code we use is near identical with a few minor changes. So if you were able to understand the signup logic, this should be rather smooth sailing.
Building the Sign In API Route
Similar to the Signup API route, you'll need to create a route.ts
file. Except for this you'll create a new folder for it like this:
app/
api/
auth/
signin/
route.ts
Then add the following code to the file:
import { NextRequest, NextResponse } from "next/server"
import { signIn, getSession } from "@/lib/auth"
export async function POST(request: NextRequest) {
try {
const { email, password } = await request.json()
if (!email || !password) {
return NextResponse.json(
{ error: "Email and password are required" },
{ status: 400 }
)
}
const result = await signIn(email, password)
if (!result.success || !result.user) {
return NextResponse.json({ error: result.error }, { status: 401 })
}
// Create session
const session = await getSession()
session.userId = result.user.id
session.email = result.user.email!
session.role = result.user.role
session.isLoggedIn = true
await session.save()
return NextResponse.json({
success: true,
user: {
id: result.user.id,
email: result.user.email,
role: result.user.role,
},
})
} catch (error) {
console.error("Signin error:", error)
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
)
}
}
As you can see, it's nearly identical to out signup route, with the main different being the signIn
function being called. We haven't defined that yet, so let's go ahead and do so back in our auth.ts
file.
Create Sign In Logic
Add the following to the auth.ts
file:
export async function signIn(
email: string,
password: string
): Promise<{ success: boolean; error?: string; user?: User }> {
try {
const user = await db.user.findUnique({ where: { email } })
if (!user || !user.password || !user.salt)
return { success: false, error: "Invalid credentials" }
const isValid = await verifyPassword(password, user.password, user.salt)
if (!isValid) return { success: false, error: "Invalid credentials" }
return { success: true, user }
} catch (error) {
console.error("Sign in error:", error)
return { success: false, error: "Failed to sign in" }
}
}
The structure is very similar to our signUp
function. However, with this function, we are looking for an existing user in the database using their email. If that user doesn’t exist, or the password or salt aren't present, then we'll throw an error.
We'll then need to create our verifyPassword
function in the same file:
export async function verifyPassword(
password: string,
hash: string,
salt: string
): Promise<boolean> {
const derivedKey = (await scryptAsync(password, salt, KEY_LENGTH)) as Buffer
return crypto.timingSafeEqual(Buffer.from(hash, "hex"), derivedKey)
}
Much like the hashPassword
function, we're getting the derived key using scrypt
. In the return statement, we're using crypto's timingSafeEqual
method to compare the hashes. This method makes it so that it always takes the same amount of time to compare, no matter how similar the values are. This prevents attackers from measuring response times to guess the password bit by bit.
It compares the hashed password, which is converted back into binary, with the derived key. It returns true if the password is correct, and false if it doesn’t match.
Back in the signIn
function, we'll return the success status, which is passed into the sign in API route back to the front end.