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.