[설정해야 하는 부분]
0. next-auth session 타입 재정의
1. signIn 함수 호출
2. auth.ts provider, callback 설정
3. session에 저장된 정보 사용하기 (client side, server side)
이번에 authjs를 사용해보면서 자체로그인 시에 Credentials라는 인증 옵션을 사용하면서, 서버로 부터 발급받은 accessToken과 refreshToken을 auth가 관리하는 session에 저장해서 활용하는 방법을 알아보았습니다.
해당 기능을 구현하면서 auth.ts 파일 내부에 jwt callback 과 session callback에 어떤 인자가 전달되고 어떤 상황에 호출이 되는지 흐름을 살펴보겠습니다.
0️⃣ nextauth session 타입 재정의
// session 속성 재정의
// src/types/next-auth.d.ts
import NextAuth from "next-auth";
declare module "next-auth" {
interface Session {
user: {
name: number;
sub: string;
type: string;
token: string;
id: string;
iat: number;
exp: number;
jti: string;
};
accessToken: string;
refreshToken: string;
}
}
1️⃣ signIn 함수 호출
// 로그인 함수를 호출하는 컴포넌트
import { signIn } from "next-auth/react";
const onSubmit = async (data: FormData) => {
const response = await signIn("credentials", {
account: data.account,
password: data.password,
redirect: false,
});
};
2️⃣ auth.ts파일 설정
// auth.ts 파일
import axios from "axios";
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { User } from "next-auth";
type ExtendedUser = User & {
accessToken: string;
refreshToken: string;
};
export const { handlers, auth, signIn } = NextAuth({
trustHost: true,
providers: [
CredentialsProvider({
async authorize(credentials) {
console.log("credentials");
const authResponse = await axios.post(
`http://localhost:8080/api/v1/auth/sign-in`,
{
account: credentials.account,
password: credentials.password,
}
);
//토큰+기본 유저 정보가 담겨져 있는 user 객체를 반환
console.log(authResponse.data);
return authResponse.data;
},
}),
],
callbacks: {
async jwt({ token, user }) {
console.log("Jwt Callback()");
console.log(token);
console.log(user);
// authorize 함수의 반환값이 user에 담겨서 넘어온다.
// user 객체가 있다는 것은 signin이 성공한 직후의 요청
if (user) {
const extendedUser = user as ExtendedUser;
return {
...token,
accessToken: extendedUser.accessToken,
refreshToken: extendedUser.refreshToken,
};
}
// user 객체가 없다는 것은 단순 세션 조회를 위한 요청
// console.log(token);
return token;
},
async session({ session, token }) {
console.log("Session Callback()");
console.log(session);
console.log(token);
//4.Jwt Callback으로부터 반환받은 token값을 기존 세션에 추가한다
if (token) {
session.accessToken = token.accessToken as string;
session.refreshToken = token.refreshToken as string;
}
console.log(session);
return session;
},
},
secret: process.env.NEXTAUTH_SECRET,
});
🌊 signIn 실행되고 나서 흐름
//signIn 함수가 실행되고 나서 호출되는 함수, api
1.authorize()
2.Jwt Callback()
3.POST /api/auth/callback/credentials? 200
//session을 확인하면서 실행되는 콜백, api
4.Jwt Callback()
5.Session Callback()
6.GET /api/auth/session 200
[1] authorize()
//토큰+기본 유저 정보가 담겨져 있는 user 객체를 반환
//console.log(authResponse.data);
{
name: '슈퍼세트',
type: 'USER',
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuynhOq4iCIsImlhdCI6MTUxNjIzOTAyMn0.QaR7jD-M2rCHZLcUZ5q4Yb3D2p_8kP9HwZU4nR8mDYg',
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuynhOq4iCIsImlhdCI6MTUxNjIzOTAyMn0.QaR7jD-M2rCHZLcUZ5q4Yb3D2p_8kP9HwZU4nR8mDYg'
}
[2] Jwt Callback
이 콜백은 세션을 확인할 때 호출됩니다. (예: /api/session 엔드포인트를 호출하거나, useSession 또는 getSession을 사용할 때). 반환 값은 클라이언트에게 노출되므로 여기에 반환하는 것에 주의해야 합니다. JWT 콜백을 통해 토큰에 추가한 것을 클라이언트에게 사용할 수 있게 하려면 여기에서 명시적으로 반환해야 합니다.
async jwt({ token, user }) {
console.log("Jwt Callback()");
console.log(token);
console.log(user);
// authorize 함수의 반환값이 user에 담겨서 넘어온다.
// user 객체가 있다는 것은 signin이 성공한 직후의 요청
if (user) {
const extendedUser = user as ExtendedUser;
return {
...token,
accessToken: extendedUser.accessToken,
refreshToken: extendedUser.refreshToken,
};
}
// user 객체가 없다는 것은 단순 세션 조회를 위한 요청
// console.log(token);
return token;
},
//console.log(token);
{
name: '슈퍼세트',
email: undefined,
picture: undefined,
sub: '0ab0d0ed-45ec-4ea1-b4a9-f68b6437d3ca'
}
//console.log(user)
{
name: '슈퍼세트',
type: 'USER',
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuynhOq4iCIsImlhdCI6MTUxNjIzOTAyMn0.QaR7jD-M2rCHZLcUZ5q4Yb3D2p_8kP9HwZU4nR8mDYg',
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuynhOq4iCIsImlhdCI6MTUxNjIzOTAyMn0.QaR7jD-M2rCHZLcUZ5q4Yb3D2p_8kP9HwZU4nR8mDYg',
id: '0ab0d0ed-45ec-4ea1-b4a9-f68b6437d3ca'
}
[3] POST /api/auth/callback/credentials? 200
[4] Jwt Callback
//console.log(token);
{
name: '슈퍼세트',
sub: '0ab0d0ed-45ec-4ea1-b4a9-f68b6437d3ca',
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuynhOq4iCIsImlhdCI6MTUxNjIzOTAyMn0.QaR7jD-M2rCHZLcUZ5q4Yb3D2p_8kP9HwZU4nR8mDYg',
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuynhOq4iCIsImlhdCI6MTUxNjIzOTAyMn0.QaR7jD-M2rCHZLcUZ5q4Yb3D2p_8kP9HwZU4nR8mDYg',
iat: 1721351274,
exp: 1723943274,
jti: 'd95196b4-c45a-4cca-b3cb-5144926144b9'
}
//console.log(user)
undefined
[5] Session Callback
async session({ session, token }) {
console.log("Session Callback()");
console.log(session);
console.log(token);
// 4번 token callback으로부터 반환받은 token값을 기존 세션에 추가한다
if (token) {
session.accessToken = token.accessToken as string;
session.refreshToken = token.refreshToken as string;
}
console.log(session);
// 서버로부터 응답받은 accessToken과
return session;
},
// console.log(session)
{
user: { name: '슈퍼세트', email: undefined, image: undefined },
expires: '2024-08-18T01:18:26.668Z'
}
// console.log(token)
{
name: '슈퍼세트',
sub: 'c950a67f-07a7-465a-9d72-c58a580e7929',
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuynhOq4iCIsImlhdCI6MTUxNjIzOTAyMn0.QaR7jD-M2rCHZLcUZ5q4Yb3D2p_8kP9HwZU4nR8mDYg',
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuynhOq4iCIsImlhdCI6MTUxNjIzOTAyMn0.QaR7jD-M2rCHZLcUZ5q4Yb3D2p_8kP9HwZU4nR8mDYg',
iat: 1721351906,
exp: 1723943906,
jti: '07e50637-c7e3-4eb4-b831-a802d8a78c75'
}
우리가 활용할 최종 세션값 반환
// console.log(session)
{
user: { name: '슈퍼세트', email: undefined, image: undefined },
expires: '2024-08-18T01:18:26.668Z',
accessToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuynhOq4iCIsImlhdCI6MTUxNjIzOTAyMn0.QaR7jD-M2rCHZLcUZ5q4Yb3D2p_8kP9HwZU4nR8mDYg',
refreshToken: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IuynhOq4iCIsImlhdCI6MTUxNjIzOTAyMn0.QaR7jD-M2rCHZLcUZ5q4Yb3D2p_8kP9HwZU4nR8mDYg'
}
[6] GET /api/auth/session 200
3️⃣ Session에 저장된 정보 사용하기
[1] Page Server Side
import { auth } from "@/auth";
import LoginComponent from "@/components/LoginComponent";
export default async function MyForm(context: any) {
//auth 호출 후 세션에 저장된 user, accesstoken 값 사용하면 된다.
const session = await auth();
return (
<>
{session ? (
<>
<h1>로그인한유저임</h1>
<h1>{JSON.stringify(session.user)}</h1>
<h1>{JSON.stringify(session.accessToken)}</h1>
<LoginComponent />
</>
) : (
<LoginComponent />
)}
</>
);
}
[2] Page Client Side
클라이언트 사이드에서 useSession()을 사용하여 세션에 접근할 때, 페이지를 <SessionProvider />로 감싸고 있는지 확인해야합니다.
// src/lib/AuthSession.tsx
'use client';
import { SessionProvider } from 'next-auth/react';
type Props = {
children: React.ReactNode;
};
export default function AuthSession({ children }: Props) {
return <SessionProvider>{children}</SessionProvider>;
}
// src/app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import AuthSession from "@/lib/AuthSession";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${inter.className} flex justify-center items-center min-h-screen`}
>
<div className="w-[390px] h-[700px]">
<AuthSession>{children}</AuthSession>
</div>
</body>
</html>
);
}
import { signOut, useSession } from "next-auth/react";
export default function LoginComponent() {
const { data: session, status } = useSession();
return (
<>
{session && (
<>
<button onClick={() => signOut()}>Sign Out</button>
<div>{JSON.stringify(session.user)}</div>
<div>{JSON.stringify(session.accessToken)}</div>
<div>{JSON.stringify(session.refreshToken)}</div>
</>
)}
</>
);
}