Front-end

Next.js 14 Next-auth를 사용한 로그인 구현

ghDev 2024. 7. 23. 16:09

 

 

Next-auth란?

Next.js에서 사용하는 인증 라이브러리로 소셜 로그인을 간단하게 구현 할 수 있고

일반적인 백엔드서버와의 로그인 방식(JWT)도 적용이 가능하다.

오늘은 내가 아트인포에 적용한 방식에 대해 얘기해 보려한다.

 

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

const handler = NextAuth(authOptions)

export { handler as GET, handler as POST }

export const authOptions = {
  pages: {
    signIn: "/auth/sign-in",
    signOut: "/auth/sign-in",
    error: "/auth/sign-in",
  },
  session: {
    maxAge: 60 * 60 * 24 * 60,
  },
  secret: process.env.NEXTAUTH_SECRET,
  providers: [
    CredentialsProvider({
      id: "signin-email",
      credentials: {
        email: { label: "email", type: "text" },
        password: { label: "password", type: "password" },
      },
      async authorize(credentials): Promise<User | null> {
        const response = await fetch(
          `${process.env.REST_API_BASE_URL}/auths/login/email`,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              email: credentials?.email,
              password: credentials?.password,
            }),
          },
        )

        const result = await response.json()

        if (result.item) {
          return {
            id: "",
            accessToken: result.item.accessToken,
            refreshToken: result.item.refreshToken,
            accessTokenExpiresIn: result.item.accessTokenExpiresIn,
            refreshTokenExpiresIn: result.item.refreshTokenExpiresIn,
          }
        }
        return null
      },
    }),
    CredentialsProvider({
      id: "sns",
      credentials: {
        accessToken: { label: "Access Token", type: "text" },
        type: { label: "SNS Type", type: "text" },
      },
      async authorize(credentials): Promise<User | null> {
        if (!credentials?.accessToken || !credentials.type) {
          return null
        }

        try {
          const response = await fetch(
            `${process.env.REST_API_BASE_URL}/auths/login/sns`,
            {
              method: "POST",
              headers: {
                "Content-Type": "application/json",
              },
              body: JSON.stringify({
                type: credentials.type,
                token: credentials.accessToken,
              }),
            },
          )

          if (!response.ok) {
            throw new Error("Network response was not ok")
          }

          const result = await response.json()

          if (result.item) {
            return {
              id: "",
              accessToken: result.item.accessToken,
              refreshToken: result.item.refreshToken,
              accessTokenExpiresIn: result.item.accessTokenExpiresIn,
              refreshTokenExpiresIn: result.item.refreshTokenExpiresIn,
            }
          } else {
            console.error("Unexpected API response structure:", result)
            return null
          }
        } catch (error) {
          console.error("SNS sign-in error", error)
          return null
        }
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }: { token: any; user: User }) {
      if (user) {
        cookies().set({
          name: "accessToken",
          value: user.accessToken,
          httpOnly: true,
          path: "/",
          expires: new Date(user.accessTokenExpiresIn),
        })
        token.accessToken = user?.accessToken
        token.refreshToken = user?.refreshToken
        token.accessTokenExpiresIn = user?.accessTokenExpiresIn
        token.refreshTokenExpiresIn = user?.refreshTokenExpiresIn
      }

      if (new Date() > new Date(token.accessTokenExpiresIn)) {
        const result = await handleRefreshToken({
          accessToken: token.accessToken,
          refreshToken: token.refreshToken,
        })

        if (result) {
          token.accessToken = result.accessToken
          token.refreshToken = result.refreshToken
          token.accessTokenExpiresIn = result.accessTokenExpiresIn
          token.refreshTokenExpiresIn = result.refreshTokenExpiresIn
        }

        return token
      }

      return token
    },

    async session({ session, token }: any) {
      session.token = token

      return session
    },
  },
}

 

next.js Api 라우트를 사용하여 /api/auth/[...nextauth] 경로에 설정을 해준다.

next-auth는 해당 라우트를 사용해 인증 로직을 처리한다.

  • pages, session, secret 등에 기본적인 프로젝트별 설정을 해줘야한다.
  • providers에 있는 CredentialProvider의 id 값들이 내가 진행할 로그인 방식의 id값이 된다.

 

next-auth의 signIn method를 사용해 'signin-email' provider를 호출하고

기본적인 flow는 

  1. provider를 사용해 로그인 성공시 받아온 토큰 정보들을 리턴해주게 되면
  2. callbacks의 user객체에 담기게 되고 토큰의 유효기간을 확인한뒤 session 객체에 담아준다
    유효기간을 확인하는 이유는 로그인 뿐만 아니라 페이지 라우팅이 변경될시(페이지 이동시에도 callbacks가 실행된다)                      session 객체에 담아주는 이유는 Next-auth에선 해당 session 객체의 데이터를 암호화하여 쿠키에 담을 뿐아니라
    CSR 환경에서 useSession 훅으로 쉽게 접근이 가능하다.   

    소셜 로그인과 같은 경우는 구글을 예를 들자면

 

  1. 구글로그인 진행

 

   2. 로그인 성공후 redirect 된 callback 페이지에서 구글에서 받아온 토큰을 signIn 'sns'로 요청한다.

 

  3. 아트인포 백엔드 서버는 받아온 토큰을 구글에 한번더 재 확인 후 백엔드 서버의 토큰을 발급해준다. 이 뒤는 이메일 방식과 같다.

 

 

 

백엔드서버 없이 진행하는 프로젝트라면 좀 더 유용하겠지만 Next-auth는 사실 필수적인 라이브러리는 아니라고 생각한다.

 사용한 이유로는 페이지별 로그인 여부 확인 등 인증 부분에선 확실히 강점이 있다고 생각해서 였고 제작년부터 눈여겨 보던 
라이브러리라 제대로 사용해보고 싶기도 했다. 

다음 프로젝트에선 인증 부분을 직접 구현하여 볼 생각이다.