본문 바로가기
개발/Next.js

[Next.js] NextAuth와 Prisma로 인증 기능 구현하기

by 핸디(Handy) 2023. 11. 27.

들어가며

이번 글에서는 NextAuth와 Prisma 그리고 PostgreSQL를 이용하여 인증 기능을 구현하는 글입니다.

Prisma와 PostgreSQL의 세팅이 완료된 시점을 기준으로 하고 Google 로그인을 예제로 합니다.

사전 준비

Google 로그인을 하기 위해선 구글 로그인의 clientId와 clientSecret 키가 필요합니다. 

이것을 발급받는거부터 빠르게 시작하겠습니다.

키 발급

 

구글 소셜 로그인 Client ID와 Client Secret Key 설정방법

구글에서 제공하는 소셜 로그인에 사용되는 Client ID와 Client Secret Key를 발급받는 과정을 설명합니다.

medipress.co.kr

해당 글을 읽은 다음에 두 개의 값을 가져오면 되겠습니다.

그 외에 설정해야할 값이 있는데요.

리디렉션 URI

\

URI2에 보면 http://localhost:4001/api/auth/callback/google 이 값이 있습니다.

저는 개발포트를 4001를 사용하고 있어서 이렇게 사용했습니다.

추가로 만약 프로덕션으로 배포했을 때에도 여기에 추가해주셔야 합니다.

ex) www.handy.com   -> https://www.handy.com/api/auth/callback/google  이렇게요 

그럼 이제 사전준비는 마무리되었습니다.

구현 예시

 

@auth/prisma-adapter | Auth.js

Official Prisma adapter for Auth.js / NextAuth.js.

authjs.dev

네 실제로는 이 문서 보고 따라 하면 됩니다.

근데 잘 안돼요. adapter와 provider들이 너무 많아서 못 챙긴 것 같다고 생각합니다. (아니면 제가 잘못한 것일 수도,,?)

실제 구현

그럼 이제 제가 했던 방식으로 구현을 진행해 보겠습니다.

그전에 잠시 NextAuth.js와 Auth.js에 대해 설명하고 가겠습니다.

NextAuth.js와 Auth.js

v4 -> v5의 메인화면

메인 화면부터 "우린 Next를 위한 인증이다"라고 말하고 있는 NextAuth.js입니다.

최근에 v4에서 v5로 넘어가면서 Next를 떼어내고 Auth.js로 거듭났습니다.

하지만 위에서 말씀드렸듯이 이번 프로젝트에서는 v4인 NextAuth.js를 사용합니다.

제가 v5의 Auth.js를 사용하니깐 Prisma의 연결고리 부분에서 에러가 있었고, 해결과정을 보니 v4를 통해서만 동작하는 것을 확인했습니다.

본격 구현에 앞서 잠깐 용어 2가지를 공부하고 가겠습니다.

Providers

providers는 이름 그대로 공급자입니다. 인증 서비스나 방법을 제공하는 업체, 혹은 모듈을 의미합니다.

providers는 유저를 로그인 페이지로 리디렉션 하고, 인증 콜백을 처리하고, 사용자 정보를 검색하는 등 인증 관련 작업을 처리합니다.

NextAuth에서 말하는 provider의 종류는 크게 4가지가 있습니다.

그중에 우리는 첫 번째 built-in OAuth를 제공하는 Google Provider를 사용해 볼 예정입니다.

Adapters

Adapters는 사용자, 계정, 세션 등에 대한 데이터를 저장하는 데 사용하려는 데이터베이스 또는 백엔드 시스템에 애플리케이션을 연결하는 모듈을 의미합니다.

다만 필수값은 아닙니다.  하지만 이번 글에서는 prisma를 데이터베이스 삼아 진행합니다. 따라서 prisma-adapter가 필요합니다.

그럼 이제 구현으로 가볼까요?

[... nextauth]. ts

해당 파일을 살펴보기 전에 필요한 라이브러리를 설치하고 넘어가겠습니다.

"@next-auth/prisma-adapter": "^1.0.7"
"next-auth": "^4.24.5"

제가 설치한 버전은 다음과 같습니다. 위에서 언급했다시피 최신버전의 코드가 동작하지 않으면 해당 버전으로 다운그레이드 설치하시면 되겠습니다.

[... nextauth]. ts파일은 next.js의 버전에 따라 라우팅 경로가 다르지만,

page 라우터 기준으로 pages/api/auth/[... nextauth]. ts에 설정하면 됩니다.

// pages/api/auth/[...nextauth].ts

import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { NextApiRequest, NextApiResponse } from "next";
import NextAuth, { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";

declare module "next-auth" {
  interface User {
    id: number;
    name: string;
    email: string;
    image: string;
  }
  interface Session {
    user: {
      email: string;
      emailVerified: any;
      id: string;
      image: string;
      name: string;
      role: "MANAGER" | "GUEST";
    };
  }
}

export const authOptions: NextAuthOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || "",
      clientSecret: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_SECRET || "",
    }),
  ],
  callbacks: {
    async signIn({ user, account, profile, email, credentials }) {
      return true;
    },
    async session({ session, user }) {
      // 추가 권한 체크가 필요하면 이곳에서 해서 내려준다
      const response = await prisma.manager.findUnique({
        where: {
          email: user.email,
        },
      });
      return { ...session, user: { ...user, role: response?.role || "GUEST" } };
    },
  },
  adapter: PrismaAdapter(prisma),
};

const Auth = (req: NextApiRequest, res: NextApiResponse) =>
  NextAuth(req, res, authOptions);

export default Auth;

Providers 부분에 처음에 발급받았던 키를 넣으면 되겠습니다. 해당 부분에는 Google 뿐만 아니라 카카오, 네이버도 넣을 수 있습니다.

추가로 callbacks의 session에 추가 권한 체크 로직이 들어가 있습니다.

다른 문서를 살펴보니 유저별 권한의 경우 User 테이블에 role를 넣어서 관리하라고 되어있습니다.

하지만 현재 프로젝트에는 별도로 Manager 테이블을 관리하고 있기 때문에 email를 비교하여 role를 가져오는 방식으로 구현하였습니다.

NEXTAUTH_URL

추가로 해당 환경변수도 설정을 해줘야 합니다. 다만 프로젝트 port가 3000번일 경우는 필요 없음.

환경변수 관리는 사람마다 다르니 어딜지는 모르겠으나 일단 필요합니다. 아니면 로그인중간에 3000번으로 튕겨요.

// .env
NEXTAUTH_URL=http://localhost:4001

Prisma 스키마 

위에서 말했듯이 인증에 관련된 저장소를 Prisma를 통해 연결할 것입니다. 그리고 prisma는 현재 postgreSQL과 연결되어 있다고 가정합니다.

prisma를 연결하고 npx prisma db pull 명령어를 실행하면 schema.prisma에 연결된 DB의 스키마가 생깁니다.그래서 해당 파일에 Account, Session, User, VerificationToken 모델을 생성해 줍니다. (복붙 추천)

// schema.prisma

model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  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])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  accounts      Account[]
  sessions      Session[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}

/* 다른 스키마들 */

그런 다음에 아래 명령어를 입력하면

npx prisma db push

이런저런 명령어와 함께 완료되었다고 뜹니다.

저는 이미 업데이트해서 already in sync 상태

그럼 이제 DB 준비도 끝났습니다.

UI 구현

완성된 모습을 보겠습니다.

중간에 prisma 관련되어 경고가 뜨긴 하지만 로그인이 성공적으로 되었습니다.

그리고 DB를 확인하면 정상적으로 값이 저장되어 있음을 확인할 수 있습니다.

useLogin 훅

저는 이런 로그인관련된 기능을 관리하기 위해 별도의 훅을 만들어 사용하고 있는데요.

import axios from "axios";
import { signIn, signOut, useSession } from "next-auth/react";

export default function useLogin() {
  const { data: session, status } = useSession();

  const logOut = async () => {
    await axios.get("/api/auth/signout");
    await signOut({ redirect: true });
  };

  return {
    login: signIn,
    logOut: logOut,
    userProfile: { ...session?.user } || {},
    isLogin: status === "authenticated",
  };
}


// 실제 사용
const Component = () => {
  const { login, isLogin, logOut, userProfile } = useLogin();

  console.log(userProfile.name); // 홍길동씨
  console.log(isLogin); // true

  return <></>;
};

이렇게 만들어서 편하게 사용하고 있습니다.

그 외

커스텀 로그인 화면

로그인 화면을 살펴보면 이렇게 투박한 형태로 되어있습니다. 하지만 해당 ui도 원하는 대로 바꿀 수 있습니다.

 

 

Pages | NextAuth.js

NextAuth.js automatically creates simple, unbranded authentication pages for handling Sign in, Sign out, Email Verification and displaying error messages.

next-auth.js.org

해당 문서를 살펴보면 됩니다.

간단히 설명하자면, 각 기능에 맞춰서 페이지를 구성할 수 있습니다.

회사에서 사용하는 내부툴의 UI는 이렇게 생겼어요.

로그인 화면 구현 예시

구글 형님들이 추천하는 로그인 양식 구현 예시입니다.

 

로그인 양식 권장사항  |  Articles  |  web.dev

크로스 플랫폼 브라우저 기능을 사용하여 안전하고 액세스 및 사용이 간편한 로그인 양식을 만들 수 있습니다.

web.dev

자동완성을 위한 속성, 추천하는 UI까지 5분 정도면 읽을 수 있는데 알찬 정보를 담은 글이니 꼭 읽어보시길 바랍니다.

댓글