-
인가 서버에서 token 받아오기 (OAuth) feat. NextJsFrontEnd/Next.js 2024. 6. 10. 20:18
- 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