목차
next-auth를 사용하면서 차근차근 고민했던 부분을 정리하고자 한다.
next-auth 도입 이유
- csrf 토큰을 통한 보안 증가
이때 Next-Auth의 CSRF는 “Next-Auth 자신”과 브라우저 사이를 보호하는 것입니다.
브라우저와 백엔드 사이를 보호하는 것이 아니기 때문에 그건 따로 설정해 주어야 합니다.
- http-only 설정을 통한 xss 공격 보호
- 추후 소셜 로그인 확장성에 대한 유연함
++추가
1. 루시아, better-auth등은 db 구축 기반의 인증 방식 사용
2. next-auth의 자체 쿠키에 암호화 방식으로 안전성 보장
간략한 사용 방법
이번 글은 커스텀 로그인 방식을 사용할 것이기 때문에 CredentialProvider를 중점적으로 보면 될 것 같다.
전체 코드
import NextAuth, { Session } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import KakaoProvider from 'next-auth/providers/kakao';
import getTokenExpiration from './shared/lib/getTokenExpiration';
import serverApi from './shared/api/server-api';
import {
LoginSuccessResponse,
RoleResponse,
} from './features/login/model/type';
import UserInfoType from './entities/my/model/type';
// TODO: 추후 루시아로 변경
export const { auth, handlers, signIn, signOut } = NextAuth({
secret: process.env.NEXT_AUTH_SECRET as string,
trustHost: true,
pages: {
error: '/login?callbackUrl=/',
},
providers: [
KakaoProvider({
clientId: process.env.KAKAO_CLIENT_ID as string,
clientSecret: process.env.KAKAO_SECRET_KEY as string,
}),
CredentialsProvider({
name: 'credentials',
credentials: {
studentId: { label: '학번', type: 'text' },
password: { label: '비밀번호', type: 'password' },
},
async authorize(credentials): Promise<any> {
try {
const response = await serverApi.post('users/auth/login', {
json: credentials,
});
const data: LoginSuccessResponse = await response.json();
const userResponse = await serverApi.get('users', {
headers: {
Authorization: `Bearer ${data.data.accessToken}`,
},
});
const userData: { data: { user: UserInfoType } } =
await userResponse.json();
return {
accessToken: data.data.accessToken,
refreshToken: data.data.refreshToken,
user: userData.data.user,
};
} catch (error) {
console.error('[authorize error]', error);
return null;
}
},
}),
],
callbacks: {
jwt: async ({ token, account, user }) => {
// 소셜 로그인
if (account?.provider === 'kakao') {
return {
...token,
accessToken: account.access_token,
refreshToken: account.refresh_token,
};
}
if (user && account?.provider === 'credentials') {
const expiredTime = getTokenExpiration(user.accessToken as string);
try {
const headers = {
Authorization: `Bearer ${user.accessToken}`,
};
const rolesRes = await serverApi.get('users/roles', { headers });
const rolesData: RoleResponse = await rolesRes.json();
console.log('token', token);
return {
...token,
accessToken: user.accessToken,
refreshToken: user.refreshToken,
expiresAt: expiredTime,
user: user.user,
role: rolesData.data.role,
};
} catch (error) {
console.error('[role fetch error]', error);
return {
...token,
accessToken: user.accessToken,
refreshToken: user.refreshToken,
expiresAt: expiredTime,
user: user.user,
};
}
}
if (Date.now() > (token.expiresAt as number)) {
try {
const response = await serverApi.post('users/auth/refresh', {
headers: {
Authorization: `Bearer ${token.refreshToken}`,
},
});
const data: LoginSuccessResponse = await response.json();
return {
...token,
accessToken: data.data.accessToken,
expiresAt: getTokenExpiration(data.data.accessToken),
};
} catch (error) {
console.error('[refresh error]', error);
signOut();
return null;
}
}
return token;
},
// TODO: 추후 타입 수정
session: async ({ session, token }) => {
return {
...session,
expiresAt: token.expiresAt,
user: token.user,
role: token.role,
} as Session;
},
redirect: async ({ url, baseUrl }) => {
if (url.startsWith('/')) return `${baseUrl}${url}`;
if (url) {
const { search, origin } = new URL(url);
const callbackUrl = new URLSearchParams(search).get('callbackUrl');
if (callbackUrl)
return callbackUrl.startsWith('/')
? `${baseUrl}${callbackUrl}`
: callbackUrl;
if (origin === baseUrl) return url;
}
return baseUrl;
},
},
});
이 부분이 로그인 시 사용되게 된다.
아래에 자신의 로그인 api 엔드포인트와 함께 응답값을 작성해 주면 된다.
async authorize(credentials): Promise<any> {
try {
const response = await serverApi.post('users/auth/login', {
json: credentials,
});
const data: LoginSuccessResponse = await response.json();
const userResponse = await serverApi.get('users', {
headers: {
Authorization: `Bearer ${data.data.accessToken}`,
},
});
const userData: { data: { user: UserInfoType } } =
await userResponse.json();
return {
accessToken: data.data.accessToken,
refreshToken: data.data.refreshToken,
user: userData.data.user,
};
} catch (error) {
console.error('[authorize error]', error);
return null;
}
},
아래 부분은 매 요청 시 마다 실행되게 된다.
크게 두가지 부분으로 나뉜다.
1. jwt 토큰을 저장하는 부분
2. 토큰 만료 시 재발급하는 부분
callbacks: {
jwt: async ({ token, account, user }) => {
// 소셜 로그인은 하지 않으므로 넘어간다.
if (account?.provider === 'kakao') {
return {
...token,
accessToken: account.access_token,
refreshToken: account.refresh_token,
};
}
// 커스텀 로그인을 여기서 분기한다.
if (user && account?.provider === 'credentials') {
const expiredTime = getTokenExpiration(user.accessToken as string);
try {
const headers = {
Authorization: `Bearer ${user.accessToken}`,
};
const rolesRes = await serverApi.get('users/roles', { headers });
const rolesData: RoleResponse = await rolesRes.json();
console.log('token', token);
return {
...token,
accessToken: user.accessToken,
refreshToken: user.refreshToken,
expiresAt: expiredTime,
user: user.user,
role: rolesData.data.role,
};
} catch (error) {
console.error('[role fetch error]', error);
return {
...token,
accessToken: user.accessToken,
refreshToken: user.refreshToken,
expiresAt: expiredTime,
user: user.user,
};
}
}
// 여기서 만료 여부를 확인하고 만료가 되었다면 다시 엑세스 토큰을 재발급한다.
if (Date.now() > (token.expiresAt as number)) {
try {
const response = await serverApi.post('users/auth/refresh', {
headers: {
Authorization: `Bearer ${token.refreshToken}`,
},
});
const data: LoginSuccessResponse = await response.json();
return {
...token,
accessToken: data.data.accessToken,
expiresAt: getTokenExpiration(data.data.accessToken),
};
} catch (error) {
console.error('[refresh error]', error);
signOut();
return null;
}
}
return token;
},
아래는 세션에 들어갈 정보이다.
이때 주의할 점은 세션은 네트워크 탭에 보이기 때문에 토큰 같은 정보를 저장해서는 안된다.
session: async ({ session, token }) => {
return {
...session,
expiresAt: token.expiresAt,
user: token.user,
role: token.role,
} as Session;
},
이는 따로 쿠키에 정보를 저장하고 있어 js로 접근할 수 없기 때문에 요청을 보내 세션 미들웨어에서 가공 처리를 하여 세션 정보를 가져오는 것이다.

설정을 했으니 사용 방법에 대해 알아보자.
클라이언트 컴포넌트에서 세션 정보 불러오기
아래와 같이 간단하게 사용할 수 있다.
'use client';
import { useSession } from 'next-auth/react';
const { data: session, status } = useSession();
서버 컴포넌트에서 토큰 넣어주기
아래와 같이 공통 api 로직을 통해 작성할 수 있다.
나는 ky를 써서 다음과 같이 작성했다.
'use server';
import 'server-only';
import ky from 'ky';
import { headers } from 'next/headers';
import { getToken } from 'next-auth/jwt';
async function authAPi() {
const reqLike = {
headers: { cookie: (await headers()).get('cookie') ?? '' },
};
const jwt = await getToken({
req: reqLike,
secret: process.env.NEXT_AUTH_SECRET,
});
if (!jwt) {
return ky.create({
prefixUrl: process.env.NEXT_PUBLIC_API_URL,
});
}
const access = jwt.accessToken;
console.log('access', access);
return ky.create({
prefixUrl: process.env.NEXT_PUBLIC_API_URL,
hooks: {
beforeRequest: [
async (req) => {
if (access && !req.headers.get('Authorization')) {
req.headers.set('Authorization', `Bearer ${access}`);
}
},
],
},
});
}
export default authAPi;
더 자세한 사용법 알아보기
https://www.heropy.dev/p/MI1Khc
Auth.js(NextAuth.js) 핵심 정리
Auth.js(NextAuth.js)는 Next.js 프로젝트의 사용자 인증 및 세션 관리를 위한 라이브러리로 Google, GitHub 등의 다양한 인증 공급자를 지원하며, Next.js의 서버와 클라이언트 측 모두에서 인증 및 세션 관리
www.heropy.dev
'코딩 정보 > NextJs' 카테고리의 다른 글
| [개념부터 nextjs] 페이지 라우터의 한계, 서버 컴포넌트 도입 (1) | 2025.08.19 |
|---|---|
| [개념부터 Nextjs] 서버 액션에 대해 자세히 알아보자 (4) | 2025.08.15 |
| [개념부터 Nextjs] Redirecting (6) | 2025.08.09 |
| [개념부터 nextjs] 서버 컴포넌트 (8) | 2025.08.01 |
| [개념부터 Nextjs] Linking and Navigating (3) | 2025.07.29 |