7/24 Credentials Signin 구현 과정

낄낄박사·2024년 7월 24일

Gotcha

목록 보기
7/22
  1. NextAuth의 매개변수인 authOption 작성 -> providers에 CredentialsProvider 정의
  • credentials는 NextAuth.js가 제공하는 기본 로그인 폼에서 사용되는 필드임. 나는 커스텀 페이지를 사용했음.
  • 커스텀 로그인 페이지에서는 NextAuth.js의 signIn 함수를 사용하여 자격 증명을 서버로 전송한다. 이 때 signIn 함수에 전달된 인자는 authorize 함수로 전달 됨!
  1. authorize 정의 => Credentials 로그인 시도시 존재하는 유저인지(findUserByEmail), 비밀번호가 일치하는지(verifyPassword) 검증하는 로직
  • authOption 전체코드
import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import KakaoProvider from "next-auth/providers/kakao";
import NaverProvider from "next-auth/providers/naver";
import CredentialsProvider from "next-auth/providers/credentials";
import { findUserByEmail } from "@/services/user";
import bcrypt from "bcrypt";

async function verifyPassword(plainPassword: string, hashedPassword: string) {
  return await bcrypt.compare(plainPassword, hashedPassword);
}

export const authOptions: NextAuthOptions = {
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        const { email, password } = credentials!;
        const user = await findUserByEmail(email);
        if (!user) return null;

        const verifyPw = await verifyPassword(password, user.password);
        if (!verifyPw) return null;

        return {
          id: user._id,
          email: user.email,
          name: user.name,
          image: null,
          username: user.email.split("@")[0],
        };
      },
    }),
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID || "",
      clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
    }),
    KakaoProvider({
      clientId: process.env.KAKAO_CLIENT_ID || "",
      clientSecret: process.env.KAKAO_CLIENT_SECRET || "",
    }),
    NaverProvider({
      clientId: process.env.NAVER_CLIENT_ID || "",
      clientSecret: process.env.NAVER_CLIENT_SECRET || "",
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
        token.username = `user_${user.id.slice(0, 8)}`;
      }
      return token;
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.id;
        session.user.username = token.username;
      }
      return session;
    },
    async redirect({ url, baseUrl }) {
      // 로그인 후 리디렉션 설정
      return baseUrl;
    },
  },
  pages: {
    signIn: "/auth/signin",
    newUser: "/auth/signup",
  },
};

3.sanity 함수 정의

  • findUserByEmail, addCredentialUser 함수 작성
  1. 비밀번호 해싱 및 검증 함수 정의
  • hashPassword, verifyPassword 함수 작성
  • 비밀번호 해싱 및 검증은 bcrypt를 사용함
  • bcrypt는 단방향 해시 알고리즘으로 평문으로 작성된 패스워드를 암호화 하는 것은 가능하나 암호화된 문자를 다시 평문으로 복호화하는 것은 불가능함.
  1. 회원가입 폼(app>auth>singup>page.tsx)
"use client";

import { useRouter } from "next/navigation";
import { FormEvent, useRef } from "react";

type Props = {
  csrfToken: string;
};
export default function SignupForm({ csrfToken }: Props) {
  const usernameRef = useRef<HTMLInputElement>(null);
  const emailRef = useRef<HTMLInputElement>(null);
  const passRef = useRef<HTMLInputElement>(null);

  const router = useRouter();

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

    const formData = new FormData();
    formData.append("username", usernameRef.current?.value ?? "");
    formData.append("email", emailRef.current?.value ?? "");
    formData.append("password", passRef.current?.value ?? "");

    fetch("/api/auth/signup", {
      method: "POST",
      body: formData,
    }).then((res) => {
      if (!res.ok) {
        return console.log(`${res.status}${res.statusText}`);
      }
      router.push("/auth/signin");
    });
  };

  return (
    <form method="post" className="flex flex-col" onSubmit={handleSubmit}>
      <input name="csrfToken" type="hidden" defaultValue={csrfToken} />
      <label>
        이름
        <input
          name="username"
          ref={usernameRef}
          type="text"
          className="border"
        />
      </label>
      <label>
        이메일
        <input name="email" ref={emailRef} type="email" className="border" />
      </label>
      <label>
        비밀번호
        <input
          name="password"
          ref={passRef}
          type="password"
          className="border"
        />
      </label>
      <button className="border" type="submit">
        회원가입
      </button>
    </form>
  );
}
  1. 회원가입 API 라우터
  1. 로그인 폼(app>auth>signin>page.tsx)
import {
  ClientSafeProvider,
  getCsrfToken,
  getProviders,
} from "next-auth/react";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/lib/auth";
import { redirect } from "next/navigation";
import OAuthSignin from "@/components/OAuthSignin";
import CredentialSigninForm from "@/components/CredentialSigninForm";

type Providers = Record<string, ClientSafeProvider>;

export default async function SignInPage() {
  const session = await getServerSession(authOptions);

  const allProviders = ((await getProviders()) as Providers) ?? {};
  const filteredProviders = Object.keys(allProviders).reduce((acc, key) => {
    if (key !== "credentials") {
      acc[key] = allProviders[key];
    }
    return acc;
  }, {} as Providers);

  const csrfToken = (await getCsrfToken()) ?? "";

  if (session) {
    redirect("/");
  }

  return (
    <section>
      <CredentialSigninForm csrfToken={csrfToken} />
      <OAuthSignin providers={filteredProviders} />
    </section>
  );
}

. 로그인 후 리디렉션 설정

0개의 댓글