서버가 클라이언트로 보내는 Response는 단순히 예/아니오로 보낼 순 없다. 인증을 요구하는 몇몇 API 엔드포인트가 있을 수 있으므로, 인증된 사용자라는 증거가 필요하다. → 인증에는 권한을 위한 크리델셜 교환이 수반된다.
서버에 고유 식별자를 저장하는 방식으로 작동된다.
서버가 어떤 식별자도 저장하지 않는다. 대신 서버는 임의의 문자열이라 할 수 있는 토큰을 생성한다.
npm install next-auth
import { MongoClient } from "mongodb";
export async function connectToDatabase() {
const client = await MongoClient.connect(
"mongodb+srv://<username>:<password>@cluster0.stdfag3.mongodb.net/auth-demo?retryWrites=true&w=majority&appName=Cluster0"
);
return client;
}
import { hash } from "bcryptjs";
export async function hashPassword(password) {
const hashedPassword = await hash(password, 12);
return hashedPassword;
}
import { hashPassword } from "@/lib/auth";
import { connectToDatabase } from "@/lib/db";
export default async function handler(req, res) {
const data = req.body;
const { email, password } = data;
if (
!email ||
!email.includes("@") ||
!password ||
password.trim().length < 7
) {
res.status(422).json({
message:
"Invalid Input - Password should also be at least 7 characters long.",
});
return;
}
const client = await connectToDatabase();
const db = client.db();
const hashedPassword = await hashPassword(password);
const result = await db.collection("users").insertOne({
email: email,
password: hashedPassword, // 비밀번호는 비밀번호 그 자체로 저장하면 안된다. 암호화한 뒤에 저장한다.
});
res.status(200).json({ message: "Created User" });
}
import { useState, useRef } from "react";
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();
function switchAuthModeHandler() {
setIsLogin((prevState) => !prevState);
}
async function submitHandler(event) {
event.preventDefault();
const enteredEmail = emailInputRef.current.value;
const enteredPassword = passwordInputRef.current.value;
if (isLogin) {
} else {
try {
const result = await createUser(enteredEmail, enteredPassword);
console.log(result);
} catch (err) {
console.log(err);
}
}
}
return (
<section className={classes.auth}>
<h1>{isLogin ? "Login" : "Sign Up"}</h1>
<form onSubmit={submitHandler}>
<div className={classes.control}>
<label htmlFor="email">Your Email</label>
<input type="email" id="email" required ref={emailInputRef} />
</div>
<div className={classes.control}>
<label htmlFor="password">Your Password</label>
<input
type="password"
id="password"
required
ref={passwordInputRef}
/>
</div>
<div className={classes.actions}>
<button>{isLogin ? "Login" : "Create Account"}</button>
<button
type="button"
className={classes.toggle}
onClick={switchAuthModeHandler}
>
{isLogin ? "Create new account" : "Login with existing account"}
</button>
</div>
</form>
</section>
);
}
export default AuthForm;
export default async function handler(req, res) {
if (req.method === "POST") {
// ...
}
}
import { hashPassword } from "@/lib/auth";
import { connectToDatabase } from "@/lib/db";
export default async function handler(req, res) {
if (req.method === "POST") {
const data = req.body;
const { email, password } = data;
if (
!email ||
!email.includes("@") ||
!password ||
password.trim().length < 7
) {
res.status(422).json({
message:
"Invalid Input - Password should also be at least 7 characters long.",
});
return;
}
const client = await connectToDatabase();
const db = client.db();
// 사용자가 이미 존재하는지 파악하기
const existingUser = await db.collection("users").findOne({ email: email });
if (existingUser) {
res.status(422).json({ message: "이미 존재하는 유저입니다." });
client.close();
return;
}
// ========================
const hashedPassword = await hashPassword(password);
const result = await db.collection("users").insertOne({
email: email,
password: hashedPassword, // 비밀번호는 비밀번호 그 자체로 저장하면 안된다. 암호화한 뒤에 저장한다.
});
res.status(200).json({ message: "Created User" });
client.close();
}
}
import { hash, compare } from "bcryptjs";
export async function verifyPassword(password, hashedPassword) {
// compare ; plain text 비밀번호가 해싱된 비밀번호와 일치하는지 확인하도록 도와준다.
const isValid = await compare(password, hashedPassword);
return isValid;
}
import { verifyPassword } from "@/lib/auth";
import { connectToDatabase } from "@/lib/db";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
export default NextAuth({
// NextAuth()를 통해 handler가 리턴된다.
session: {
strategy: "jwt", // JSON Web Token 사용 - default 값
},
providers: [
CredentialsProvider({
// credentials: {} // -> 자동으로 NextAuth에서 로그인 폼 생성
async authorize(credentials) {
// 들어오는 로그인 요청을 Next.js가 수신할 때 Next.js가 개발자 대신 호출해주는 메서드이다.
const client = await connectToDatabase();
const usersCollection = client.db().collection("users");
const user = await usersCollection.findOne({
email: credentials.email,
});
if (!user) {
client.close();
throw new Error("No user found!");
// 만약 authorize 내에서 오류가 발생하면 authorize가 생성한 프로미스를 거부하고 기본적으로 클라이언트를 다른 페이지에 리디렉션한다.
}
const isValid = await verifyPassword(
credentials.password,
user.password
);
if (!isValid) {
client.close();
throw new Error("로그인 불가!");
}
client.close();
// 로그인 성공
return { email: user.email }; // authorize내에서 object return함으로써 NextAuth에 인증이 성공했다고 알림
},
}),
],
});
import { useState, useRef } from "react";
import { signIn } from "next-auth/react";
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();
function switchAuthModeHandler() {
setIsLogin((prevState) => !prevState);
}
async function submitHandler(event) {
event.preventDefault();
const enteredEmail = emailInputRef.current.value;
const enteredPassword = passwordInputRef.current.value;
// ===== LOGIN =====
if (isLogin) {
// 여기선 HTTP Request를 사용하지 않는다.
const result = await signIn("credentials", {
redirect: false,
email: enteredEmail,
password: enteredPassword,
}); // 이 함수를 컴포넌트에서 호출하면 signIn 요청을 자동으로 전송하는 역할을 한다.
console.log(result);
// ===============
} else {
try {
const result = await createUser(enteredEmail, enteredPassword);
console.log(result);
} catch (err) {
console.log(err);
}
}
}
return {
/* ... */
};
}
export default AuthForm;
{redirect:false}
로 설정하여 입력란에 메시지만 출력할 수 있도록 한다.authorize(credentials)
의 credentials라는 매개변수는 signIn의 인수에서 전달받는다.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;
import Link from "next/link";
import { useSession } from "next-auth/react";
import classes from "./main-navigation.module.css";
function MainNavigation() {
// session : 세션의 활성 상태를 나타냄, status: 사용자가 현재 페이지에 로그인된 상태인지 아닌지 확인
const { data: session, status } = useSession();
console.log(status);
console.log(session);
return (
<header className={classes.header}>
<Link href="/">
<div className={classes.logo}>Next Auth</div>
</Link>
<nav>
<ul>
{!session && status === "unauthenticated" && (
<li>
<Link href="/auth">Login</Link>
</li>
)}
{session && status === "authenticated" && (
<li>
<Link href="/profile">Profile</Link>
</li>
)}
{session && status === "authenticated" && (
<li>
<button>Logout</button>
</li>
)}
</ul>
</nav>
</header>
);
}
export default MainNavigation;
import Link from "next/link";
import { useSession, signOut } from "next-auth/react";
import classes from "./main-navigation.module.css";
function MainNavigation() {
// session : 세션의 활성 상태를 나타냄, status: 사용자가 현재 페이지에 로그인된 상태인지 아닌지 확인
const { data: session, status } = useSession();
function logoutHandler() {
signOut();
}
return (
<header className={classes.header}>
<Link href="/">
<div className={classes.logo}>Next Auth</div>
</Link>
<nav>
<ul>
{!session && status === "unauthenticated" && (
<li>
<Link href="/auth">Login</Link>
</li>
)}
{session && status === "authenticated" && (
<li>
<Link href="/profile">Profile</Link>
</li>
)}
{session && status === "authenticated" && (
<li>
<button onClick={logoutHandler}>Logout</button>
</li>
)}
</ul>
</nav>
</header>
);
}
export default MainNavigation;
signOut
: 프로미스를 반환하여 처리가 완료됨을 알려준다.