만약 next에서 서버와 데이터베이스를 사용하려면 next-auth의 adapters 함수를 사용하면 되는데 나는 서버를 따로 두었기에 다른 방법으로 시도를 하려고 한다.
next에서 풀스택으로 사용하려면 아래 문서를 참고하면 된다.
https://authjs.dev/guides/adapters/creating-a-database-adapter
Next-auth callbacks
콜백은 작업이 수행될 때 발생하는 작업을 제어하는 데 사용할 수 있는 비동기 함수
공식문서를 보면 callbacks에 signIn, redirect, session, jwt 등이 있다.
여기에서는 signIn 콜백을 이용해 nestJS 서버랑 통신하고 데이터베이스에 값을 저장하려고 한다.
signIn 콜백을 이용한 데이터 베이스에 유저 정보 저장
src/auth.ts
import NextAuth from 'next-auth';
import KakaoProvider from 'next-auth/providers/kakao';
export const {
handlers: { GET, POST },
auth,
} = NextAuth({
providers: [
KakaoProvider({
clientId: process.env.AUTH_KAKAO_CLIENT_ID!,
clientSecret: process.env.AUTH_KAKAO_CLIENT_SECRET!,
}),
],
callbacks: {
async signIn({ user }) {
const { name, email, image } = user;
const response = await fetch('http://localhost:8000/auth/login', {
method: 'POST',
body: JSON.stringify({
name,
email,
image,
}),
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) return false;
return true;
},
},
});
카카오에서 인증이 완료되면 signIn 함수가 실행이 된다.
signIn 함수에는 매개변수로user, account, profile, email, credentials가 있다.
user는 카카오에서 넘어온 정보를 간추려 id, name, email, image 값들이 들어있다. 이 값은 next에서 session을 호출했을 때 보이는 정보이다.
account는 session의 더 자세한 정보들이 들어있다. 토큰 값과 만료시간 등등 들어있다.
profile은 카카오에서 넘어오는 모든 정보가 들어있다.
email과 credentials는 undefined라고 뜨는데 이유는 모르겠다.
나는 user 정보에 들어 있는 값들만 데이터베이스에 저장할 것이기에 user를 서버로 API를 날려서 회원가입이 완료되면 로그인을 하게하고 실패하면 로그인을 실패하게 로직을 구현하였다.
공식 문서
https://authjs.dev/guides/basics/callbacks#sign-in-callback
여기까지가 데이터베이스에 로그인 정보를 저장이다.
하지만 해당 next-auth session은 클라이언트에서만 쓰는 session이다. 보통은 서버 인증처리는 nestJS(서버)에서 session을 생성해클라이언트에 심어두고 이 session 값으로 요청시 매번 서버에서 확인을 해야 한다.
나는 jwt를 이용해 인증을 구현할 것이다. 로그인이 성공했을 때 서버에서 jwt 값을 넘겨주고 이 값을 쿠키에 저장을 할 것이다.
로그인시 쿠키에 jwt 값 저장
src/auth.ts
import NextAuth from 'next-auth';
import KakaoProvider from 'next-auth/providers/kakao';
import { cookies } from 'next/headers';
export const {
handlers: { GET, POST },
auth,
} = NextAuth({
providers: [
KakaoProvider({
clientId: process.env.AUTH_KAKAO_CLIENT_ID!,
clientSecret: process.env.AUTH_KAKAO_CLIENT_SECRET!,
}),
],
callbacks: {
async signIn({ user }) {
const { name, email, image } = user;
const response = await fetch('http://localhost:8000/auth/login', {
method: 'POST',
body: JSON.stringify({
name,
email,
image,
}),
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) return false;
const data = await response.json();
const { accessToken, refreshToken } = data;
// 브라우저에 쿠키를 심어주는 것
cookies().set('accessToken', accessToken);
cookies().set('refreshToken', refreshToken);
return true;
},
},
});
서버에서 응답값으로 jwt의 accessToken 값과 refreshToken값을 넘겨주어 쿠키에 심는 것이다.
next의 cookies 함수(서버에서만 사용가능)를 이용하면 쉽게 쿠키에 값을 저장할 수 있다.
src/app/mypage/page.tsx
'use client';
import { signOut } from 'next-auth/react';
import { deleteCookie } from 'cookies-next';
export default function MyPage() {
const handleLogOut = async () => {
await signOut({ redirect: true, callbackUrl: '/' }).then(() => {
deleteCookie('accessToken');
deleteCookie('refreshToken');
});
};
return <button onClick={handleLogOut}>LogOutButton</button>;
}
로그인 시 쿠키에 토큰을 심어줬으면 로그아웃 시에는 쿠키에 토큰 값을 삭제시키도록 하였다.
cookies-next를 쓴 이유는 아래에 나와있다.
lib/axios.ts
import axios from 'axios';
import { getCookie, setCookie } from 'cookies-next';
const baseURL = process.env.NEXT_PUBLIC_API_URI;
const isServer = typeof window === 'undefined';
export const axiosInstance = axios.create({
baseURL,
});
async function getRefreshToken() {
if (isServer) {
const { cookies } = await import('next/headers');
return cookies().get('refreshToken')?.value;
} else {
return getCookie('refreshToken');
}
}
async function getAccessToken() {
if (isServer) {
const { cookies } = await import('next/headers');
return cookies().get('accessToken')?.value;
} else {
return getCookie('accessToken');
}
}
async function updateAccessToken(token: string) {
if (isServer) {
const { cookies } = await import('next/headers');
return cookies().set('accessToken', token);
} else {
return setCookie('accessToken', token);
}
}
async function refreshAccessToken() {
const refreshToken = await getRefreshToken();
try {
const response = await axiosInstance.post('/auth/refresh', {
refreshToken,
});
const newAccessToken = response.data.accessToken;
updateAccessToken(newAccessToken);
return newAccessToken;
} catch (error) {
console.error('Error refreshing token:', error);
throw error;
}
}
axiosInstance.interceptors.request.use(
async function (config) {
const accessToken = await getAccessToken();
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
},
function (error) {
return Promise.reject(error);
},
);
axiosInstance.interceptors.response.use(
function (response) {
return response;
},
async function (error) {
const originalRequest = error.config;
if (error.response.data.message === '토큰 만료') {
try {
const newAccessToken = await refreshAccessToken();
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
return axios(originalRequest);
} catch (refreshError) {
console.error('Error refreshing token:', refreshError);
throw refreshError;
}
}
return Promise.reject(error);
},
);
next에서는 fetch 함수가 캐싱과 중복요청을 피할 수 있다고 fetch 함수를 사용하는 것을 권장한다.
하지만 fetch 함수는 인터셉터 기능을 직접 구현해야되서 나는 인터셉터 기능을 지원하는 axios와 캐싱기능이 잘 구현되어 있는 react-query를 사용할 것이기에 axios를 선택했다.
위 로직은 axios 인터셉터를 사용한 jwt 인증 구현 코드인데 server랑 client랑 쿠키를 가져오는 방법이 달라서 서버에서 요청할 때와 클라이언트에서 요청할 때 두가지를 한번에 쓰려고 구현한 코드이다.
서버를 구별할 수 있는 방법은 window 객체가 있는지 없는지를 판별하면 된다.
최상단에서 cookies를 불러오지 않고 함수안에서 불러오는 이유는 클라이언트 환경에서 사용할 수 없다고 에러 뜨기때문에 최상단에서 불러오지 않고 서버일때만 if문 안에서 불러와서 에러를 없앴다.
클라이언트에서는 next의 cookies를 사용하지 못하기때문에 cookies-next 라이브러리를 사용해서 클라이언트 환경에서는 라이브러리를 사용해서 쿠키 값을 들고온다.
깃허브 코드
https://github.com/msm0748/next-auth-5-study/commit/1a587955d266ee779310efca84356d1ab750ba7d
다음 글에는 관리자만 들어갈 수 있는 페이지를 구별하기 위해 서버에서 관리자인지 아닌지를 서버에서 판별해서 클라이언트 session 값에 추가
'Project > 코하루 마켓' 카테고리의 다른 글
Next-auth v5 session 커스텀 - NestJS (0) | 2024.01.02 |
---|---|
Next-auth v5 카카오 로그인 구현 (0) | 2023.12.22 |
Nextjs 공통 레이아웃 만들기 (0) | 2023.12.12 |