미쳐버리지 않고 OAuth 2를 Django/DRF 백엔드에 통합하는 방법
게시 됨: 2022-03-11우리는 모두 거기에 있었다. API 백엔드 작업을 하고 있으며 진행 상황에 만족하고 있습니다. 최근에 MVP(Minimal Viable Product)를 완료했으며 테스트를 모두 통과했으며 몇 가지 새로운 기능을 구현하기를 고대하고 있습니다.
그런 다음 상사는 이메일을 보냅니다. “그런데 사람들이 Facebook과 Google을 통해 로그인할 수 있도록 해야 합니다. 그들은 우리와 같은 작은 사이트를 위한 계정을 만들 필요가 없습니다.”
엄청난. 스코프 크립이 다시 공격합니다.
좋은 소식은 OAuth 2가 소셜 및 제3자 인증(Facebook, Google 등의 서비스에서 사용)을 위한 업계 표준으로 부상했기 때문에 해당 표준을 이해하고 구현하여 광범위한 소셜 인증을 지원한다는 것입니다. 인증 공급자.
OAuth 2에 익숙하지 않을 수 있습니다. 나에게 이런 일이 일어났을 때 나는 그렇지 않았다.
Python 개발자로서 본능에 따라 Python 패키지 설치를 위한 PyPA(Python Package Index) 권장 도구인 pip가 표시될 수 있습니다. 나쁜 소식은 pip가 OAuth를 처리하는 278개의 패키지에 대해 알고 있다는 것입니다. 그 중 53개는 Django를 구체적으로 언급합니다. 옵션을 조사하고 코드 작성을 시작하는 데는 일주일의 가치가 있습니다.
이 튜토리얼에서는 Python Social Auth를 사용하여 OAuth 2를 Django 또는 Django Rest Framework에 통합하는 방법을 배웁니다. 이 기사는 Django REST Framework에 초점을 맞추고 있지만 여기에 제공된 정보를 적용하여 다양한 다른 공통 백엔드 프레임워크에서 동일한 것을 구현할 수 있습니다.
OAuth 2 흐름에 대한 간략한 개요
OAuth 2는 처음부터 웹 인증 프로토콜로 설계되었습니다. 이것은 네트 인증 프로토콜로 설계된 것과 완전히 같지 않습니다. HTML 렌더링 및 브라우저 리디렉션과 같은 도구를 사용할 수 있다고 가정합니다.
이것은 분명히 JSON 기반 API의 장애물이지만 이 문제를 해결할 수 있습니다.
전통적인 서버 측 웹 사이트를 작성하는 것처럼 프로세스를 진행합니다.
서버 측 OAuth 2 흐름
첫 번째 단계는 애플리케이션 흐름 외부에서 완전히 발생합니다. 프로젝트 소유자는 로그인이 필요한 각 OAuth 2 제공업체에 애플리케이션을 등록해야 합니다.
이 등록 중에 애플리케이션이 요청을 수신하는 데 사용할 수 있는 콜백 URI 를 OAuth 2 제공업체에 제공합니다. 그 대가로 클라이언트 키 와 클라이언트 암호 를 받습니다. 이러한 토큰은 로그인 요청의 유효성을 검사하기 위해 인증 프로세스 중에 교환됩니다.
토큰은 서버 코드를 클라이언트로 참조합니다. 호스트는 OAuth 2 공급자입니다. API 클라이언트를 위한 것이 아닙니다.
흐름은 애플리케이션이 "Facebook으로 로그인" 또는 "Google+로 로그인"과 같은 버튼이 포함된 페이지를 생성할 때 시작됩니다. 기본적으로 이들은 다음과 같은 URL을 가리키는 단순한 링크에 불과합니다.
https://oauth2provider.com/auth? response_type=code& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email
(참고: 가독성을 위해 위의 URI에 줄 바꿈이 삽입되었습니다.)
클라이언트 키와 리디렉션 URI를 제공했지만 비밀은 제공하지 않았습니다. 그 대가로 서버에 응답으로 인증 코드를 원하고 '프로필' 및 '이메일' 범위에 대한 액세스를 원한다고 말했습니다. 이러한 범위는 사용자에게 요청하는 권한을 정의하고 받는 액세스 토큰의 권한 부여를 제한합니다.
수신 시 사용자의 브라우저는 OAuth 2 공급자가 제어하는 동적 페이지로 연결됩니다. OAuth 2 공급자는 계속하기 전에 콜백 URI와 클라이언트 키가 서로 일치하는지 확인합니다. 그렇다면 사용자의 세션 토큰에 따라 흐름이 잠시 분기됩니다.
사용자가 현재 해당 서비스에 로그인하지 않은 경우 로그인하라는 메시지가 표시됩니다. 사용자가 로그인하면 애플리케이션이 로그인할 수 있도록 권한을 요청하는 대화 상자가 표시됩니다.
사용자가 승인한다고 가정하면 OAuth 2 서버는 쿼리 매개변수 GET https://api.yourapp.com/oauth2/callback/?code=AUTH_CODE
의 인증 코드 를 포함하여 사용자가 제공한 콜백 URI로 다시 리디렉션합니다.
인증 코드는 빨리 만료되는 일회용 토큰입니다. 수신 즉시 서버는 돌아서서 인증 코드와 클라이언트 암호를 포함하여 OAuth 2 제공자에게 또 다른 요청을 해야 합니다.
POST https://oauth2provider.com/token/? grant_type=authorization_code& code=AUTH_CODE& redirect_uri=CALLBACK_URI& client_id=CLIENT_KEY& client_secret=CLIENT_SECRET
이 인증 코드의 목적은 위의 POST 요청을 인증하는 것이지만 흐름의 특성으로 인해 사용자 시스템을 통해 라우팅되어야 합니다. 따라서 본질적으로 불안정합니다.
인증 코드에 대한 제한 사항(즉, 빠르게 만료되고 한 번만 사용할 수 있음)은 신뢰할 수 없는 시스템을 통해 인증 자격 증명을 전달하는 고유한 위험을 완화하기 위한 것입니다.
서버에서 OAuth 2 공급자의 서버로 직접 수행되는 이 호출은 OAuth 2 서버 측 로그인 프로세스의 핵심 구성 요소입니다. 통화를 제어한다는 것은 통화가 TLS로 보호된다는 것을 알고 있다는 것을 의미하므로 도청 공격으로부터 통화를 보호하는 데 도움이 됩니다.
인증 코드를 포함하면 사용자가 명시적으로 동의했음을 확인할 수 있습니다. 사용자에게 절대 표시되지 않는 클라이언트 암호를 포함하면 이 요청이 인증 코드를 가로채는 사용자 시스템의 일부 바이러스 또는 맬웨어에서 시작되지 않습니다.
모든 것이 일치하면 서버는 액세스 토큰 을 반환합니다. 이 토큰을 사용하여 사용자로 인증된 동안 해당 공급자를 호출할 수 있습니다.
서버에서 액세스 토큰을 받으면 서버는 사용자의 브라우저를 방금 로그인한 사용자의 방문 페이지로 다시 한 번 리디렉션합니다. 액세스 토큰을 사용자의 서버 측 세션 캐시에 유지하는 것이 일반적이므로 서버는 필요할 때마다 주어진 소셜 제공자를 호출할 수 있습니다.
사용자가 액세스 토큰을 사용할 수 있게 해서는 안 됩니다!
더 자세히 알아볼 수 있습니다.
예를 들어 Google에는 액세스 토큰의 수명을 연장하는 새로 고침 토큰 이 포함되어 있는 반면 Facebook은 수명이 짧은 액세스 토큰을 더 오래 지속되는 것과 교환할 수 있는 끝점을 제공합니다. 하지만 이 흐름을 사용하지 않을 것이기 때문에 이러한 세부 정보는 중요하지 않습니다.
이 흐름은 REST API의 경우 번거롭습니다. 프론트 엔드 클라이언트가 초기 로그인 페이지를 생성하고 백엔드가 콜백 URL을 제공하도록 할 수 있지만 결국에는 문제에 봉착하게 됩니다. 액세스 토큰을 받은 후 사용자를 프런트 엔드의 랜딩 페이지로 리디렉션하려고 하는데 그렇게 하는 명확하고 RESTful한 방법이 없습니다.
운 좋게도 사용 가능한 또 다른 OAuth 2 흐름이 있으며 이 경우 훨씬 더 잘 작동합니다.
클라이언트 측 OAuth 2 흐름
이 흐름에서 프런트 엔드는 전체 OAuth 2 프로세스를 처리합니다. 일반적으로 서버 측 흐름과 비슷하지만 중요한 예외는 프런트 엔드가 사용자가 제어하는 시스템에 있으므로 클라이언트 암호를 신뢰할 수 없다는 점입니다. 솔루션은 단순히 프로세스의 전체 단계를 제거하는 것입니다.
서버 측 흐름과 마찬가지로 첫 번째 단계는 애플리케이션을 등록하는 것입니다.
이 경우 프로젝트 소유자는 여전히 응용 프로그램을 등록하지만 웹 응용 프로그램으로 등록합니다. OAuth 2 공급자는 여전히 클라이언트 키 를 제공하지만 클라이언트 암호는 제공하지 않을 수 있습니다.
프론트 엔드는 OAuth 2 공급자가 제어하는 웹 페이지로 연결되는 소셜 로그인 버튼을 사용자에게 제공하고 애플리케이션이 사용자 프로필의 특정 측면에 액세스할 수 있는 권한을 요청합니다.
하지만 이번에는 URL이 약간 다르게 보입니다.
https://oauth2provider.com/auth? response_type=token& client_id=CLIENT_KEY& redirect_uri=CALLBACK_URI& scope=profile& scope=email
이번에는 URL의 response_type
매개변수가 token
입니다.
그렇다면 리디렉션 URI는 어떻습니까?
이것은 액세스 토큰을 적절하게 처리할 준비가 된 프런트 엔드의 모든 주소입니다.
사용 중인 OAuth 2 라이브러리에 따라 프런트 엔드는 실제로 일시적으로 사용자 장치에서 HTTP 요청을 수락할 수 있는 서버를 실행할 수 있습니다. 이 경우 리디렉션 URL은 http://localhost:7862/callback/?token=TOKEN
형식입니다.
OAuth 2 서버는 사용자가 수락한 후 HTTP 리디렉션을 반환하고 이 리디렉션은 사용자 장치의 브라우저에서 처리되기 때문에 이 주소는 올바르게 해석되어 토큰에 대한 프런트 엔드 액세스 권한을 부여합니다.
또는 프런트 엔드에서 적절한 페이지를 직접 구현할 수 있습니다. 어느 쪽이든 프런트 엔드는 이 시점에서 쿼리 매개 변수를 구문 분석하고 액세스 토큰을 처리합니다.
이 시점부터 프런트 엔드는 토큰을 사용하여 OAuth 2 공급자의 API를 직접 호출할 수 있습니다. 그러나 사용자는 그것을 원하지 않습니다. 그들은 API에 대한 인증된 액세스를 원합니다. 백엔드가 제공해야 하는 모든 엔드포인트는 프론트엔드가 소셜 제공자의 액세스 토큰을 API에 대한 액세스 권한을 부여하는 토큰과 교환할 수 있는 끝점입니다.
프론트 엔드에 액세스 토큰을 제공하는 것이 본질적으로 서버 측 흐름보다 덜 안전하다는 점을 감안할 때 이것을 허용하는 이유는 무엇입니까?
클라이언트 측 흐름을 사용하면 백엔드 REST API와 사용자 대면 프론트엔드를 더 엄격하게 구분할 수 있습니다. 백엔드 서버를 리디렉션 URI로 지정하는 것을 엄격히 막는 것은 없습니다. 최종 효과는 일종의 하이브리드 흐름이 될 것입니다.
문제는 서버가 적절한 사용자 대면 페이지를 생성한 다음 어떤 식으로든 프론트엔드에 제어권을 다시 넘겨야 한다는 것입니다.
모든 비즈니스 로직을 처리하는 프론트엔드 UI와 백엔드 사이의 관심사를 엄격하게 분리하는 것은 현대 프로젝트에서 일반적입니다. 일반적으로 잘 정의된 JSON API를 통해 통신합니다. 위에서 설명한 하이브리드 흐름은 문제의 분리를 흐리게 하므로 백엔드가 둘 다 사용자 대면 페이지를 제공하고 일부 흐름을 설계하여 어떻게든 제어를 다시 프런트 엔드로 되돌려야 합니다.

프런트 엔드가 액세스 토큰을 처리하도록 허용하는 것은 관심사의 분리를 유지하는 편리한 기술입니다. 손상된 클라이언트의 위험이 다소 증가하지만 일반적으로 잘 작동합니다.
이 흐름은 프런트 엔드에서 복잡해 보일 수 있으며 프런트 엔드 팀이 모든 것을 자체적으로 개발해야 하는 경우 그렇습니다. 그러나 Facebook과 Google 모두 최소한의 구성으로 전체 프로세스를 처리하는 로그인 버튼을 프론트 엔드에 포함할 수 있는 라이브러리를 제공합니다.
다음은 백엔드에서 토큰을 교환하는 방법입니다.
클라이언트 흐름에서 백엔드는 OAuth 2 프로세스와 상당히 격리되어 있습니다. 오해하지 마세요. 이것은 간단한 작업이 아닙니다. 최소한 다음 기능을 지원하기를 원할 것입니다.
- 임의의 임의 문자열이 아니라 프런트 엔드에서 제공한 토큰이 유효한지 확인하기 위해 OAuth 2 공급자에게 하나 이상의 요청을 보냅니다.
- 토큰이 유효하면 API에 대해 유효한 토큰을 반환하십시오. 그렇지 않으면 정보 오류를 반환합니다.
- 새 사용자인 경우 해당 사용자를 위한
User
모델을 만들고 적절하게 채우십시오. -
User
모델이 이미 존재하는 사용자인 경우 이메일 주소와 일치시켜 소셜 로그인을 위해 새 계정을 만드는 대신 올바른 기존 계정에 액세스할 수 있도록 합니다. - 소셜 미디어에서 제공한 정보를 기반으로 사용자의 프로필 세부 정보를 업데이트합니다.
좋은 소식은 백엔드에서 이 모든 기능을 구현하는 것이 예상보다 훨씬 간단하다는 것입니다.
단 24줄의 코드로 백엔드에서 이 모든 작업을 수행하는 방법에 대한 마법이 있습니다. 이것은 Python Social Auth 라이브러리(이하 "PSA")에 따라 다르므로 requirements.txt에 social-auth-core
및 social-auth-app-django
를 모두 포함해야 requirements.txt
.
또한 여기에 설명된 대로 라이브러리를 구성해야 합니다. 여기에는 명확성을 위해 일부 예외 처리가 제외됩니다.
이 예제의 전체 코드는 여기에서 찾을 수 있습니다.
@api_view(http_method_names=['POST']) @permission_classes([AllowAny]) @psa() def exchange_token(request, backend): serializer = SocialSerializer(data=request.data) if serializer.is_valid(raise_exception=True): # This is the key line of code: with the @psa() decorator above, # it engages the PSA machinery to perform whatever social authentication # steps are configured in your SOCIAL_AUTH_PIPELINE. At the end, it either # hands you a populated User model of whatever type you've configured in # your project, or None. user = request.backend.do_auth(serializer.validated_data['access_token']) if user: # if using some other token back-end than DRF's built-in TokenAuthentication, # you'll need to customize this to get an appropriate token object token, _ = Token.objects.get_or_create(user=user) return Response({'token': token.key}) else: return Response( {'errors': {'token': 'Invalid token'}}, status=status.HTTP_400_BAD_REQUEST, )
설정(전체 코드)에 들어가야 할 것이 조금 더 있습니다. 그러면 모든 설정이 완료됩니다.
AUTHENTICATION_BACKENDS = ( 'social_core.backends.google.GoogleOAuth2', 'social_core.backends.facebook.FacebookOAuth2', 'django.contrib.auth.backends.ModelBackend', ) for key in ['GOOGLE_OAUTH2_KEY', 'GOOGLE_OAUTH2_SECRET', 'FACEBOOK_KEY', 'FACEBOOK_SECRET']: # Use exec instead of eval here because we're not just trying to evaluate a dynamic value here; # we're setting a module attribute whose name varies. exec("SOCIAL_AUTH_{key} = os.environ.get('{key}')".format(key=key)) SOCIAL_AUTH_PIPELINE = ( 'social_core.pipeline.social_auth.social_details', 'social_core.pipeline.social_auth.social_uid', 'social_core.pipeline.social_auth.auth_allowed', 'social_core.pipeline.social_auth.social_user', 'social_core.pipeline.user.get_username', 'social_core.pipeline.social_auth.associate_by_email', 'social_core.pipeline.user.create_user', 'social_core.pipeline.social_auth.associate_user', 'social_core.pipeline.social_auth.load_extra_data', 'social_core.pipeline.user.user_details', )
urls.py
에서 이 함수에 대한 매핑을 추가하면 모든 설정이 완료됩니다!
그 마법은 어떻게 작동합니까?
Python Social Auth는 매우 훌륭하고 매우 복잡한 기계입니다. 수십 개의 소셜 인증 제공자에 대한 인증 및 액세스를 처리하는 것은 매우 기쁩니다. Django, Flask, Pyramid, CherryPy 및 WebPy를 비롯한 가장 인기 있는 Python 웹 프레임워크에서 작동합니다.
대부분의 경우 위 코드는 매우 표준적인 DRF(Django REST Framework) 기능 기반 보기입니다. urls.py
에서 매핑한 경로에 상관없이 POST 요청을 수신하고, 요청을 보낸다고 가정합니다. 형식을 지정하면 User
개체 또는 None
을 얻습니다.
User
개체를 얻는 경우 이미 존재하거나 존재하지 않을 수 있는 프로젝트의 다른 곳에서 구성한 모델 유형입니다. PSA는 이미 토큰 유효성 검사, 사용자 일치 여부 식별, 필요한 경우 사용자 생성, 소셜 제공자의 사용자 세부 정보 업데이트를 처리했습니다.
사용자가 소셜 제공자의 사용자에서 귀하의 사용자로 매핑되고 기존 사용자와 연결되는 방법에 대한 정확한 세부 정보는 위에 정의된 SOCIAL_AUTH_PIPELINE
에 의해 지정됩니다. 이 모든 것이 어떻게 작동하는지에 대해 배울 것이 더 많지만 이 게시물의 범위를 벗어납니다. 여기에서 자세한 내용을 읽을 수 있습니다.
마술의 핵심은 뷰의 @psa()
데코레이터로, 뷰에 전달되는 request
객체에 일부 멤버를 추가합니다. 우리에게 가장 흥미로운 것은 request.backend
입니다(PSA에게 백엔드는 모든 소셜 인증 제공자입니다).
적절한 백엔드가 선택되어 URL 자체에 의해 채워지는 보기에 대한 backend
인수를 기반으로 request
객체에 추가되었습니다.
backend
객체를 손에 넣으면 액세스 코드가 주어지면 해당 공급자에 대해 인증하는 것이 매우 기쁩니다. 그것이 do_auth
메소드입니다. 이는 차례로 구성 파일의 SOCIAL_AUTH_PIPELINE
전체를 사용합니다.
파이프라인은 기본 내장 기능 외에는 필요한 모든 것을 이미 수행하지만 확장하면 꽤 강력한 작업을 수행할 수 있습니다.
그 후에는 정상적인 DRF 코드로 돌아갑니다. 유효한 User
개체가 있으면 적절한 API 토큰을 매우 쉽게 반환할 수 있습니다. 유효한 User
개체를 다시 받지 못한 경우 오류가 발생하기 쉽습니다.
이 기술의 한 가지 단점은 오류가 발생한 경우 반환하는 것이 비교적 간단하지만 구체적으로 무엇이 잘못되었는지에 대한 많은 통찰력을 얻기가 어렵다는 것입니다. PSA는 문제가 무엇인지에 대해 서버가 반환했을 수 있는 모든 세부 정보를 삼킵니다.
다시 말하지만, 잘 설계된 인증 시스템의 특성상 오류 소스에 대해 상당히 불투명합니다. 로그인 시도 후 응용 프로그램에서 사용자에게 "잘못된 암호"를 알리는 경우 "축하합니다! 유효한 사용자 이름을 추측했습니다."
왜 당신의 것을 굴리지 않습니까?
한마디로 확장성. 아주 소수의 소셜 OAuth 2 제공업체가 API 호출에서 정확히 동일한 방식으로 정확히 동일한 정보를 요구하거나 반환합니다. 모든 종류의 특별한 경우와 예외가 있습니다.
PSA를 이미 설정한 후 새 소셜 공급자를 추가하는 것은 설정 파일에서 몇 줄의 구성 문제입니다. 코드를 전혀 조정할 필요가 없습니다. PSA는 이 모든 것을 추상화하여 사용자가 자신의 애플리케이션에 집중할 수 있도록 합니다.
도대체 이것을 어떻게 테스트합니까?
좋은 질문! unittest.mock
은 라이브러리 깊숙이 있는 추상화 계층 아래에 묻혀 있는 API 호출을 조롱하는 데 적합하지 않습니다. 모의에 대한 정확한 경로를 찾는 것만으로도 상당한 노력이 필요할 것입니다.
대신 PSA가 Requests 라이브러리 위에 구축되기 때문에 우수한 Responses 라이브러리를 사용하여 HTTP 수준에서 공급자를 조롱합니다.
테스트에 대한 전체 논의는 이 기사의 범위를 벗어나지만 테스트 샘플이 여기에 포함되어 있습니다. mocked
된 컨텍스트 관리자와 SocialAuthTests
클래스가 있다는 점에 유의해야 합니다.
PSA가 힘든 일을 하도록 하십시오.
OAuth2 프로세스는 내재된 복잡성으로 인해 상세하고 복잡합니다. 운 좋게도 가능한 한 고통 없이 처리하는 전용 라이브러리를 가져와서 이러한 복잡성을 대부분 우회할 수 있습니다.
Python Social Auth는 그 일을 훌륭하게 수행합니다. 우리는 클라이언트 측의 암시적 OAuth2 흐름을 활용하여 단 25줄의 코드로 원활한 사용자 생성 및 일치를 얻는 Django/DRF 보기를 시연했습니다. 너무 초라하지 않습니다.