Joonas' Note

Joonas' Note

[FastAPI + React] 소셜 로그인 구현하기 - 커스텀 로그인 (feat. 카카오) 본문

개발

[FastAPI + React] 소셜 로그인 구현하기 - 커스텀 로그인 (feat. 카카오)

2022. 9. 20. 23:57 joonas

    아래의 글을 먼저 읽고 진행하시는 것을 추천한다.

     

    [FastAPI + React] 소셜 로그인 구현하기 - 구글 로그인

    아래 글에서 이어지는 내용입니다. [FastAPI + React] 소셜 로그인 구현하기 - 이메일 로그인 아래 글에서 이어지는 내용이다. [FastAPI + React] 소셜 로그인 구현하기 - 기본 환경 구축 들어가기 전에 Reac

    blog.joonas.io


    들어가기 전에

    Google, GitHub, Microsoft 등은 FastAPI Users에서 친절하게 구현을 미리 해주었습니다. 하지만 카카오와 같은 국내 기업들에 대한 로그인은 구현되어 있지 않다.

    그래서 이 글에서는 클래스를 오버라이딩해서, 기존에 작성한 FastAPI Users와 완전히 호환되는 로그인을 구현한다.


    FastAPI

    커스텀 클라이언트 클래스

    구글 로그인을 연동할 때에는, 아래와 같이 GoogleOAuth2 라는 클래스를 사용했었다.

    from httpx_oauth.clients.google import GoogleOAuth2
    
    google_oauth_client = GoogleOAuth2(
        client_id=Configs.GOOGLE_CLIENT_ID,
        client_secret=Configs.GOOGLE_CLIENT_SECRET,
        scope=[
            "https://www.googleapis.com/auth/userinfo.profile",
            "https://www.googleapis.com/auth/userinfo.email",
            "openid"
        ],
    )

    이것과 동일하게 KakaoOAuth2 라는 클래스를 만들어서, Client ID와 Client Secret, 그리고 scope를 넘기면 유저 생성부터 기존 아이디 연동까지, FastAPI Users 에 그대로 호환되도록 한다.

    아래와 같이 클래스를 만든다.

    from typing import Any, Dict, List, Optional, Tuple, cast
    
    import json
    from httpx_oauth.errors import GetIdEmailError
    from httpx_oauth.oauth2 import BaseOAuth2
    from httpx_oauth.typing import TypedDict
    
    AUTHORIZE_ENDPOINT = "https://kauth.kakao.com/oauth/authorize"
    ACCESS_TOKEN_ENDPOINT = "https://kauth.kakao.com/oauth/token"
    PROFILE_ENDPOINT = "https://kapi.kakao.com/v2/user/me"
    BASE_SCOPES = ["account_email"]
    BASE_PROFILE_SCOPES = ["kakao_account.email"]
    
    
    class KakaoOAuth2(BaseOAuth2[Dict[str, Any]]):
        display_name = "Kakao"
        logo_svg = LOGO_SVG
    
        def __init__(
            self,
            client_id: str,
            client_secret: str,
            scopes: Optional[List[str]] = BASE_SCOPES,
            name: str = "kakao",
        ):
            super().__init__(
                client_id,
                client_secret,
                AUTHORIZE_ENDPOINT,
                ACCESS_TOKEN_ENDPOINT,
                name=name,
                base_scopes=scopes,
            )
    
        async def get_id_email(self, token: str) -> Tuple[str, Optional[str]]:
            async with self.get_httpx_client() as client:
                response = await client.post(
                    PROFILE_ENDPOINT,
                    params={"property_keys": json.dumps(BASE_PROFILE_SCOPES)},
                    headers={**self.request_headers,
                             "Authorization": f"Bearer {token}"},
                )
    
                if response.status_code >= 400:
                    raise GetIdEmailError(response.json())
    
                account_info = cast(Dict[str, Any], response.json())
                kakao_account = account_info.get('kakao_account')
    
                return str(account_info.get('id')), kakao_account.get('email')

    주의할 점은, 카카오 API에서는 계정 id를 정수형으로 반환하는데, get_id_email 함수에서는 DB에서 조회할 때 이 타입이 달라서 검색을 하지 못하고 계속 새로운 계정을 생성한다. 그렇기 때문에 문자열로 변환해서 리턴해야 한다.

    라우터 추가

    이렇게 만든 클래스를 사용해서 구글 로그인과 동일하게 아래와 같이 라우터를 만들어서 추가한다.

    auth_backend_kakao = AuthenticationBackend(
        name="jwt-kakao",
        transport=bearer_transport,
        get_strategy=get_jwt_strategy
    )
    
    # 인증 클라이언트
    kakao_oauth_client = KakaoOAuth2(
        client_id=Configs.KAKAO_CLIENT_ID,
        client_secret=Configs.KAKAO_CLIENT_SECRET,
        scopes=[
        	# https://developers.kakao.com/docs/latest/ko/kakaologin/common#user-info-kakao-account
            "profile_nickname", "profile_image", "account_email",
        ]
    )
    
    # 카카오 로그인 JWT 라우터
    kakao_oauth_router = fastapi_users.get_oauth_router(
        oauth_client=kakao_oauth_client,
        backend=auth_backend_kakao,
        state_secret=Configs.SECRET_KEY,
        redirect_url="http://localhost:3000/login/kakao",
        associate_by_email=True,
    )
    
    # 라우터 추가
    app.include_router(kakao_oauth_router, prefix="/auth/kakao", tags=["auth"])

    이제 Interactive docs에서 엔드포인트를 확인해보면 잘 추가된 것을 볼 수 있다.


    React

    리액트쪽에서 해줄 것은 구글 로그인과 동일하게 콜백에 대한 처리를 하는 부분밖에 없다.

    <Route path="/login">
      <Route index element={<Auth.Login />} />
      <Route path="google" element={<Auth.Redirects.Google />} />
      <Route path="github" element={<Auth.Redirects.Github />} />
      <Route path="kakao" element={<Auth.Redirects.Kakao />} />
    </Route>

    엔드 포인트의 형식이 다른 로그인과 동일하기 때문에, /auth/kakao/callback 로 데이터를 그대로 전달해서 토큰을 발급받으면 된다.

    customAxios()
      .get("/auth/kakao/callback" + location.search)
      .then(({ data }) => {
        // 토큰 받아서 로그인 처리
        login({
          token: data.access_token,
        });
      })
      .catch(({ response }) => {
        console.error(response);
        // 에러 처리
      });

    결과

    카카오 로그인

    코드

    https://github.com/joonas-yoon/fastapi-react-oauth2/tree/signin-with-kakao

     

    GitHub - joonas-yoon/fastapi-react-oauth2: FastAPI + MongoDB with Beanie + React Login Example

    FastAPI + MongoDB with Beanie + React Login Example - GitHub - joonas-yoon/fastapi-react-oauth2: FastAPI + MongoDB with Beanie + React Login Example

    github.com

     

    Comments