새소식

Next.js

[Next.js]Next-Auth를 이용한 custom 로그인

  • -

🐶서론

next.js 13버전이 나오고, 전반적으로 큰 변화가 있어 12버전만을 붙들고 있어서는 안되겠다 ! 싶어 13버전으로 기존프로젝트를 마이그레이션 하기 시작했다. 

기존 로그인폼에서는 useState를 이용해 아이디와 비밀번호를 받았기 때문에, 당연히 로그인은 client side rendering이 되어야 한다 생각했다. 

next-auth 라는 라이브러리의 존재를 알고있었지만, 우리 프로젝트에는 백엔드가 있었고 JWT토큰을 사용하여 쿠키에 저장해야했기에 굳이? 싶어 사용하지 않았다.

마이그레이션 계획을 하면서, 굉장히 편한 라이브러리 라는것을 알게되었고 기존페이지들의 렌더링방식 결정에 조금 더 도움이 되지 않을까 하는 생각에 도입하게 되었다. 

 

 

1️⃣ Next-Auth 란 무엇인가? 

next-auth는 next.js에서 로그인을 쉽게 구현할 수 있도록 관련기능을 제공하는 3rd Party라이브러리이다. 

next-auth 공식문서에서는 다음과 같이 소개하고 있다 (참고)

 

⭐️ Next-auth 사용방법

 API 라우트 구성하기 

next auth에서 로그인을 구현하기 위해서는 동적API route를 이용해서 구현해야한다. (서버사이드에서 동작)

 

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

import { NextApiRequest } from "next";
import NextAuth, { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";


export  const authOptions: NextAuthOptions ={
    providers: [
      
    ],
   }

export default NextAuth(authOptions)

next.js가 13버전으로 업데이트 되면서 pages 폴더가 없어졌는데, next-auth가 이 부분에 대해서는 아직 업데이트가 진행되지 않은 것같았다. 따라서 나는 src 폴더 내에 pages폴더를 생성하여 다음과 같이 Provider를 설정해주었다. 

 

next-auth가 간편하게 로그인과 로그아웃을 도와주는데에는 next-auth에서 제공하는 signIn/singOut 함수의 역할이 매우 크다. 이 두 함수 모두 내가 설정한 이 api로 요청을 보내는 것. 

 

api route를 이용해서 설정했기 때문에, useSession() Hook(client 단에서 session 정보를 불러올 수 있게함/ 서버단에서 세션정보가 필요할 경우 getServerSession())을 사용하기 위해 최상단에 Provider를 심어주어야한다. 

 

src/context/AuthContext.tsx

"use client";
import { SessionProvider, useSession } from "next-auth/react";

type Props = {
  children: React.ReactNode;
};
export default function AuthContext({ children }: Props) {
  return <SessionProvider>{children}</SessionProvider>;
}

src/app/layout.tsx

import { Noto_Sans_KR } from "next/font/google";
import ReactQueryProvider from "./ReactQueryProvider";
import Header from "@/components/view/Header";
import AuthContext from "@/context/AuthContext";



export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={notoSansKr.className}>
      <body>
        <AuthContext>
            <Header />
            <ReactQueryProvider>{children}</ReactQueryProvider>
        </AuthContext>
      </body>
    </html>
  );
}

 

 

Credential Provider 설정하기

next-auth에서는 다양한 provider를 제공하는데, 현재 프로젝트는 우리가 로그인에 필요한 정보를 구성하여 로그인을 구현하고 있기 때문에, credential Provider를 사용했다. 

 

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

import { NextApiRequest } from "next";
import NextAuth, { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";


export  const authOptions: NextAuthOptions ={
    providers: [
        CredentialsProvider({
            id: "user-credentials",
            name: "Credentials",
            credentials: {
                email: { label: "email", type: "email", placeholder: "아이디를 입력하세요" },
                password: { label: "Password", type: "password" }
            },
            
            async authorize(credentials: Record<any, any> | undefined, req: NextApiRequest | undefined) {
//credentials.status === 401 이면 없는 유저로 signup페이지로 리다이렉트 시키기
                    const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/login`, {
                    method: "POST",
                    body: JSON.stringify(credentials),
                    headers: {"Content-Type": "application/json"}
                    })
                    if(res.status === 401){
                        console.log("에러에러에러 없는 유저다")
                        throw new Error("로그인 실패")
                    } else {
                        const user = await res.json();
                        return user;
                    }
            }
        })
    ],

    pages: {
        signIn: '/login',
    }
}

export default NextAuth(authOptions)

credentials 를 통해 개발자가 어떤 정보를 로그인 시 받을지 정할 수 있다.

현 프로젝트에서는 유저가 회원가입한 이메일과 비밀번호를 통해 로그인을 하고 있으므로 email, password 두가지를 받고 있다.

next-auth는 로그인페이지나 폼 자체를 구현하지 않아도 기본적인 로그인페이지를 제공하고 있다.

기본적인 로그인페이지가 아닌, 현 프로젝트에서 사용하고 있는 디자인으로 로그인 페이지를 구성하기 위해 나는 signIn : '/login'으로 돌려주었다. 

 

authorize은 credentials에 입력한 값을 통해 로그인이 가능한지 가능하지 않은지 판단하여 제어할 수 있는 함수이다. 

나는 백엔드 서버로 입력 받은 값을 보내주어서 응답을 받고, 그 응답에 따라 처리 해야되어서 응답이 401인 경우 에러를 던져 주었고, 아닌 경우는 user 객체를 반환했다. 

 

 

Callback

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

    callbacks: {
        async jwt({user, token, account}){
            if(user) {
                token.Authorization = user?.accessToken
                token.refreshToken = user.refreshToken
            }
            return token;
        },
        async session({token, session}){
            session.Authorization = token.Authorization
            session.RefreshToken = token.refreshToken
            return session;

        },

    },

콜백은 로그인 한 뒤 실행된다.

jwt 콜백은 JWT가 생성되거나 업데이트 되었을 때 실행된다. JWT를 자동으로 쿠기에 저장한다. 

프로젝트에서는 토큰을 클라이언트로 노출시켜야하는 상황이 필요 했기에, session 콜백을 함께 사용해주었다. 

authorize 함수를 실행한 뒤 백엔드에서 온 응답 중 access token 을 토큰 객체에 담아 보내주었고 session 콜백은 이를 받아 활용할 수 있다. 

 

 

axios default header 

로그인이 되었을 때 로그인 정보를 하위컴포넌트에 알리고 axios 요청시에 필요한 헤더를 설정해줘야한다.

useSession()훅 (클라이언트단에서만 사용가능)을 사용해야하기 때문에 기존에 설정한 sessionProvider 하위에 위치 해야한다.

 

src/app/layout.tsx

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={notoSansKr.className}>
      <body>
        <AuthContext>
          <AuthroizationHeader>
            <Header />
            <ReactQueryProvider>{children}</ReactQueryProvider>
            <FloatingBtn />
          </AuthroizationHeader>
        </AuthContext>
      </body>
    </html>
  );
}

 

src/context/AuthroizationHeader.tsx

"use client";
import { setCookie } from "@/util/cookies";
import { Session } from "next-auth";
import { useSession } from "next-auth/react";
import { useEffect } from "react";

type Props = {
  children: React.ReactNode;
};

type SessionData = {
  Authorization: string;
  RefreshToken: string;
  expires: string;
};

type SessionType = {
  data: SessionData | any;
  status: string;
  update: any;
};

export default function AuthroizationHeader({ children }: Props) {
  const { status, data: session }: SessionType = useSession();

  const isLogin = !!session && status === "authenticated";
  const accesstoken = isLogin ? session.Authorization : "";
  const refreshToken = isLogin ? session.RefreshToken : "";

  useEffect(() => {
    setCookie("Authroization", accesstoken);
    setCookie("RefreshToken", refreshToken);
  }, [isLogin]);
  return <>{children}</>;
}

 

미들웨어

로그인 한 유저가 로그인페이지에 진입하려하면?, 로그인 되지 않은 유저가 마이페이지에 진입을 하게 된다면? 

이 두 상황 모두 페이지의 진입을 막고 리다이렉트 시킬 수 있는 기능이 필요하다. 

해당 기능은 next.js middleware를 통해 구현이 가능하다.(참고)

 

next-auth에서도 분명 미들웨어 기능을 지원할 것이라 생각해서 한참을 찾아 본 결과 미들웨어 기능을 지원하고 있다. 

미들웨어는 app과 동일한 레벨에 생성해야한다 (src/middleware.ts)

 

현재 프로젝트에서 리다이렉트 되어야하는 경우는 로그인이 되어있을때와 되어있지 않을때, 두가지로 나뉜다.

  • 로그인이 되었을 때 (withAuth) : 마이페이지, 글작성페이지 접근가능
  • 로그인이 되어있지 않을 때 (withOutAuth) : 로그인페이지, 회원가입페이지 접근 가능
import { getToken } from "next-auth/jwt";
import { NextRequest, NextResponse } from "next/server";




const FALLBACK_URL =""
const withAuth = async (req: NextRequest, token: boolean) =>{

    const url = req.nextUrl.clone();
    const {pathname} = req.nextUrl;

    if(!token) {
        //토큰값이 falsy한 사용자가 withAuth페이지에 진입하려하면,
        //미들웨어에서 req객체 중에 NextUrl 안에 담긴 pathname을 쿼리스트링을 붙여서 로그인페이지로 리다이렉트 시킴
        url.pathname = '/login';
        //로그인하면 이전페이지로 이동하기 위해서 쿼리스트링사용하여 붙여줌. 
        url.search =`callbackUrl=${pathname}`;

        return NextResponse.redirect(url)
    }
}

const withOutAuth = async (req:NextRequest, token: boolean, to: string | null) => {

    const url = req.nextUrl.clone();
    const {pathname} = req.nextUrl;

    if(token) {
        url.pathname = to ?? FALLBACK_URL;
        url.search = "";

        return NextResponse.redirect(url)
    }
}


const withAuthList = ["/mypage", "/write"]
const withOutAuthList = ["/login", "/signup"]

export default async function middleware(req: NextRequest) {

    //미들웨어 쿠키
    let cookie = req.cookies.get("Authroization")?.value || ""


    //setting Headers
    const requestHeaders = new Headers(req.headers);
    requestHeaders.set("Authroization", cookie);


    const token = await getToken({ req });
    const {searchParams} = req.nextUrl;
    const callbackUrl = searchParams.get("callbackUrl");
    const pathname = req.nextUrl.pathname

    const isWithAuth = withAuthList.includes(pathname);
    const isWithOutAuth = withOutAuthList.includes(pathname);

    if(isWithAuth) return withAuth(req, !!token)
    if(isWithOutAuth) return withOutAuth(req, !!token, callbackUrl)

  }
  
  // 미들웨어가 실행될 특정 pathname을 지정하면, 해당 pathname에서만 실행 가능 
  export const config = {
      mathcher : [...withAuthList, ...withOutAuthList]
  }

 

 

 

 

ref

https://next-auth.js.org/

https://velog.io/@dosomething/Next-auth-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84

'Next.js' 카테고리의 다른 글

[Next.js] SSR, CSR, SSG, Hydrate  (0) 2023.01.30
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.