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/ 에서 로그인 기능을 확인하실 수 있습니다.