ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 인가 서버에서 token 받아오기 (OAuth) feat. NextJs
    FrontEnd/Next.js 2024. 6. 10. 20:18
     

    Kakao Developers

    카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

    developers.kakao.com

    • 카카오 로그인 문서에 따르면 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 });
        }
    }

     

Designed by Tistory.