- next-auth를 사용하면 provider를 통해 oauth 구현이 쉽겠지만 oauth에 대해 알고 있는 로직을 직접 구현해보고 싶어졌음
- https://developers.kakao.com/docs/latest/ko/kakaologin/common
- 카카오 로그인 문서에 따르면 oauth의 로직은 아래 이미지와 같다
- 메인 페이지(Home)에 있는 카카오 로그인 버튼을 누르면 아래의 함수가 실행된다.
const HomePage = () => {
const handleKakaoLogin = () => {
//리다이렉트 uri는 https://developers.kakao.com/console/app 에서 설정하면 됨. 이때 client Id도 발급 됨
const kakaoAuthUrl = `https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID}&redirect_uri=${process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI}&prompt=login`;
window.location.href = kakaoAuthUrl;
};
return (
<div className="full center flex-col mx-auto space-y-4">
<Credential />
<div className="w-full h-[0.5px] bg-slate-400" />
<button
onClick={handleKakaoLogin}
className="w-full px-10 py-3 bg-yellow-400 rounded-xl text-white"
>
Kakao
</button>
</div>
);
};
- 본인은 redirect uri를 http://localhost:3000/auth/callback/kakao 로 설정해둠.
- 이렇게 코드를 짜면 로그인 버튼을 클릭시 카카오 로그인 페이지가 뜰것이고 로그인이 성공한다면 인가 서버에서 code(인가코드)를 넘겨줄 것이고 리다이렉트 uri로 페이지는 이동된다.
- app 폴더에 /auth/callback/kakao/page.tsx를 만들어줌.
- 해당 페이지에서는 카카오에서 받아온 code(인가 코드)를 api 서버에 넘겨준 뒤 해당 서버에서는 kakao 인가 서버에 코드를 넘겨줌으로써 accessToken과 refreshToken을 받아올것이다.(이 때 유효 기간도 함께 받아옴)
// app/auth/callback/kakao/page.tsx
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import React, { useEffect } from 'react';
const OAuthPage = () => {
const searchParams = useSearchParams();
const router = useRouter();
useEffect(() => {
(async () => {
const code = searchParams.get('code');
if (code) {
try {
const res = await fetch('/api/auth/callback/kakao', {
method: 'POST',
body: JSON.stringify({ code }),
});
if (res.ok) {
// 본인은 accessToken을 서버단에서 jwt화 시킴
const { jwt, refesh_token } = await res.json();
console.log(jwt, refesh_token);
router.push('/');
}
} catch (error) {
console.log(error);
}
}
})();
}, [searchParams, router]);
return (
<div className="h-full w-full flex justify-center items-center ">
Loading
</div>
);
};
export default OAuthPage;
- 인가 코드를 통해 카카오 서버에서는 액세스 토큰과 리프레쉬 토큰을 발급해 줄 것인데 이제 이 토큰을 통해서 유저의 정보들을 받아올 수 있다.
- 참고로 'Content-Type'이 'application/x-www-form-urlencoded', 일 경우에는 바디에 JSON 형태가 아닌 URLSearchParams 형태로 담아서 보내줘야함
- accessToken은 JWT 페이로드에 담아서 클라이언트 쪽으로 보내고 , refreshToken은 HttpOnly 쿠키에 담아서 응답해준다. (ContextAPI를 이용해서 jwt를 메모리에 담고, 리프레쉬 토큰은 쿠키에 담아서 안전하게 보내주기 위함)
- ---> 정정사항: 카카오에서 Bearer Token 형식으로 액세스를 보내주기 때문에 굳이 또 jwt화 시킬 필요 없음(2024.08.08)
// app/api/auth/callback/kakao/route.ts
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import { calExpireDate } from '@/utils/cal-date';
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { code } = body;
if (!code) {
return new NextResponse('Code is missing', { status: 402 });
}
const tokenResponse = await fetch(
'https://kauth.kakao.com/oauth/token',
{
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID!,
redirect_uri: process.env.NEXT_PUBLIC_KAKAO_REDIRECT_URI!,
client_secret: process.env.NEXT_PUBLIC_KAKAO_CLIENT_SECRET!,
code,
}),
}
);
const tokenData = await tokenResponse.json();
const payload = {
accessToken: tokenData.access_token,
};
// jwt의 유효기간을 받아온 액세스 토큰의 유효기간과 동일시 해준다. 초 단위 값을 넣으면 자동으로 현재 시간으로부터 x초가 지난 시간이 exp에 담겨짐
const jwtToken = jwt.sign(payload, process.env.SECRET_KEY!, {
algorithm: 'HS256',
expiresIn: tokenData.expires_in,
});
const response = NextResponse.json({
jwt: jwtToken,
});
response.cookies.set('refresh_token', tokenData.refresh_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
expires: calExpireDate(tokenData.refresh_token_expires_in),
path: '/',
});
return response;
} catch (error) {
console.log(error);
return new NextResponse('Internal Server Error', {
status: 500,
statusText: 'Internal Server Error',
});
}
}
- 클라이언트 쪽에서 액세스 토큰을 활용하여 유저에 대한 정보를 요청하기 위해서는 jwt 에 대한 검증이 필요
'use server';
import jwt, { JwtPayload } from 'jsonwebtoken';
import getUserInfo from './get-userInfo';
export const verifyJwt = (jwtToken: string) => {
try {
jwt.verify(jwtToken, process.env.SECRET_KEY!, async (err, decoded) => {
if (err) throw new Error('Failed to verify JWT');
const curDate = new Date();
if (!decoded || typeof decoded === 'string')
throw new Error('Failed to decode JWT');
const decodedPayload = decoded as JwtPayload;
if (!decodedPayload.exp) {
throw new Error('JWT does not contain an exp claim');
}
// 현재 시간에 대해 만료되었다면 액세스 재발급
const expireDate = new Date(decodedPayload.exp * 1000);
if (curDate.getTime() > expireDate.getTime()) {
const renewToken = await refetchAccessToken()
}
const userData = await getUserInfo(decodedPayload.accessToken);
console.log(await userData);
});
} catch (error) {
console.log(error);
}
};
//재발급 요청
const refetchAccessToken = async () => {
try {
const res = await fetch('/api/auth/callback/kakao/refetch', {
method: 'POST',
});
if (res.ok) console.log(await res.json());
} catch (error) {
console.log(error);
}
};
아래는 리프레쉬 토큰을 통해 액세스 토큰을 재발급하는 과정
https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#refresh-token
// /api/auth/callback/kakao/refetch
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
try {
const refreshToken = req.cookies.get('refresh_token');
if (!refreshToken) {
return new NextResponse('No refresh token', { status: 401 });
}
const { value: refresh_token } = refreshToken;
const body = new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.NEXT_PUBLIC_KAKAO_CLIENT_ID!,
client_secret: process.env.NEXT_PUBLIC_KAKAO_CLIENT_SECRET!,
refresh_token,
}).toString();
const res = await fetch('https://kauth.kakao.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type':
'application/x-www-form-urlencoded;charset=utf-8',
},
body,
});
if (res.ok) {
const data = await res.json();
return NextResponse.json({ accessToken: data.access_token });
}
const errorData = await res.json();
console.error('Failed to refresh token:', errorData);
return new NextResponse('Failed to refresh token', { status: 401 });
} catch (error) {
console.error('Internal Server Error:', error);
return new NextResponse('Internal Server Error', { status: 500 });
}
}
'FrontEnd > Next.js' 카테고리의 다른 글
form에서 server action 다루기 (0) | 2024.06.15 |
---|---|
Form에서 두개 이상의 버튼을 다루기 (feat. server action ) (1) | 2024.06.14 |
next.js 에 kakao map 띄우기 (feat. react-kakao-maps-sdk) (1) | 2024.02.06 |
[next.js] react-quill 에디터 값을 출력하기 (feat. dangerouslySetInnerHTML ) (1) | 2024.01.31 |
Link , Router 비교 (0) | 2023.11.16 |