Next.js

Next.js 13에서 NextAuth 익명 로그인 구현하기

presentKey 2023. 9. 8. 00:49

NextAuth는 OAuth(Google, Github 등), Email, 아이디와 비번을 입력받는 Credentials 방식을 제공하고 있습니다.

 

프로젝트에 firebase의 익명 로그인 기능이 필요했는데, NextAuth 문서에는 이에 대한 지원을 찾아볼 수 없었습니다. 아래의 링크를 참고하여 NextAuth에서 익명 로그인을 구현할 수 있었습니다.

 

1. https://github.com/nextauthjs/next-auth/issues/568

2. https://next-auth.js.org/configuration/pages

3. https://next-auth.js.org/configuration/providers/credentials


 

api route 설정

이 프로젝트에서 회원은 구글 OAuth 방식을 사용하고 있고, 비회원은 Credential 방식을 이용하여 로그인을 구현했습니다. CredentialsProvider 및 callbacks을 다음과 같이 설정합니다.

 

로그인 실행 순서

Credential: authorize() → signin() → jwt() → session()

OAuth: signin() → jwt() → session()  

export const authOptions: NextAuthOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_OAUTH_ID || '',
      clientSecret: process.env.GOOGLE_OAUTH_SECRET || '',
    }),
    CredentialsProvider({
      name: 'Nonmember',
      credentials: {}, // 사용자에게 입력받는 값이 있다면 설정
      async authorize(_, req) {
        const requestUID = req?.body?.uid; // client에 저장된 기존 비회원 uid
        const existNonmember =
          requestUID !== 'null' && (await findNonmember(requestUID)); // uid DB 조회

        if (existNonmember) {
          // DB에 저장된 비회원인 경우
          const user = {
            nonmember: true,
            id: existNonmember.uid,
          };

          return user;
        } else {
          // DB에 저장되지 않은 비회원인 경우
          const uid = createNonmemberUID(); // 비회원 uid 생성
          const UIDduplicateCheck = await findNonmember(uid); // 생성된 uid 중복 검사

          if (UIDduplicateCheck) {
            return null; // 중복 uid는 익명 로그인 실패
          }

          // 새로운 비회원 생성
          const user = {
            nonmember: true,
            id: uid,
          };

          return user;
        }
      },
    }),
  ],
  callbacks: {
     async signIn({ user: { id: uid, nonmember } }) {
       // 익명 로그인인 경우: authorize()에서 return 된 user
      if (!uid) {
        return false;
      }

      nonmember ? addNonMember(uid) : addMember(uid); // DB에 회원 저장
      return true;
    },
    async session({ session, token }) {
      const user = session?.user;

      if (user) {
        // getSession(), useSession()에서 uid와 nonmember 값을 이용하기 위해
        // jwt()의 token값을 session.user에 추가
        session.user = {
          ...user,
          uid: token.id as string,
          nonmember: token.nonmember as boolean,
        };
      }
      return session;
    },
    async jwt({ token, user }) {
      // session에 uid와 nonmember 값을 추가하기 위해
      // user에 저장된 값을 token에 할당
      if (user) {
        token.id = user.id;
        token.nonmember = user.nonmember;
      }
      return token;
    },
  },
  pages: {
    signIn: '/signin',
  },
  secret: process.env.NEXTAUTH_SECRET,
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

로그인 버튼 구현

이 프로젝트는 NextAuth의 custom 로그인 페이지를 사용하고 있습니다. Credentials 로그인을 이용하기 위해선 Server 컴포넌트의 csrfToken을 Client 컴포넌트에 전달해야 합니다.

// signin Server 컴포넌트
export default async function SignPage({
  searchParams: { callbackUrl },
}: Props) {
  const session = await getServerSession(authOptions);

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

  const providers = (await getProviders()) ?? {};
  const csrfToken = (await getCsrfToken()) ?? ''; // Credentials 로그인

  return (
    <section className={styles.container}>
      <Notice type='note' textList={NOTE_TEXT} />
      <Notice type='tip' textList={TIP_TEXT} />
      <div className={styles.divider} />
      <SignIn
        providers={providers}
        callbackUrl={callbackUrl ?? '/'}
        csrfToken={csrfToken}
      />
    </section>
  );
}

 

CredentialsProvider에서 설정한 name 값이 'Nonmember'인 경우, 익명 로그인 버튼을 렌더링합니다.

csrfToken은 <NonMemberLogin>에 전달합니다.

// Client 컴포넌트
type Props = {
  providers: Record<string, ClientSafeProvider>;
  callbackUrl: string;
  csrfToken: string;
};

export default function SignIn({ providers, callbackUrl, csrfToken }: Props) {
  return (
    <>
      {Object.values(providers).map(({ name, id }) => {
        switch (name) {
          case 'Nonmember':
            return (
              <NonMemberLogin key={id} csrfToken={csrfToken}>
                <LoginButton
                  name={name}
                  onClick={() =>
                    signIn(id, {
                      callbackUrl,
                      uid: localStorage.getItem('nonmember') ?? null, // client에 저장된 uid를 authorize()에 전달
                    })
                  }
                />
              </NonMemberLogin>
            );
          default:
            return (
              <LoginButton
                key={id}
                name={name}
                onClick={() => signIn(id, { callbackUrl })}
              />
            );
        }
      })}
    </>
  );
}

 

사용자로부터 어떠한 값도 입력받지 않기 때문에, 다음과 같이 <form>을 구성합니다.

export default function NonMemberLogin({ children, csrfToken }: Props) {
  return (
    <form
      className={styles.form}
      method='post'
      action='/api/auth/callback/credentials'
    >
      <input name='csrfToken' type='hidden' defaultValue={csrfToken} />
      {children}
    </form>
  );
}

 

구현

https://www.moncol.kr/ 에서 로그인 기능을 확인하실 수 있습니다.