NextAuth.js를 이용한 인증 - 2

·2024년 5월 17일
0

NextJS

목록 보기
19/26
post-thumbnail

📌 페이지 가드(라우트 보호) 추가하기

📖 클라이언트 사이드 페이지 가드(라우트 보호) 추가하기

  • 인증되지 않으면 접근하지 못하는 페이지로 가서 설정

💎 /components/profile/user-profile.js

import ProfileForm from "./profile-form";
import classes from "./user-profile.module.css";
import { useSession, getSession } from "next-auth/react";
import { useEffect, useState } from "react";

function UserProfile() {
  // Redirect away if NOT auth
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    getSession().then((session) => {
      if (!session) {
        window.location.href = "/auth";
      } else {
        setIsLoading(false);
      }
    });
  }, []);

  if (isLoading) {
    return <p className={classes.profile}>Loading...</p>;
  }

  return (
    <section className={classes.profile}>
      <h1>Your User Profile</h1>
      <ProfileForm />
    </section>
  );
}

export default UserProfile;
  • useSession: 즉시 세션과 상태를 가져오고 세션데이터가 가져와지면 세션과 상태 전부 변경할 수 있다. 로그아웃해서 세션이 없다면 세션은 변경되지 않는다.
  • getSession : 새 요청을 보내서 최근 세션 데이터를 가져온다. 세션을 가져오는 동안 상태를 관리하며 적절하게 표현할 수 있다.

  • 로그인을 하지 않았는데 /profile로 접근했을 때, /auth로 다시 리디렉션 된다.

📖 서버 사이드 페이지 가드 추가하기

  • Next.js는 클라이언트 사이드 코드 뿐만 아니라 서버 사이드 코드도 사용할 수 있다.
  • 서버 사이드 코드에서 인증 여부를 확인해 다른 페이지 콘텐츠를 반환하고 사용자가 인증되지 않은 경우에는 리디렉션하도록 하자!

💎 /pages/profile.js

export async function getServerSideProps(context) {
  const session = await getServerSession(context.req, context.res);
  console.log("session=>", session);

  if (!session) {
    return {
      redirect: {
        destination: "/auth",
        permanent: false, // 이 리디렉션이 영구적으로 적용되는 건지 임시로 리디렉션되는 건지 알려준다. => 이 리디렉션은 로그인 되지 않았을 때만 작용하는 임식 리디렉션임
      },
    };
  }

  return {
    props: { session },
  };
}

export default ProfilePage;

💎 오류 발생 및 해결

🔗 참고 1

🔗 참고 2

[next-auth][error][JWT_SESSION_ERROR] > https://next-auth.js.org/errors#jwt_session_error decryption operation failed {
message: 'decryption operation failed',
stack: 'JWEDecryptionFailed: decryption operation failed\n'

  1. openssl rand -base64 32
  2. .env 에서 다음과 같이 작성
        NEXTAUTH_SECRET='openssl rand -base64 32 결과값'
  3. /pages/api/auth/[...nextauth].js에서 다음을 추가 작성
    NextAuth({
      secret: process.env.NEXTAUTH_SECRET,
      // ...
    });

💎 /pages/profile.js

import UserProfile from "../components/profile/user-profile";
import { getServerSession } from "next-auth/next";

function ProfilePage() {
  return <UserProfile />;
}

export async function getServerSideProps(context) {
  const session = await getServerSession(context.req, context.res);
  console.log("session=>", session);

  if (!session) {
    return {
      redirect: {
        destination: "/auth",
        permanent: false,
      },
    };
  }

  if (session && session.user) {
    session.user.name = session.user.name || null;
    session.user.image = session.user.image || null;
  }

  return {
    props: { session },
  };
}

export default ProfilePage;
  • 이렇게 작성하면, 로그인 하지 않은 상태에서 /profile로 접근한 경우 즉시 /auth로 리디렉션 한다.
  • 만약 로그인해서 성공한 경우, 프로필 페이지에 성공적으로 접근할 수 있다.

session.user.name, session.user.image가 undefined를 포함해 JSON으로 직렬화하는데 오류가 발생하게 된다. 따라서 이를 방지하기 위해 getServerSideProps 내에서 두 값이 undefined이면 null로 변환하는 작업을 진행했다.


📖 인증 페이지 보호하기

  • 만약 유저가 로그인에 성공한 상태인 경우, /auth 페이지로 갈 필요가 없다. 따라서 로그인 이후 리디렉션하는 작업을 진행할 예정

💎 /components/auth/auth-form.js

import { useState, useRef } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/router"; // useRouter를 이용하여 redirect
import classes from "./auth-form.module.css";

async function createUser(email, password) {
  const response = await fetch("/api/auth/signup", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, password }),
  });

  const data = await response.json();
  if (!response.ok) {
    throw new Error(data.message || "Somethine went wrong!");
  }
  return data;
}

function AuthForm() {
  const [isLogin, setIsLogin] = useState(true);
  const emailInputRef = useRef();
  const passwordInputRef = useRef();
  const router = useRouter(); // useRouter를 이용하여 redirect

  function switchAuthModeHandler() {
    setIsLogin((prevState) => !prevState);
  }

  async function submitHandler(event) {
    event.preventDefault();
    const enteredEmail = emailInputRef.current.value;
    const enteredPassword = passwordInputRef.current.value;

    if (isLogin) {
      const result = await signIn("credentials", {
        redirect: false,
        email: enteredEmail,
        password: enteredPassword,
      });
      console.log(result);

      // useRouter를 이용하여 redirect
      if (!result.error) {
        router.replace("/profile");
      }
    } else {
      try {
        const result = await createUser(enteredEmail, enteredPassword);
        console.log(result);
      } catch (err) {
        console.log(err);
      }
    }
  }

  return {
    /*...*/
  };
}

export default AuthForm;

💎 /pages/auth.js

import AuthForm from "../components/auth/auth-form";
import { getSession } from "next-auth/react";
import { useEffect, useState } from "react";
import { useRouter } from "next/router";

function AuthPage() {
  const [isLoading, setIsLoading] = useState(true);
  const router = useRouter();
  useEffect(() => {
    getSession().then((session) => {
      if (session) {
        router.replace("/");
      } else {
        setIsLoading(false);
      }
    });
  }, [router]);

  if (isLoading) {
    return <p>Loading...</p>;
  }

  return <AuthForm />;
}

export default AuthPage;

📌 next-auth 세션 제공자 컴포넌트 사용하기

/profile에 접근할 때 getServerSideProps를 이용해 세션을 확인하므로 인증된 사용자인지 아닌지를 판별할 수 있다. 따라서 /components/main-navigation.js에서 useSession에서 세션을 확인할 필요가 없다.

/pages/_app.js에서 다음과 같이 작성한다.(이미 작성했지만 강의 순서 상 현재 작성)
🔗 참고

import Layout from "../components/layout/layout";
import "../styles/globals.css";
import { SessionProvider } from "next-auth/react";

function MyApp({ Component, pageProps: { session, ...pageProps } }) {
  return (
    <SessionProvider session={session}>
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </SessionProvider>
  );
}

export default MyApp;
  • SessionProvider에서 session 프로퍼티에 이미 가지고 있을 수 있는 세션을 전달한다.
  • 루트 컴포넌트가 pageProps(getStaticProps, getServerSideProps가 결정하는 프로퍼티)도 가지고 온다.
  • 따라서 /pages/profile.js에서 getServerSideProps를 통한 props.session은 유효성 검사까지 프로퍼티이므로 pageProps에 속해질 것이다...! 대부분의 페이지에서는 session 프로퍼티가 없으므로 값이 정의되지 않을 테지만 /profile의 경우 getServerSideProps에서 props.session으로 정의되었다.

📌 추가적인 인증 요구사항 분석하기 - 비밀번호 변경하기

  • 프로젝트에서 인증이 완료된 사용자만이 어떤 동작을 수행할 수 있도록 하고 싶을 수 있다.(ex. 블로그 포스트/삭제 등)
  • 따라서 인증된 사용자만 API 라우트의 작업을 트리거하고 싶다면, 해당 API에서 인증된 사용자가 보낸 요청인지 확인할 필요가 있다.

📖 API 라우트 보호하기

💎 /pages/api/user/change-password.js

import { getServerSession } from "next-auth/next";

export default async function handler(req, res) {
  if (req.method !== "PATCH") {
    return;
  }

  // 인증된 사용자인지 확인
  const session = await getServerSession(req);
  if (!session) {
    res.status(401).json({ message: "Not authenticated" });
    return;
  }
}

🔗 API Route에서 getServerSession


📖 비밀번호 변경 논리 추가하기

💎 /pages/api/user/change-password.js

import { hashPassword, verifyPassword } from "@/lib/auth";
import { connectToDatabase } from "@/lib/db";
import { getServerSession } from "next-auth/next";

export default async function handler(req, res) {
  if (req.method !== "PATCH") {
    return;
  }

  // 인증된 사용자인지 확인
  const session = await getServerSession(req, res);
  if (!session) {
    res.status(401).json({ message: "Not authenticated" });
    return;
  }

  const oldPassword = req.body.oldPassword;
  const newPassword = req.body.newPassword;
  const userEmail = session.user.email;

  const client = await connectToDatabase();
  const usersCollection = client.db().collection("users");
  const user = await usersCollection.findOne({ email: userEmail });

  if (!user) {
    res.status(404).json({ message: "사용자를 찾을 수 없습니다." });
    client.close();
    return;
  }

  const currentPassword = user.password;
  const passwordAreEqual = await verifyPassword(oldPassword, currentPassword);
  if (!passwordAreEqual) {
    res.status(403).json({ message: "유효하지 않은 비밀번호입니다." });
    client.close();
    return;
  }

  const hashedPassword = await hashPassword(newPassword);

  const result = await usersCollection.updateOne(
    { email: userEmail },
    { $set: { password: hashedPassword } }
  );

  // error handling

  client.close();
  res.status(200).json({ message: "성공적으로 비밀번호를 변경하였습니다." });
}

📖 프론트엔드에서 비밀번호 변경 요청 전송하기

💎 /components/profile/profile-form.js

import { useRef } from "react";
import classes from "./profile-form.module.css";

function ProfileForm({ onChangePassword }) {
  const newPasswordRef = useRef();
  const oldPasswordRef = useRef();

  async function submitChangePassword(event) {
    event.preventDefault();
    const enteredOldPassword = oldPasswordRef.current.value;
    const enteredNewPassword = newPasswordRef.current.value;

    // optional : validation..

    onChangePassword({
      oldPassword: enteredOldPassword,
      newPassword: enteredNewPassword,
    });
  }
  return (
    <form className={classes.form} onSubmit={submitChangePassword}>
      <div className={classes.control}>
        <label htmlFor="new-password">New Password</label>
        <input type="password" id="new-password" ref={newPasswordRef} />
      </div>
      <div className={classes.control}>
        <label htmlFor="old-password">Old Password</label>
        <input type="password" id="old-password" ref={oldPasswordRef} />
      </div>
      <div className={classes.action}>
        <button>Change Password</button>
      </div>
    </form>
  );
}

export default ProfileForm;

💎 /components/profile/user-profile.js

import ProfileForm from "./profile-form";
import classes from "./user-profile.module.css";
// import { useSession, getSession } from "next-auth/react";
// import { useEffect, useState } from "react";

function UserProfile() {
  // Redirect away if NOT auth
  // const [isLoading, setIsLoading] = useState(true);

  // useEffect(() => {
  //   getSession().then((session) => {
  //     if (!session) {
  //       window.location.href = "/auth";
  //     } else {
  //       setIsLoading(false);
  //     }
  //   });
  // }, []);

  // if (isLoading) {
  //   return <p className={classes.profile}>Loading...</p>;
  // }

  async function changePasswordHandler(passwordData) {
    const response = await fetch("/api/user/change-password", {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(passwordData),
    });

    const data = await response.json();
    console.log(data);
  }

  return (
    <section className={classes.profile}>
      <h1>Your User Profile</h1>
      <ProfileForm onChangePassword={changePasswordHandler} />
    </section>
  );
}

export default UserProfile;
  • 이전 비밀번호를 잘못 입력한 경우

  • 이전 비밀번호를 제대로 입력한 경우(성공 케이스)

    1. 바꾸기 이전

    2. 바꾸기 이후

🚨 (+) NEXTAUTH_URL은 인증을 사용하는 어플리케이션을 배포할 때 추가하고자 하는 환경변수이다. 🔗 참고

0개의 댓글

관련 채용 정보