wagnerdelima/drf-social-oauth2

Authorization backend function do_auth called twice

Opened this issue · 1 comments

I've implemented apple sign-in following this article using subclass:
https://github.com/truffls/sign-in-with-apple-using-django/blob/master/backend.md

Code:

import jwt
import requests
from datetime import timedelta
from django.conf import settings
from django.utils import timezone
from social_core.utils import handle_http_errors
from social_core.backends.oauth import BaseOAuth2


class AppleOAuth2(BaseOAuth2):
    name = 'apple'
    ACCESS_TOKEN_URL = 'https://appleid.apple.com/auth/token'
    SCOPE_SEPARATOR = ','
    ID_KEY = 'uid'

    @handle_http_errors
    def do_auth(self, access_token, *args, **kwargs):
        response_data = {}
        client_id, client_secret = self.get_key_and_secret()
        
        headers = {'content-type': "application/x-www-form-urlencoded"}
        data = {
            'client_id': client_id,
            'client_secret': client_secret,
            'code': access_token,
            'grant_type': 'authorization_code',
        }

        res = requests.post(AppleOAuth2.ACCESS_TOKEN_URL, data=data, headers=headers)
        response_dict = res.json()
        id_token = response_dict.get('id_token', None)

        if id_token:
            decoded = jwt.decode(id_token, '', algorithms=["ES256"], options={"verify_signature": False})
            response_data.update({'email': decoded['email']}) if 'email' in decoded else None
            response_data.update({'uid': decoded['sub']}) if 'sub' in decoded else None

        response = kwargs.get('response') or {}
        response.update(response_data)
        response.update({'access_token': access_token}) if 'access_token' not in response else None

        kwargs.update({'response': response, 'backend': self})
        return self.strategy.authenticate(*args, **kwargs)

    def get_user_details(self, response):
        email = response.get('email', None)
        details = {
            'email': email,
        }

        return details

    def get_key_and_secret(self):
        headers = {
            'kid': settings.SOCIAL_AUTH_APPLE_ID_KEY
        }**strong text**

        payload = {
            'iss': settings.SOCIAL_AUTH_APPLE_ID_TEAM,
            'iat': timezone.now(),
            'exp': timezone.now() + timedelta(days=180),
            'aud': 'https://appleid.apple.com',
            'sub': settings.SOCIAL_AUTH_APPLE_ID_CLIENT,
        }

        client_secret = jwt.encode(
            payload,
            settings.SOCIAL_AUTH_APPLE_ID_SECRET,
            algorithm='ES256',
            headers=headers
        )
        
        return settings.SOCIAL_AUTH_APPLE_ID_CLIENT, client_secret

Authorization succeeds during first call, i receive access token from apple, new entries in Users & User social auths tables are created but after that function do_auth is called one more time causing duplicate entry:
enter image description here

The very time user logs in - new entry in Users & User social auths tables created.
enter image description here

During the second call id_token is empty because apple's authorization code is one time token.

in oauth2_endpoints.py after user was created it searches for access token and doesn't find any thus calls again:

enter image description here

Access token isn't created during the first iteration, I can't understand why. During the second call it again creates user, social auth user and this time access & refresh tokens are created. So every time I perform sign in via above auth backend new user is created.
Can somebody help me? My head is gonna blow up

I have the same problem in the covert_token flow.
At the moment I solved it by implementing some kind of cache to be able to query the id_token in the second call, in the following example I use a static dictionary as cache.

from social_core.backends.apple import AppleIdAuth
from social_core.utils import handle_http_errors


class AppleOAuth2(AppleIdAuth):
    name = 'apple'
    cache = {}

    @handle_http_errors
    def _auth_complete(self, access_token):
        data = self.auth_complete_params()
        data.update({'code': access_token})
        response = self.request_access_token(
            self.access_token_url(),
            data=data,
            headers=self.auth_headers(),
            auth=self.auth_complete_credentials(),
            method=self.ACCESS_TOKEN_METHOD
        )
        self.process_error(response)
        return response

    def do_auth(self, access_token, *args, **kwargs):
        response = kwargs.pop('response', None) or {}
        jwt_string = response.get(self.TOKEN_KEY) or AppleOAuth2.cache.pop(access_token, '')

        if not jwt_string:
            response = self._auth_complete(access_token, *args, **kwargs)
            jwt_string = response.get(self.TOKEN_KEY)
            AppleOAuth2.cache.update({access_token: jwt_string})
        else:
            response = {self.TOKEN_KEY: jwt_string}

        return super().do_auth(access_token, response=response, *args, **kwargs)