Django - user customizing과 social login with DRF 구현하기, (allauth & dj-rest-auth & simpleJWT)

정현우·2023년 10월 26일
7

Django Basic to Advanced

목록 보기
38/40
post-thumbnail

[ 글의 목적: 산재되어 있는 django의 social login & jwt token & custom user 를 위한 라이브러리 활용 정리 & 핵심 정리 ]

DRF Social Login

django social login라기보단 drf의 social login에 대한 내용이다. drf는 django를 base로 하며 restfull 함을 지향하는 API를 만들기 위한 framework이다.그 drf에서 social login을 allauth 기반으로 RESTFul 하게 바꿔 제공해주는 것이 dj-rest-auth이며 해당 라이브러리 들을 활용해 social login을 jwt token base로 만들어보자!

🔥 Django - user customizing과 jwt token based auth with DRF 구현하기 글과 이어지는 글입니다.

1. OAuth

✅ 소셜 로그인은 OAuth 프로토콜을 기반으로 한다. 사용자는 소셜 네트워크에 직접 로그인하여 해당 네트워크로부터 액세스 토큰을 받고, 애플리케이션은 이 토큰을 사용하여 사용자의 기본 정보를 소셜 네트워크에서 가져올 수 있다.

1) 개념과 원리

  • 앞서 언급한대로 우리가 어플리케이션을 만들땐 사실 사용자의 모델이 아니라 "인증(Authentication)"과 "인가(Authorization)" 이다. 인증과 인가는 다르다. "인증"은 사용자가 자신이 주장하는 사람인지 확인하는 과정 이고, "인가"는 이미 인증된 사용자에게 특정 리소스나 기능에 접근하는 권한을 부여하거나 제한하는 과정 이다.

  • OAuth("Open Authorization") 는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 프로토콜(인증방식의 표준) 이다.

(1) 비밀번호 방식의 문제

  1. 인증방식에 대한 표준화가 되어 있지 않았다!
  2. 사용자가 thrid party application 에 비밀번호 제공하기를 꺼려한다!
  3. 만약 여러 서비스에 같은 비밀번호를 사용했다면, 하나의 서비스의 정보가 유출될시 연쇄적으로 피해를 받을 수 있다.
  • OAuth는 위의 기존의 비밀번호 방식의 문제를 해결하는 방향으로 발전 되었다. 하지만 그 인증방식이 태초에는 규모가 큰 기업마다 제각각이었다. 그래서 제기된 방법이 해당 인증방식을 표준화한 것이 OAuth이며 OAuth를 이용하면 이 "인증을 공유하는 애플리케이션끼리는 별도의 인증이 필요없다." 따라서 여러 애플리케이션을 통합하여 사용하는 것이 가능하게 된다.

  • 그리고 개인 정보가 점점 중요해지면서 규모가 작은 기업에서는 개인 정보를 저장하는 것 자체가 리스크를 가지게 되었다. 그러니 "이미 규모 큰 기업에서 회원가입에 성공한 사람은 이미 어느 정도 인가되었다고 가정하고, 필요한 정보만 위탁해서 가져오겠다" 라는 목적에서 "social login" 이 제대로 시작되었다.

(2) oauth http request flow

  1. Resource Owner (application, 우리의 app) 가 자원 (Auth) 을 요청한다.
  2. 그러면 우리의 client 에게 target auth service web의 인증을 사용자에게 요청한다 (보통 "로그인페이지"를 이용한다.)
  3. 사용자가 credentials 을 입력한다.
  4. clientoauth key 와 함께 유저의 credentials 를 Authorization server 에 전달한다.
  5. Authorization ServerAccess Tokenclient 에게 발급해준다.
  6. client 는 발급 받은 Access Token 을 가지고 Resource Server 에게 필요한 정보를 요청한다.
  7. Resource Server 는 client에게 필요한 자원을 건내주고 Resource Owner 는 이제 application을 사용할 수 있다.

2) 구현 방향과 사용할 라이브러리

최대한 라이브러리를 활용하는 방향, DRY 원칙을 지키면서 진행할 것이다.


1. (🥔 이전 게시글) simptJWT 로 JWT token based sign-up & sign-in 구현
2. allauth & dj-rest-auth 로 JWT token based RESTful 한 user 관련 API 구현 - 커스텀 유저 모델 사용하도록 세팅
3. allauth & dj-rest-auth social login - OAuth for google 구현
4. (🥔 다음 게시글) web token의 한계를 최대한 극복하기 위한 "Blacklist" 세팅
5. (🥔 다음 게시글) 모든 회원가입 과정에서 email 인증 받기

  • 원래 django에서는 allauth 로 social login (OAuth)를 간단하게 구현할 수 있다. 참고로 "소셜 로그인" 은 사용자가 다른 서비스에 로그인하는 방법을 의미 하고, "OAuth" 는 그러한 로그인을 안전하게 수행하기 위한 인증 프로토콜 이다.

  • allauth 공식홈페이지 를 보면 알 수 있듯이 해당 써드파티가 굉장히 잘 만들어져있어서, 특별한 커스텀을 하지 않을 것이고 django core user model을 그대로 사용한다면 템플릿 추가와 몇 가지 세팅하면 하면 바로 활용할 수 있다. 아래는 그 예시를 바로 보여주는 youtube 영상이다.

django allauth youtube

  • django core user model을 기반으로 하고, session login의 경우 social login은 가볍게 allauth 로 대부분의 경우를 핸들링할 수 있다.

  • 영상에서 보면 알 수 있듯이, social auth는 (당연하게) 해당 소셜 네트워크의 허락이 필요하다. 그렇기 때문에 원하는 social login platform 에서 API를 가져와서 녹여야 하며 각 각 넘겨주는 데이터나 형태가 다르다.

  • 이 부분을 allauth 로 핸들링하며, drf를 위해 dj-rest-auth 를 사용한다. 그리고 앞 선 글과 같이 jwt token 으로 auth 부분을 관리를 하는 방향으로 구현할 것이다. 이 과정에서 adapter pattern 과 같은 형식이 필요하다.

(1) 간략하게 살펴보는 django-allauth flow chart

  • OAuth2Adapterallauth 에서 제공하는 Google OAuth 처리하는 class이다. 실제 OAuth를 위한 API call을 라이브러리 레벨에서 처리한다. (allauth가 기본제공하는 template을 생각해보자!)

  • 해당 과정에서 "provider" 개념이 활용되며 official docs에서 자세한 내용을 확인할 수 있다. 구글에 관한 official docs는 여기

  • 아래에서 살펴보겠지만 path("", include("allauth.urls")) 로 추가되는 view & template 에서는 기본적으로 .../google/login/callback/... 과 같은 endpoint를 제공한다.

(2) dj-rest-auth

  • dj-rest-auth 는 DRF에서 소셜 로그인 및 유저 관리 패키지에 대해서 알아본 사람이면 한번쯤 들어봤을 django-rest-auth 에서 "따로 fork된 패키지" 이다. django-rest-auth가 업데이트를 더이상 하지 않자 dj-rest-auth 에서 개발을 진행하고 있다.

  • django-allauth & dj-rest-auth 를 모두 사용한다. 기본적으로 dj-rest-auth 는 django에서 지원하는 "Token based authentication" 을 이용한다. 만약 JWT를 이용한 인증을 구현하고 싶다면, dj_rest_auth.jwt_auth.JWTCookieAuthenticaton 를 이용해야 한다! -> official docs

  • dj-rest-auth 의 기능(로그인, 비밀번호 찾기...)를 이용한다면, 인증 클래스에 rest_framework_simplejwt.authentication.JWTAuthentication 가 아니라 dj_rest_auth.jwt_auth.JWTCookieAuthenticaton 를 이용해야 JWT 기반의 인증을 이용할 수 있다.

  • 이 부분에서 dj-rest-auth 를 이용하지 않는다면 인증 클래스에 rest_framework_simplejwt.authentication.JWTAuthentication 를 그대로 사용하면 된다.

(3) 라이브러리 정리

  1. allauth: OAuth based 의 social login을 포함한 다양한 인증 방식을 제공하는 패키지
  2. dj-rest-auth: REST API 환경에서 User 관리를 쉽게 해주는 라이브러리 (allauth를 RESTFul한 API로 casting)
  3. simpleJWT: JWT Token을 쉽게 활용하게 해주는 라이브러리
  • 각 라이브러리의 "핵심 목표" 가 다르다. 그리고 세 라이브러리 모두 서로의 통합을 지향하는 방향으로 개발 및 구현되어있다.

2. allauth social OAuth & dj-rest-auth & JWT Token & Custom User 구현하기

구현 자체의 난이도 보다 라이브러리의 각 설정값이 무엇을 의미하는지, 어디까지 관여하는지 docs 보고 익히는게 더 어려운 작업인...

1) 초기 세팅하기

(1) settings.py

(🔥 최종 설정 값이 아닙니다. 계속 부족한 부분을 고도화합니다! 🔥)


INSTALLED_APPS = [
	...
    # ============ #
    # auth & OAuth
    # ============ #
    "allauth",  # django-allauth
    "allauth.account",  # django-allauth
    "allauth.socialaccount",  # django-allauth
    "allauth.socialaccount.providers.google",  # django-allauth for google OAuth
    "dj_rest_auth",  # dj-rest-auth
    "dj_rest_auth.registration",  # dj-rest-auth
    "rest_framework_simplejwt",  # djangorestframework-simplejwt
]

MIDDLEWARE = [
	...
    "allauth.account.middleware.AccountMiddleware",  # django-allauth
]

EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

# allauth config, https://docs.allauth.org/en/latest/account/configuration.html
ACCOUNT_USER_MODEL_USERNAME_FIELD = None  # 커스텀한 user model엔 name field가 있다.
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_EMAIL_VERIFICATION = "none" # "mandatory"  # Setting this to "mandatory" requires ACCOUNT_EMAIL_REQUIRED to be True.

# dj-rest-auth config, https://dj-rest-auth.readthedocs.io/en/latest/configuration.html
REST_AUTH = {
    "TOKEN_MODEL": None,  # jwt token 쓸꺼임!, 이 경우 밑에 값이 무조건 True 거나 session 방식
    "USE_JWT": True,  # jwt token based auth를 위해 True
    "JWT_AUTH_HTTPONLY": False,  # refresh_token를 사용할 예정이라면, False로 설정을 바꿔야한다.
}

SIMPLE_JWT = {
	# ...해당 설정은 앞 글로 대체한다...
}
  • allauth.account 는 "로컬 계정과 관련된 기능" 이다. 대표적으로 전자 메일 기반의 로그인, 이메일 확인, 비밀번호 재설정 등의 기능을 제공한다.

  • allauth.socialaccount 는 OAuth based의 social login을 지원하며 위에 언급한 "providers" 들을 제공한다.

  • dj_rest_auth 자체는 RESTful API를 통해 로그인, 로그아웃, 비밀번호 변경/재설정 등의 기능을 제공한다. dj_rest_auth.registration 는 RESTful API를 통한 회원 가입 기능을 제공한다. allauth.account 와 연계되어 작동하므로, allauth 의 여러 설정과 통합되어 개발되었다.

  • allauth.account.middleware.AccountMiddleware 는 사용자가 로그인한 후 즉시 암호를 변경하도록 강제할 수 있는 기능을 제공한다. 자세한 사항은 official docs를 보는 것을 추천한다. (password_change_required attribute 사용)

  • ACCOUNT_ 로 시작하는 설정값은 official docs에 상세한 설명이 되어 있다. 해당 설정값은 "커스텀한 user이면서, email 로 로그인하며, 메일 인증이 필요하며, social login 까지 가능하게 세팅한 설정값이다.

  • EMAIL_BACKEND 는 email 전송 등을 handling 하기 위해 필요하다. 이메일 전송을 다루는 자세한 글은 다음 글에서 차근이 체크해보자!

  • REST_AUTHdj-rest-auth 의 설정값이며 기본적으로 저렇게 세팅하면 된다.

  • 설정을 마치고 python manage.py migrate 하면 위와 같은 마이그레이션이 쭉 딸려온다.

(2) urls.py

# accounts/urls.py, 참고로 accounts는 제가 직접 만든 app의 이름이며 커스텀한 유저 모델이 속해있는 app
from django.urls import include, path

urlpatterns = [
    path("", include("dj_rest_auth.urls")),
    path("", include("dj_rest_auth.registration.urls")),
]
  • 위에서 봤듯, dj_rest_authallauth를 기반의 view들을 RESTFul API를 만들어주는 라이브러리다. 결국 비즈니스 로직은 allauth 를 기반으로 두고있다는 얘기다. 이렇게 url 추가만 해줘도 아래 API 들이 쭉 딸려온다. (빠른 이해를 위해 swagger를 사용했고, 필수 세팅 사항이 아니다. api 접두사는 직접 붙인 세팅값이다.)

  • 하지만 /api/accounts/ 에서 회원가입 로직이 정상 작동을 안한다. 정확하게는 "name" field가 먹통이다.

  • 짧게 allauth 회원가입 flow를 보자면, (account 접두사 table은 allauth 에서 만든 table이다.)

    • (1) 성공적으로 회원가입이된 email은 account_emailaddress 에 insert 되고,
    • (2) verified 된 email은 account_emailconfirmation table에 insert 된다. 후자는 세팅을 안했기 때문에 작동을 안한다.
    • (3) 이후에 우리가 custom한 user model 로 넘어간다.
  • 그리고 우리 custom table의 PK 값을 가지고 있는데, 그 친구는 "name" 값이 비워져있다. 이유는 당연하게, "우리가 커스텀해서"다. 그래서 우리가 model도 커스텀한 경우 라이브러리를 오버라이딩을 해야한다. official docs

2) 커스텀 유저를 위한 세팅 보완하기

  • 역시 보완하는 방법은 다양하다. 직접 라이브러리 "view" 를 오버라이딩하거나 "seriailzer" 를 오버라이딩 하거나 "adpater" 를 활용하거나 등이 있다. 여기서는 간단하게 "seriailzer" 를 활용한 방법을 살펴보자

(1) dj-rest-auth의 기본 RegisterSerializer

  • 회원가입시 처리되는 기본 serialzier의 save 는 위와 같다.
    • (1) "adapter" 라는 놈을 통해서 한 번 이미 save_user 처리를 하고,
    • (2)마지막으로 custom_signup 을 통해 처리한다. 우리는 저 친구를 상속받고 custom_signup 오버라이딩만 하면 우리가 직접 커스텀한 User model 의 custom field를 모두 적절하게 처리할 수 있다.
from dj_rest_auth.registration.serializers import (
    RegisterSerializer as DefaultRegisterSerializer,
)


class UserRegisterSerializer(DefaultRegisterSerializer):
    name = serializers.CharField(max_length=50, write_only=True, required=True)

    def custom_signup(self, request, user):
        name = self.validated_data.pop("name")
        if name:
            user.name = name
            user.save()
  • name field를 추가해버려서 self.validated_data.pop 을 통해 가져오자 그리고 settings.py 에 아래와 같이 추가한다.
# dj-rest-auth config, https://dj-rest-auth.readthedocs.io/en/latest/configuration.html
REST_AUTH = {
	...
    "REGISTER_SERIALIZER": "apps.accounts.serializers.UserRegisterSerializer",
}
  • 정상적으로 name field 값이 추가되며 채워진다. 좀 더 자유로운 편집을 원한다면, 즉 Restful API와 form 까지 동시에 모두 편집하길 원한다면, 이렇게 커스텀 시리얼라이저를 만듦과 동시에 "adpater" 를 활용해야 한다.

  • 그리고 drf에서 제공하는 위 form 을 바꾸고 싶다면!? 직접 오버라이딩을 모두 해야한다.

(2) adapter는 어디에 사용하나요?

  • [DRF] dj_rest_auth로 커스텀 회원가입 구현하기 글로 대체한다.

  • 사실 철저하게 RestFul API로 설계가 목표라면, 그리고 user 와 one2one profile 등의 데이터가 signal 이후 update 되어야 한다면 등의 비즈니스로직이 아니고서야 기본 회원가입에서 굳이 adpater 까지 사용할 필요는 없다.

3) Social Login - Google 추가

  • 일단 우리가 social login을 구현하고 싶은 플랫폼의 OAuth API를 활용해야 한다. 구글 부터 살펴보자. 구글 official docs 를 보자. 특히 타 플랫폼의 OAuth API를 활용해야 하기 때문에 항상 어느정도 업데이트 사항이 있다.

(1) google OAuth API 인가 받기

  • 23년 10월 기준 또 UI가 많이 바뀌었더라.. 최초 세팅하시는 분들은 "프로젝트" 부터 만들어야 한다. 프로젝트를 만들고 -> 사용자 인증 정보를 누르면, OAuth 동의 화면 부터 세팅해야 한다.

  • 여기 callback url은 언제든지 바꿀 수 있으니 일단 나중에 채워넣겠다는 생각으로 진행하면 된다.

  • 만들어진 OAuth API에 활용할 값이 위와 같고, 이제 allauth 를 통해 세팅할 값이다.

(2) google oauth restful API test

  • 구글 official docs 를 보면 "OAuth 인증을 위한 URL" 이 있을 것이다 (어떤 엔드포인트를 가지는지). 기본 url은 https://accounts.google.com/o/oauth2/v2/auth 이며 우리가 만든 저 OAuth API를 호출하도록 필수 query string 값을 추가해야한다.

  • 여기서 꼭 명심해야 할 부분은 우리가 google api 등록하면서 세팅한 scope 등의 설정을 완벽하게 해줘야 에러가 안난다.

  • 위와 같이 구성되며, url을 그대로 입력해서 들어가보면 아래 로그인 창이 뜨는것을 확인할 수 있다. scope 값 많이 실수한다, %20 (공백) 을 url 인코딩한 문자열이다.

  • 로그인을 시도하면 "특정데이터를 같이 포함해서" 등록한 callback url로 가는 것을 볼 수 있다. 우리가 살펴본 OAuth의 request flow를 다시 생각해보자! 이제 이 callback url request 를 우리 django application에서 받아줘야 한다!

(3) settings.py

INSTALLED_APPS = [
	...
    "django.contrib.sites",  # allauth 위해
	...
]

# 사이트는 1개만 사용할 것이라고 명시
SITE_ID = 1
  • sites 가 이제 필요하다! 왜? allauth 를 활용하기 위해 "callback url" 을 등록해야 하는데, allauth 에서는 social login을 위해 기본적으로 sites 패키지에서 세팅된 사이트 도메인을 가져온다.

  • 그래서 해당 설정값을 추가해주고, admin에 가서 "사이트 등록" 을 해주자!

  • 만약 Migration socialaccount.0001_initial is applied before its dependency sites.0001_initial on database 'default'. 를 마주했다면 어쩔수 없다. socialaccount.0001_initialsites.0001_initial 이후에 올 수 없다..! (밀어버리라는 뜻)

  • http://localhost:8000 로 callback url의 domain으로 세팅했으니 위와 같이 세팅하고, 아래 사진과 같이 "소셜 어플리케이션" 을 추가하자

(4) views.py

  • dj_rest_auth 는 기본적으로 allauth와 호환이 되는 SocialLoginView 가 있다. 이 부분을 오버라이딩 하면 된다! -> official docs
# views.py
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.oauth2.client import OAuth2Client
from dj_rest_auth.registration.views import SocialLoginView


class GoogleLoginView(SocialLoginView):
    adapter_class = GoogleOAuth2Adapter
    callback_url = "..."  # 일단 비워보자
    client_class = OAuth2Client


# urls.py
from .views import GoogleLoginView

urlpatterns = [
	...
    path("", include("allauth.urls")),  # OAuth 관련 작업에서도 필요함, Reverse name 때문에
    # google OAuth
    path(
        "google/login",
        GoogleLoginView.as_view(),
        name="api_accounts_google_oauth",
    ),
]
  • 바로 테스트 가능하다! 진행해보면 아래와 같이 될 것 이다.

  • 이 정체 불명의 form은? path("", include("allauth.urls")), 을 통해 추가된 allauth 기본 제공 django model form & template 이다. 왜 실패할까? 일단 결론 부터 보자면, social login도 결국 user model을 만들어야 하고, 라이브러리들은 social login을 통해 들어온 user model field 들이 우리가 커스텀을 어떻게 했는지 모른다. 세부 사항은 밑에서 더 살펴보자!

(5) 직접 OAuth 태워보기

  • 일단 path("", include("allauth.urls")), 를 빼고 API test tool (또는 직접 curl 사용) 으로 OAuth flow를 그대로 태워보자!

  • 그러면 404 를 마주하게 된다. 너무 당연하게 우리가 callback url을 세팅하지 않았기 때문이다. path("", include("allauth.urls")) 를 통해 등록된 view 가 해당 부분을 처리해서 이전과 같이 보여졌던 것이다. 여기서 google OAuth를 위해 "code" 값이 필요하다.

  • 기본적으로 url 인코딩되어있기 때문에 (특수기호 등때문) url 디코딩 이후 복사해서 사용해야 한다!

  • 이제 로그인을 시도해서 404를 마주한 브라우저의 콘솔에서 (쿠키값들 때문에) 아래 js 코드로 우리가 직접 만든 GoogleLoginView 로 요청을 때려보자! code 값은 위에서 가져온 code 값이다.
const options = {
  method: 'POST',
  headers: {'Content-Type': 'application/json', 'User-Agent': 'insomnia/8.3.0'},
  body: '{"code":"4/0AfJohXm9AGu8M9jYezLgoSTmOyf7m4FoCK6K78SHo3kUB045AQEPllr5gNtlAyQYMJE5oQ"}'
};

fetch('http://127.0.0.1:8000/api/accounts/google/login', options)
  .then(response => response.json())
  .then(response => console.log(response))
  .catch(err => console.error(err));

  • 그러면 위와 같이 성공할 것이다. 정상적으로 custom user model 역시 생성되었다. 크게 연계 생성된 데이터는 아래와 같다!

(6) 만약 에러를 마주했다면?

  • 위와 모두 같은데 아래와 같은 "500 error" 는 99% google OAuth API url 또는 google OAuth API 설정 이슈다. 특히 scope 부분을 dubble check 하자!

4) Social 회원가입 & 로그인 마무리

(1) path("", include("allauth.urls")) 대체하기

  • 우린 일단 path("", include("allauth.urls")) 를 사용하지 않을 것이다. dj-rest-auth 를 사용하는 것 자체가 RESTful API를 지향하는 것인데 allauth에서 제공하는 template을 그대로 사용하면 의미가 없다.

  • 그리고 앞 글에서 살펴본 직접 simpleJWT를 활용한 sign-in / sign-out 를 구현할 필요가 없다. 이제 라이브러리를 오버라이딩해서 진행하면 된다.

  • 일단 "로그인 페이지" 를 가볍게 구성하고 callback url을 받아서 우리가 만든 GoogleLoginView 의 endpoint로 request를 쏴줄, 리다이렉트 해줄 API를 만들자.

먼저 로그인 페이지

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Login Page</title>
    <style>
      body {
        font-family: Arial, sans-serif;
        background-color: #f5f5f5;
        display: flex;
        align-items: center;
        justify-content: center;
        height: 100vh;
        margin: 0;
      }

      .login-container {
        background-color: #fff;
        padding: 20px 30px;
        border-radius: 8px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        text-align: center;
      }

      .google-login-btn img {
        width: 200px;
        border-radius: 3px;
        transition: opacity 0.3s;
      }

      .google-login-btn:hover img {
        opacity: 0.8;
      }
    </style>
  </head>

  <body>
    <div class="login-container">
      <h2>Login</h2>
      <a
        href="https://accounts.google.com/o/oauth2/v2/auth?scope=profile%20email&client_id=455357967138-vtrajmgv0f463ab0me0ajhasu5td5b2k.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Fapi%2Faccounts%2Fgoogle%2Flogin%2Fcallback%2F&response_type=code"
        class="google-login-btn"
      >
        <img
          src="https://developers.google.com/identity/images/btn_google_signin_dark_normal_web.png"
          alt="Google Login"
        />
      </a>
    </div>
  </body>
</html>

로그인 이후 google api console에 등록할 callback api

# views.py
import requests

... #생략

class GoogleOAuthCallbackView(APIView):
    def get(self, request: Request):
        # code 값을 URL의 query string에서 추출
        if code := request.GET.get("code"):
            response = self.forward_code_to_google_login_view(code)
            if response.status_code == 200:
                return Response(response.json(), status=status.HTTP_200_OK)
            return Response(
                {"error": "Failed to process with GoogleLoginView"},
                status=response.status_code,
            )

        return Response(
            {"error": "Code not provided"}, status=status.HTTP_400_BAD_REQUEST
        )

    def forward_code_to_google_login_view(self, code: str):
        url = "http://localhost:8000/api/accounts/google/login/"
        payload = {"code": code}
        headers = {"Content-Type": "application/json"}
        response = requests.post(url, json=payload, headers=headers)
        return response


class GoogleLoginView(SocialLoginView):
    adapter_class = GoogleOAuth2Adapter
    callback_url = "http://localhost:8000/api/accounts/google/login/callback"
    client_class = OAuth2Client

    def post(self, request, *args, **kwargs):
        print(request.POST)
        return super().post(request, *args, **kwargs)
        
# urls.py
urlpatterns = [
	...
    # google OAuth
    path(
        "google/login/callback/",
        GoogleOAuthCallbackView.as_view(),
        name="api_accounts_google_oauth_callback",
    ),
    path(
        "google/login/",
        GoogleLoginView.as_view(),
        name="api_accounts_google_oauth",
    )
]
  • GoogleOAuthCallbackView 의 경우 실제 Backend에서 저렇게 처리할 수 있으나, 대게 FE에서 callback을 처리한다. 실제 운영환경이라면 GoogleOAuthCallbackView 를 대체할 수 있는 FE가 필요하다.

(2) custom User model field 채우게 CustomSocialAccountAdapter 만들기

  • GoogleLoginView 에서 처리되는 로직, 라이브러리 코어는 "우리가 유저 모델을 커스텀한지 모른다!" 그래서 allauth 에서 사용하는 adapter 를 오버라이딩 해야한다! 일단 아래 코드를 먼저 보자!
# settings.py
...
SOCIALACCOUNT_ADAPTER = "apps.accounts.adapter.CustomSocialAccountAdapter"


# adapater.py
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter


class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
    def save_user(self, request, sociallogin, form=None):
        user = super().save_user(request, sociallogin, form)
        oauth_data = sociallogin.account.extra_data
        user.name = oauth_data.get("name")
        user.save()
        return user
  • settings.py 에서 allauth 설정으로 SOCIALACCOUNT_ADAPTER 를 세팅해준다. 그리고 CustomSocialAccountAdapterfrom allauth.socialaccount.adapter import DefaultSocialAccountAdapter 를 상속 받는다.

  • save_user 에서 넘어오는 socialloginallauth.socialaccount.models.SocialLogin instance 이다. 해당 class를 (model)을 살펴보면 아래와 같다.

  • 여기서 살펴볼 method는 sociallogin.serialize() 이다. OAuth를 통해 social platform 에서 우리가 scope로 잡은 유저의 개인정보가 넘어오고, allauth를 통해 시리얼라이징 되어서 저장된다. 그 데이터는 위에서도 살펴본 socialaccount_socialaccount table에 저장된다.

  • sociallogin.accountsocialaccount_socialaccount model instance 에 접근할 수 있기 때문에 sociallogin.account.extra_data 에서 우리가 원하는 데이터를 가져오면 된다. 그래서 코드가 user.name = oauth_data.get("name") 로 되는 것이다.


3. 마무리

  • 라이브러리도 꽤 업데이트되고, 변동되는 사항있어서 official docs가 가장 믿을만하다. OAuth의 구현보다 라이브러리 기반으로 커스텀해서 구현하는 난이도 때문에 초반 러닝커브가 생각보다 높다. 그러나 인증가 인가인 만큼 초반에 제대로 잡아둬야 미래에 문제없이 편하게 활용가능하다.

  • 구현 방향에서 언급했듯, 다음 글은 jwt token의 탈취된 상황의 피해 최소를 위해 만료시간(expires date) 뿐 아니라 "Blacklist" 를 세팅해보고, OAuth & 자체 회원가입 모두 email 인증" 을 진행하려고 한다.


1. (🥔 이전 게시글) simptJWT 로 JWT token based sign-up & sign-in 구현
2. allauth & dj-rest-auth 로 JWT token based RESTful 한 user 관련 API 구현 - 커스텀 유저 모델 사용하도록 세팅
3. allauth & dj-rest-auth social login - OAuth for google 구현
4. (🥔 다음 게시글) web token의 한계를 최대한 극복하기 위한 "Blacklist" 세팅
5. (🥔 다음 게시글) 모든 회원가입 과정에서 email 인증 받기


출처

profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!

2개의 댓글

comment-user-thumbnail
2023년 10월 26일

너무 알차요!

1개의 답글