Auth.js
Auth.js ? Next Auth?
Auth.js, Next Auth는 같은 라이브러리 입니다. 현재 베타 버전으로 v5.0.0을 제공하고 있고 해당 버전에서 Next Auth를 Auth.js로 라이브러리 이름을 변경했습니다. Auth.js로의 변경 이유는 기존의 NextJs에서만 지원하던 기능을 다른 프레임워크에도 확장해서 지원하기 위함입니다. (참고)
하지만 아직 까지는 Beta 버전이기 때문에 Next.js 프레임워크를 사용한다면 Next Auth v4를 사용하는 것이 좋을 것 같습니다. 기본적으로 인증 솔루션을 제공하는 라이브러리이기 때문에 Email 인증, Oaut 인증, Credentials 인증을 대부분 지원하고 있습니다. 한국에서 가장 많이 쓰이는 Oauth naver, kakao 또한 지원하고 있습니다. (현재 네이버 Oauth를 v5로 구현하는 경우 다음과 같은 문제에 직면하게 됩니다. 링크) 앞으로는 Next Auth로 통일하도록 하겠습니다.
Next Auth
Next Auth는 Next.js 프레임워크에서 보안 인증 솔루션을 쉽게 처리할 수 있도록 제공된 오픈 소스 프로젝트입니다. 따라서 Next Auth는 처음부터 Next.js와 서버리스를 지원하도록 설계되었습니다.
따라서 기존의 서버와 연결해서 사용할 수도 있으며, 서버 없이 보안 인증 솔루션을 만들 수도 있습니다. DB가 없기 때문에 세션 정보를 영구적으로 저장하는 것은 불가능하지만, 애플리케이션에서 세션 관리에는 영향을 주지 않습니다.
만약 DB와 연결을 하고 싶다면, 혹은 이미 사용하고 있는 유저 DB가 있다면, Next Auth에서 제공하는 기능을 커스터마이징해서 사용해야 합니다.
JWT
Next Auth 라이브러리는 JSON 웹 토큰(JWT)을 사용하여 세션을 만들 수 있습니다. 이는 Next Auth 라이브러리의 기본 세션 전략입니다. 사용자가 로그인(signIn)하면 HttpOnly 쿠키에 JWT가 생성됩니다. 쿠키를 HttpOnly로 만들면 자바스크립트가 클라이언트 측(document.cookie)에서 액세스하지 못하므로 공격자가 값을 훔치기가 더 어려워집니다. 또한 JWT는 서버만 알고 있는 비밀 키로 암호화됩니다.(AUTH_SECRET || NEXTAUTH_SECRET) 따라서 공격자가 쿠키에서 JWT를 훔치더라도 해독할 수 없습니다. 짧은 만료 시간과 결합하여 JWT는 세션을 생성하는 안전한 방법입니다. 사용자가 로그아웃(signOut)하면 쿠키에서 JWT가 삭제되고 세션이 파괴됩니다.
DataBase
JWT 세션 전략으로 Auth.js 라이브러리는 데이터베이스 세션도 지원합니다. 이 경우 로그인 후 사용자 데이터와 함께 JWT를 저장하는 대신 Auth.js 라이브러리가 데이터베이스에 세션을 생성합니다. 그런 다음 세션 ID가 HttpOnly 쿠키에 저장됩니다. 이는 JWT 세션 전략과 유사하지만 사용자 데이터를 쿠키에 저장하는 대신 데이터베이스에 세션을 가리키는 모호한 값만 저장합니다.
OAuth 적용 방법.
Next Auth에서 OAuth 적용 방법에 대해서 정리합니다. Next Auth에서 내부적으로 serverless OAuth를 구현하면 아래의 그림과 같은 과정을 거치게 됩니다.

- App Server는 Next.js App Router를 의미합니다.
- 브라우저에서 사용자가 로그인 버튼을 클릭했을때 NextAuth Providers 설정을 보고 구글, 트위터 등 로그인 가능한 Providers를 보여줍니다.
- 여기서는 사용자가 깃허브를 골랐고, NextAuth에서 내부적으로 깃허브 Auth Server에게 인증 요청을 합니다.
- 그러면 깃허브 Auth Server는 깃허브 로그인 URL을 넘겨주고, 이를 브라우저에 보여줍니다.

- 이후 사용자가 로그인 승인을 하면 깃허브 인증 서버는 code라는 authorization grant 를 보내줍니다.
- 그러면 NextAuth에서 이를 받아서 다시 액세스 토큰을 요청할 수 있습니다.
- 액세스 토큰을 발급 받으면 이를 통해 사용자 프로필과 같은 리소스 요청을 할 수 있습니다. 그러면 사용자 정보(id, username, email, image)를 받아와서 session db에 저장합니다.
- 해당 세션id를 토큰화해서 브라우저에 넘겨주면 로그인 처리가 완료됩니다.
간단한 실습
라이브러리 버전
{
"name": "next-auth",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "14.1.0",
"next-auth": "^4.24.5",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"eslint": "^8",
"eslint-config-next": "14.1.0",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}
핵심 auth.ts 작성
해당 파일에서 Next Auth가 OAuth를 진행 할 때 사용하는 옵션과 설정을 작성합니다.
Next Auth에서 naver, kakao provider를 제공하기 때문에, 각각의 provider를 등록하고 각각 API 문서에 등록된 CLIENT_ID, SECRET_ID를 등록합니다.
- 네이버
- 카카오
- NEXTAUTH_URL : 프로덕션 환경에 배포할 때는 NEXTAUTH_URL 환경 변수를 사이트의 정식 URL로 설정하세요.
- NEXTAUTH_SECRET : NextAuth.js JWT를 암호화하고 이메일 인증 토큰을 해시하는 데 사용됩니다. 이 값은 NextAuth 및 미들웨어의 비밀 옵션의 기본값입니다.
// src/auth.ts
import NextAuth from 'next-auth';
import NaverProvider from 'next-auth/providers/naver';
import KakaoProvider from 'next-auth/providers/kakao';
import type { AuthOptions, DefaultSession } from 'next-auth';
const naverCustomProvider = NaverProvider({
clientId: process.env.NAVER_CLIENT_ID!,
clientSecret: process.env.NAVER_SECRET!,
});
const kakaoCustomProvider = KakaoProvider({
clientId: process.env.KAKAO_CLIENT_ID!,
clientSecret: process.env.KAKAO_SECRET!,
});
export const config: AuthOptions = {
providers: [naverCustomProvider, kakaoCustomProvider],
};
// singIn, singOut, updateUser 등의 메서드를 반환합니다.
// 이는 server side action에서 활용할 수 있습니다.
// client에서는 next-auth/react에서 제공하는 singIn, singOut 메서드를 활용합니다.
export default NextAuth(config);
// .env
NAVER_CLIENT_ID=asd
NAVER_SECRET=asd
KAKAO_CLIENT_ID=asd
KAKAO_SECRET=asd
NEXTAUTH_SECRET=asd
NEXTAUTH_URL=asd
이후 내부적으로 Next.js RouteHandler를 활용해서 Next Auth는 로그인과 회원가입 과정에서 GET, POST를 실행합니다. 따라서 저희가 해당 API를 직접 구현할 필요 없이 auth.ts
에서 설정한 값과 변수들로 대체 할 수 있습니다. Next Auth는 기본적으로 /api/auth/[…nextauth] 라우트에서 모든 처리를 수행합니다. ([…nextauth]
catchall-segment)
// src/app/api/auth/[…nextauth]/route.ts
import NextAuth from '@/auth';
export { NextAuth as GET, NextAuth as POST };
이후 client측에서 Next-Auth에서 제공하는 session data를 활용하기 위해 session provider context component를 구현한 후, layout에서 감싸줍니다.
'use client';
import { SessionProvider } from 'next-auth/react';
export default function AuthProvider({
children,
}: {
children: React.ReactNode;
}) {
return <SessionProvider>{children}</SessionProvider>;
}
실제 로그인 기능을 RCS로 구현합니다.
'use client';
import { useSession, signOut, signIn } from 'next-auth/react';
import { useRouter } from 'next/navigation';
export default function LoginPage() {
const { data: session } = useSession();
const router = useRouter();
console.log('useSession : ', session);
const handleLogout = () => {
signOut({ redirect: false }).then(() => {
router.replace('/');
});
};
const handleNaver = async () => {
await signIn('naver', {
redirect: false,
callbackUrl: 'http://localhost:3000',
});
};
const handleKakao = async () => {
await signIn('kakao', {
redirect: false,
callbackUrl: 'http://localhost:3000',
});
};
if (session) {
return (
<>
<div>
{session.user!.name}님 반갑습니다.
<img
src={session.user!.image!}
alt="profile"
width={40}
height={40}
/>
<button onClick={handleLogout}>로그아웃</button>
</div>
</>
);
}
return (
<>
<div className="w-[200px] h-[200px] flex flex-col border-2 border-solid m-auto mt-4 p-4 gap-2">
<button
onClick={handleNaver}
className="border-solid border-2 cursor-pointer py-2 font-bold text-2xl rounded-lg">
네이버
</button>
<button
onClick={handleKakao}
className="border-solid border-2 cursor-pointer py-2 font-bold text-2xl rounded-lg">
카카오
</button>
</div>
</>
);
}
만약 서버측에서 session 정보에 접근하고자 한다면 다음과 같이 session 정보를 가지고 올 수 있습니다.
// src/app/page.tsx
import Link from 'next/link';
import { config } from '@/auth';
import { getServerSession } from 'next-auth';
export default async function Home() {
const session = await getServerSession(config);
if (session?.user) {
return (
<div>
<img src={session.user.image!} alt={'profile'} />
<div>{session.user.name!}</div>
<div>{session.user.email!}</div>
</div>
);
}
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<Link href={'/login'}>Login</Link>
</div>
</main>
);
}
실제 로그인에 성공하면, 다음과 같이 next-auth.session-token 쿠키가 생성됩니다. authjs로 시작하는 쿠키는 v5에서 제공되는 쿠키 이름입니다.

callbacks
콜백은 동작이 수행될 때 발생하는 상황을 제어하는 데 사용할 수 있는 비동기 함수입니다.
콜백은 데이터베이스 없이도 액세스 제어를 구현하고 외부 데이터베이스 또는 API와 통합할 수 있습니다. Sign in callback, redirect callback, JWT callback, Session callback을 사용할 수 있습니다. (링크)
export const config: AuthOptions = {
providers: [naverCustomProvider, kakaoCustomProvider],
callbacks: {
async signIn(data) {
console.log('SignIN DATA : ', data);
return true;
},
async jwt(data) {
console.log('JWT Data : ', data);
return data.token;
},
async session(data) {
console.log('Session Data : ', data);
data.session.user.address = 'address';
// useSession, getServerSession의 user property에 전달되는 데이터 형식을 결정
return data.session;
},
},
};
각각 네이버 OAut를 활용했을 때, 각각 인자로 전달되는 데이터 형태를 콘솔로 찍어보면 다음과 같습니다.
// signIn callback
SignIN DATA : {
user: {
id: 'qwe',
name: '준팔이',
email: 'qwe',
image: 'qwe'
},
account: {
provider: 'naver',
type: 'oauth',
providerAccountId: 'qwe',
access_token: 'qwe',
refresh_token: 'qwe',
token_type: 'bearer',
expires_at: 1706699921
},
profile: {
resultcode: '00',
message: 'success',
response: {
id: 'qwe',
nickname: '준팔이',
profile_image: 'qwe',
email: 'qwe',
name: 'qwe'
}
}
}
// jwt callback
JWT Data : {
token: {
name: 'qwe',
email: 'qwe',
picture: 'qwe',
sub: 'qwe'
},
user: {
id: 'qwe',
name: 'qwe',
email: 'qwe',
image: 'qwe'
},
account: {
provider: 'naver',
type: 'oauth',
providerAccountId: 'qwe',
access_token: 'qwe',
refresh_token: 'qwe',
token_type: 'bearer',
expires_at: 1706699921
},
profile: {
resultcode: '00',
message: 'success',
response: {
id: 'qwe',
nickname: 'qwe',
profile_image: 'qwe',
email: 'qwe',
name: 'qwe'
}
},
isNewUser: undefined,
trigger: 'signIn'
}
// session callback
Session Data : {
session: {
user: {
name: 'asd',
email: 'asd',
image: 'asd'
},
expires: '2024-03-01T10:18:41.792Z'
},
token: {
name: 'asd',
email: 'asd',
picture: 'asd',
sub: 'asd',
iat: 1706696321,
exp: 1709288321,
jti: '5cbd6438-a580-461c-b4d7-fc2c67d464d4'
}
}
{
user: {
name: 'asd',
email: 'asd',
image: 'asd',
address: 'asd'
}
}
Typescript
Next Auth는 다른 라이브러리와 달리 제네릭을 권장하지 않습니다. 실제 내부 타입 또한 제네릭 없이 구현되어 있습니다. 이는 제네릭을 사용하면 한 곳에서는 유형을 재정의할 수 있지만 다른 곳에서는 재정의할 수 없으므로 유형이 구현과 동기화되지 않을 수 있기 때문입니다. 따라서 typescript의 declare를 활용해서 새롭게 type을 정의해야 합니다.
만약 session에서 접근하는 user 데이터를 확장하고 싶다면 다음과 같이 처리할 수 있습니다.
// user module 새롭게 정의하기
declare module 'next-auth' {
interface Session {
user: {
token?: string;
address?: string;
accessToken?: string;
refreshToken?: string;
} & DefaultSession['user'];
}
}
정리
직접 Oauth 처리를 위한 API 생성 없이 쉽게 로그인 구현을 할 수 있다는 점이 인상깊었습니다. 또한 CSRF 토큰을 통해 사용자 요청을 검증하기 때문에, 보안에도 유리하다는 점을 알 수 있었습니다.
더불어 callbacks 기능을 통해 추가적인 api 요청, session update 등을 쉽게 진행할 수 있다는 점이 매력적이었습니다.
하지만 로그인 로직과 인증 로직이 복잡해질수록 (동시 로그인 제한, 공용 pc 브라우저 종료시 자동 로그아웃 등) 해당 라이브러리에 의존하는 것이 옳지 않다고 느껴졌습니다. 해당 구조에 맞추어서 대부분의 문제를 해결할 수는 있지만, 꽤나 제한적이라는 생각이 듭니다. 간단한 사이드 프로젝트 혹은 작은 단위의 프로덕션에서 빠르게 구현을 할 때, 사용을 하면 좋을 것 같습니다.
아직까지 복잡한 로그인, 인증 체계를 다룬적이 없기에, 이 또한 해당 라이브러리, 혹은 순수 구현을 경험해 보아야 더 깊게 알 수 있을 것 같습니다. ㅎ
참조
- Auth.js ? Next Auth?
- Next Auth
- - JWT
- - DataBase
- OAuth 적용 방법.
- 간단한 실습
- - 라이브러리 버전
- - 핵심 auth.ts 작성
- callbacks
- - Typescript
- 정리
- 참조