Auth.js로 인증 시스템 구현

김민석·2026년 3월 19일
post-thumbnail

프론트,백엔드 에서 모두 Prisma를 사용한다. 그 전의 프로젝트를 진행하면서는 프론트에서 Prisma를 사용해 DB(supabase)를 모두 접근하는 방식으로 하였다. 이번에는 백엔드까지 구축하려고 한다. 그래서 프론트엔드와 백엔드 에서의 Prisma를 이용해 어떤것을 하는지 알아보자

FrontEnd Prisma

  • Auth.js의 Prisma 어댑터를 연동하기 위하여 사용해야함.
    => 어댑터를 사용하면 인증 관련해서 따로 DB 모델 구축 안하고 빠르게 적용가능
  • Auth.js에서 권장하는 인증 관련 스키마를 적용하기 위해
  • 백엔드에서 마이그레이션한 스키마(모델) 그대로 사용

BackEnd Prisma

  • DB 스키마에 직접적으로 접근
  • 데이터베이스 마이그레이션 수행
  • Auth.js의 인증 관련 모델을 포함하여 모델 관리를 담당함

Auth.js 설치

pnpm을 사용중이라 이렇게 설치한다.
pnpm dlx 는 npx랑 같은 것
frontend로 이동하여 해야함.

pnpm add next-auth@beta
pnpm dlx auth secret

env 파일에 AUTH_SECRET 생성됨.

auth.ts 파일 생성

import NextAuth from "next-auth"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [],
})

로그인 / 로그아웃 / 세션 확인을 위한 함수들을 만들어서 앱 전체에서 쓰게 해주는 파일
providers에 credentials,kakao 등 들어갈 수 있음.

/frontend/app/api/auth/[…nextauth]/route.ts 생성

import { handlers } from "@/auth" 
export const { GET, POST } = handlers

NextAuth 인증 로직을 API 라우트에 연결하는 코드

middleware.ts

export { auth as middleware } from "@/auth"
middleware 이곳에선 로그인 안한 사용자를 특정 페이지에서 막아주는 역할을 해줌

middleware

요청이 들어올 때 페이지 들어가기 전에 검사하는 부분이다.

Prisma 연동을 위해 설치

pnpm add @prisma/client @auth/prisma-adapter
pnpm add prisma --save-dev

그 다음에 env 파일에 backend에서 설정했던 DATABASE_URL 설정 해줘야함.

prisma.ts

import { PrismaClient } from "@prisma/client"
 
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
 
export const prisma = globalForPrisma.prisma || new PrismaClient()
 
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma

PrismaClient를 한 번만 만들고
global에 저장해서 계속 재사용하도록 해주는 코드

인증관련 Prisma 모델

https://authjs.dev/getting-started/adapters/prisma
참고해서 사용했습니다.
User : 사용자의 정보를 담는 Table
Account : 유저가 어떤 소셜 계정으로 로그인했는지 연결
Session : 세션 로그인 시 사용
VerificationToken : 이메일 인증 토큰
아래는 예시이므로 추가해도됨. 하지만 필드를 삭제하거나 관계를 꺠는것은 하지않는게 좋음.

model Account {
  id                 String  @id @default(cuid())
  userId             String  @map("user_id")
  type               String
  provider           String
  providerAccountId  String  @map("provider_account_id")
  refresh_token      String? @db.Text
  access_token       String? @db.Text
  expires_at         Int?
  token_type         String?
  scope              String?
  id_token           String? @db.Text
  session_state      String?
 
  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
 
  @@unique([provider, providerAccountId])
  @@map("accounts")
}
 
model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique @map("session_token")
  userId       String   @map("user_id")
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
 
  @@map("sessions")
}
 
model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime? @map("email_verified")
  image         String?
  accounts      Account[]
  sessions      Session[]
 
  @@map("users")
}
 
model VerificationToken {
  identifier String
  token      String
  expires    DateTime
 
  @@unique([identifier, token])
  @@map("verification_tokens")
}

Credentials 로그인 구현하기

이메일/비밀번호 기반 로그인을 구현하기 위해NextAuth의 Credentials Provider와 Prisma Adapter를 사용함. 비밀번호 검증과 사용자 조회 로직을 서버에서 처리하도록 구성했다.

전체코드

import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/prisma";
import CredentialsProvider from "next-auth/providers/credentials";
import { comparePassword } from "./lib/password-utils";

export const { handlers, auth, signIn, signOut } = NextAuth({
  useSecureCookies: process.env.NODE_ENV === "production",
  trustHost: true,
  adapter: PrismaAdapter(prisma),
  secret: process.env.AUTH_SECRET,
  providers: [
    CredentialsProvider({
      name: "credentials",
      credentials: {
        email: { label: "Email", type: "email", placeholder: "Email" },
        password: {
          label: "Password",
          type: "password",
        },
      },

      async authorize(credentials) {
        if (!credentials) return null;

        const email = credentials.email as string;
        const password = credentials.password as string;

        if (!email || !password) {
          throw new Error("Invalid credentials");
        }

        const user = await prisma.user.findUnique({
          where: { email },
        });

        if (!user || !user.hashedPassword) {
          throw new Error("Invalid credentials");
        }

        const passwordMatch = await comparePassword(
          password,
          user.hashedPassword,
        );

        if (!passwordMatch) {
          throw new Error("Invalid credentials");
        }

        return user;
      },
    }),
  ],

  session: {
    strategy: "jwt",
  },

  pages: {
    signIn: "/login",
  },
});

여기서 핵심은 authorize 함수이다. 살펴보면 받아온 email과 password로 user 테이블에서 같은 email을 찾고 비밀번호를 비교한다.

import bcrypt from "bcryptjs";

// salt + hash password

export const comparePassword = async (
  password: string,
  hash: string,
): Promise<boolean> => {
  return bcrypt.compare(password, hash);
};

comparePassword 함수를 사용해 비교한다.
맞는게 없다면 에러 맞는게 있다면 user를 반환 즉 로그인 성공 또한 jwt를 사용한다. jwt를 사용하면 서버 세션 저장 없이 인증 가능 하며 확장성과 성능 측면에서 유리하다.

또한 adapter: PrismaAdapter(prisma),를 사용하면 NextAuth와 DB연결을 쉽게 도와준다.

로그인 함수

 const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    signIn("credentials", {
      email,
      password,
      redirectTo: "/",
    });
  };

signIn("credentials")를 통해 email과 password를 보내준 후 성공 시 /(main) 으로 가게 한다.

회원가입

const existingUser = await prisma.user.findUnique({
      where: {
        email,
      },
    });

    if (existingUser) {
      return { status: "error", message: "이미 존재하는 이메일입니다." };
    }

    const user = await prisma.user.create({
      data: {
        email,
        hashedPassword: await hashSaltPassword(password),
      },
    });

전달받은 email과 password로 진행된다.
email로 user table에 email과 같은 user를 찾고 있다면 return 해준다. 없다면 user.create를 사용해 유저를 생성 시켜줌. email과 hashSaltPassword 함수를 통해 만들어진 password를 통해서

export const hashSaltPassword = async (password: string): Promise<string> => {
  const saltRounds = 10;
  const salt = await bcrypt.genSalt(saltRounds);
  const hash = await bcrypt.hash(password, salt);
  return hash;
};

로그아웃

import { signOut } from "@/auth";
 await signOut();

signOut을 사용하면 바로 로그아웃이 가능함.

profile
나만의 기록장

0개의 댓글