Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apple social login #226

Open
Allan-Nava opened this issue Sep 24, 2019 · 16 comments
Open

Apple social login #226

Allan-Nava opened this issue Sep 24, 2019 · 16 comments

Comments

@Allan-Nava
Copy link

When is possible to implement the social login with apple sign in?

@AlBartash
Copy link

+1 to the request

@ramonyaskal
Copy link

+1

@aehlke
Copy link

aehlke commented Jan 23, 2020

Please don't spam subscribers with +1 replies and use emoji response to the OP, thanks.

@goetzb
Copy link

goetzb commented Apr 12, 2020

This should now be possible with Python Social Auth - Core 3.3.0 (social-auth-core==3.3.0).

You can find the backend named apple-id in https://github.com/python-social-auth/social-core/blob/master/social_core/backends/apple.py - I believe you can use that release of social-auth-core together with the existing release of social-auth-app-django.

@Allan-Nava
Copy link
Author

This should now be possible with Python Social Auth - Core 3.3.0 (social-auth-core==3.3.0).

You can find the backend named apple-id in https://github.com/python-social-auth/social-core/blob/master/social_core/backends/apple.py - I believe you can use that release of social-auth-core together with the existing release of social-auth-app-django.

perfect!

@MiltonMilton
Copy link

is there an implementation example ? im trying to implement this on a django rest app but for some reason the the response that arrives to the apple backend in the do_auth method is empty, any help would be appreciated

@ramonyaskal
Copy link

is there an implementation example ? im trying to implement this on a django rest app but for some reason the the response that arrives to the apple backend in the do_auth method is empty, any help would be appreciated

I had to do it myself, maybe it will help you.

`class AppleOAuth2(BaseOAuth2):
"""apple authentication backend"""

name = 'apple-oauth2'
RESPONSE_TYPE = 'code'
AUTHORIZATION_URL = 'https://appleid.apple.com/auth/authorize'
ACCESS_TOKEN_METHOD = 'POST'
ACCESS_TOKEN_URL = 'https://appleid.apple.com/auth/token'
SCOPE_SEPARATOR = ','
ID_KEY = 'uid'
REDIRECT_STATE = False
jwks_url = 'https://appleid.apple.com/auth/keys'

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    if self.redirect_uri and settings.SCHEME not in self.redirect_uri:
        self.redirect_uri = self.redirect_uri.replace("http", settings.SCHEME) \
            if "http" in self.redirect_uri else self.redirect_uri

def auth_url(self):
    url = super().auth_url()
    extra_params = {
        'response_mode': 'form_post',
        'scope': 'email+name',
    }
    url += ''.join((f'&{key}={extra_params[key]}' for key in extra_params))
    return url

@handle_http_errors
def auth_complete(self, *args, **kwargs):
    """Completes login process, must return user instance"""
    request = kwargs.get('request', None)
    post = request.POST if request else None
    self.STATE_PARAMETER = post.get('state', self.STATE_PARAMETER)

    logger.info(f"Sing with Apple: auth_complete {{'request.POST': {post}}}")

    if 'user' in post:
        kwargs.update({'user_info': json.loads(post['user'])})

    try:
        self.process_error(self.data)
    except social_exceptions.AuthCanceled:
        return None

    state = post.get('state', None) if post else self.validate_state()
    data, params = None, None
    if self.ACCESS_TOKEN_METHOD == 'GET':
        params = self.auth_complete_params(state)
    else:
        data = self.auth_complete_params(state)

    response = self.request_access_token(
        self.access_token_url(),
        data=data,
        params=params,
        headers=self.auth_headers(),
        auth=self.auth_complete_credentials(),
        method=self.ACCESS_TOKEN_METHOD
    )
    logger.info(f"Sing with Apple: auth_complete {{'response': {response}}}")
    self.process_error(response)
    return self.do_auth(response['access_token'], response=response, *args, **kwargs)

@handle_http_errors
def do_auth(self, access_token, *args, **kwargs):
    """
    Finish the auth process once the access_token was retrieved
    Get the email from ID token received from apple
    """
    response_data = {}
    response = kwargs.get('response') or {}
    id_token = response.get('id_token') if 'id_token' in response else None
    user_info = kwargs.get('user_info', None)

    logger.info(f"Sing with Apple: {{'id_token': {id_token}}}")

    if id_token:
        public_key = self.get_public_key(jwks_url=self.jwks_url, id_token=id_token)
        decoded = jwt.decode(id_token, public_key, verify=True, audience=settings.CLIENT_ID)
        logger.info(f"Sing with Apple: {{'decode': {decoded}}}")

        response_data.update({'email': decoded['email']}) if 'email' in decoded else None
        response_data.update({'uid': decoded['sub']}) if 'sub' in decoded else None

        if user_info and 'name' in user_info and user_info['name']:
            response_data.update({'first_name': user_info['name']['firstName']}) \
                if 'firstName' in user_info['name'] else None
            response_data.update({'last_name': user_info['name']['lastName']}) \
                if 'lastName' in user_info['name'] 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})

    # Manual create UserSocialAuth obj if user exists
    email = response.get('email', None)
    if email:
        user = User.objects.filter(email=email).first()
        is_exist_user_auth = UserSocialAuth.objects.filter(
            uid=response.get('uid'),
            user=user,
            provider=self.name,
        ).exists()
        if user and not is_exist_user_auth:
            user_social_auth = UserSocialAuth.objects.create(
                user=user,
                provider=self.name,
                uid=response.get('uid'),
                extra_data={
                    'auth_time': int(time.time()),
                    'access_token': response.get('access_token'),
                    'token_type': response.get('token_type') or kwargs.get('token_type'),
                }
            )

            logger.info(f"Sing with Apple: Manual create obj UserSocialAuth for exist user"
                        f" {{'UserSocialAuth': {user_social_auth}}}")

    logger_data = {
        'args': args,
        'kwargs': kwargs,
    }
    logger.info(f"Sing with Apple: finality do_auth {logger_data}")
    return self.strategy.authenticate(*args, **kwargs)

def get_user_details(self, response):
    email = response.get('email', None)
    first_name = response.get('first_name', None)
    last_name = response.get('last_name', None)
    details = {
        'email': email,
        'is_client': True,
        'is_verified': True,
    }
    details.update({'first_name': first_name}) if first_name else None
    details.update({'last_name': last_name}) if last_name else None
    return details

def get_key_and_secret(self):
    headers = {
        'kid': settings.SOCIAL_AUTH_APPLE_KEY_ID
    }

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

    client_secret = jwt.encode(
        payload,
        settings.SOCIAL_AUTH_APPLE_PRIVATE_KEY,
        algorithm='ES256',
        headers=headers
    ).decode("utf-8")

    return settings.CLIENT_ID, client_secret

@staticmethod
def get_public_key(jwks_url, id_token):
    """
    Apple give public key https://appleid.apple.com/auth/keys
    Example:
        {
          "keys": [
            {
              "kty": "RSA",
              "kid": "86D88Kf",
              "use": "sig",
              "alg": "RS256",
              "n": "some_key",
              "e": "AQAB"
            },
            {
              "kty": "RSA",
              "kid": "eXaunmL",
              "use": "sig",
              "alg": "RS256",
              "n": "some_key",
              "e": "AQAB"
            },
            {
              "kty": "RSA",
              "kid": "AIDOPK1",
              "use": "sig",
              "alg": "RS256",
              "n": "some_key",
              "e": "AQAB"
            }
          ]
        }
    Use kid for give some parameters for public key.

    Now use by default "kid": "eXaunmL" ago get ['keys'][1]
    Decode https://jwt.io/ id_token and show what kid in HEADER
    :return: RSAAlgorithm obj
    """
    header = jwt.get_unverified_header(id_token)
    kid = header['kid']

    response = requests.get(jwks_url)
    if response.ok:
        json_data = response.json()
        for key_data in json_data['keys']:
            if key_data['kid'] == kid:
                key_json = json.dumps(key_data)
                return RSAAlgorithm.from_jwk(key_json)

        # Or used default
        keys_json = json.dumps(json_data['keys'][1])
        return RSAAlgorithm.from_jwk(keys_json)`

@MiltonMilton
Copy link

is there an implementation example ? im trying to implement this on a django rest app but for some reason the the response that arrives to the apple backend in the do_auth method is empty, any help would be appreciated

I had to do it myself, maybe it will help you.

`class AppleOAuth2(BaseOAuth2):
"""apple authentication backend"""

name = 'apple-oauth2'
RESPONSE_TYPE = 'code'
AUTHORIZATION_URL = 'https://appleid.apple.com/auth/authorize'
ACCESS_TOKEN_METHOD = 'POST'
ACCESS_TOKEN_URL = 'https://appleid.apple.com/auth/token'
SCOPE_SEPARATOR = ','
ID_KEY = 'uid'
REDIRECT_STATE = False
jwks_url = 'https://appleid.apple.com/auth/keys'

def __init__(self, *args, **kwargs):
    super().__init__(*args, **kwargs)
    if self.redirect_uri and settings.SCHEME not in self.redirect_uri:
        self.redirect_uri = self.redirect_uri.replace("http", settings.SCHEME) \
            if "http" in self.redirect_uri else self.redirect_uri

def auth_url(self):
    url = super().auth_url()
    extra_params = {
        'response_mode': 'form_post',
        'scope': 'email+name',
    }
    url += ''.join((f'&{key}={extra_params[key]}' for key in extra_params))
    return url

@handle_http_errors
def auth_complete(self, *args, **kwargs):
    """Completes login process, must return user instance"""
    request = kwargs.get('request', None)
    post = request.POST if request else None
    self.STATE_PARAMETER = post.get('state', self.STATE_PARAMETER)

    logger.info(f"Sing with Apple: auth_complete {{'request.POST': {post}}}")

    if 'user' in post:
        kwargs.update({'user_info': json.loads(post['user'])})

    try:
        self.process_error(self.data)
    except social_exceptions.AuthCanceled:
        return None

    state = post.get('state', None) if post else self.validate_state()
    data, params = None, None
    if self.ACCESS_TOKEN_METHOD == 'GET':
        params = self.auth_complete_params(state)
    else:
        data = self.auth_complete_params(state)

    response = self.request_access_token(
        self.access_token_url(),
        data=data,
        params=params,
        headers=self.auth_headers(),
        auth=self.auth_complete_credentials(),
        method=self.ACCESS_TOKEN_METHOD
    )
    logger.info(f"Sing with Apple: auth_complete {{'response': {response}}}")
    self.process_error(response)
    return self.do_auth(response['access_token'], response=response, *args, **kwargs)

@handle_http_errors
def do_auth(self, access_token, *args, **kwargs):
    """
    Finish the auth process once the access_token was retrieved
    Get the email from ID token received from apple
    """
    response_data = {}
    response = kwargs.get('response') or {}
    id_token = response.get('id_token') if 'id_token' in response else None
    user_info = kwargs.get('user_info', None)

    logger.info(f"Sing with Apple: {{'id_token': {id_token}}}")

    if id_token:
        public_key = self.get_public_key(jwks_url=self.jwks_url, id_token=id_token)
        decoded = jwt.decode(id_token, public_key, verify=True, audience=settings.CLIENT_ID)
        logger.info(f"Sing with Apple: {{'decode': {decoded}}}")

        response_data.update({'email': decoded['email']}) if 'email' in decoded else None
        response_data.update({'uid': decoded['sub']}) if 'sub' in decoded else None

        if user_info and 'name' in user_info and user_info['name']:
            response_data.update({'first_name': user_info['name']['firstName']}) \
                if 'firstName' in user_info['name'] else None
            response_data.update({'last_name': user_info['name']['lastName']}) \
                if 'lastName' in user_info['name'] 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})

    # Manual create UserSocialAuth obj if user exists
    email = response.get('email', None)
    if email:
        user = User.objects.filter(email=email).first()
        is_exist_user_auth = UserSocialAuth.objects.filter(
            uid=response.get('uid'),
            user=user,
            provider=self.name,
        ).exists()
        if user and not is_exist_user_auth:
            user_social_auth = UserSocialAuth.objects.create(
                user=user,
                provider=self.name,
                uid=response.get('uid'),
                extra_data={
                    'auth_time': int(time.time()),
                    'access_token': response.get('access_token'),
                    'token_type': response.get('token_type') or kwargs.get('token_type'),
                }
            )

            logger.info(f"Sing with Apple: Manual create obj UserSocialAuth for exist user"
                        f" {{'UserSocialAuth': {user_social_auth}}}")

    logger_data = {
        'args': args,
        'kwargs': kwargs,
    }
    logger.info(f"Sing with Apple: finality do_auth {logger_data}")
    return self.strategy.authenticate(*args, **kwargs)

def get_user_details(self, response):
    email = response.get('email', None)
    first_name = response.get('first_name', None)
    last_name = response.get('last_name', None)
    details = {
        'email': email,
        'is_client': True,
        'is_verified': True,
    }
    details.update({'first_name': first_name}) if first_name else None
    details.update({'last_name': last_name}) if last_name else None
    return details

def get_key_and_secret(self):
    headers = {
        'kid': settings.SOCIAL_AUTH_APPLE_KEY_ID
    }

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

    client_secret = jwt.encode(
        payload,
        settings.SOCIAL_AUTH_APPLE_PRIVATE_KEY,
        algorithm='ES256',
        headers=headers
    ).decode("utf-8")

    return settings.CLIENT_ID, client_secret

@staticmethod
def get_public_key(jwks_url, id_token):
    """
    Apple give public key https://appleid.apple.com/auth/keys
    Example:
        {
          "keys": [
            {
              "kty": "RSA",
              "kid": "86D88Kf",
              "use": "sig",
              "alg": "RS256",
              "n": "some_key",
              "e": "AQAB"
            },
            {
              "kty": "RSA",
              "kid": "eXaunmL",
              "use": "sig",
              "alg": "RS256",
              "n": "some_key",
              "e": "AQAB"
            },
            {
              "kty": "RSA",
              "kid": "AIDOPK1",
              "use": "sig",
              "alg": "RS256",
              "n": "some_key",
              "e": "AQAB"
            }
          ]
        }
    Use kid for give some parameters for public key.

    Now use by default "kid": "eXaunmL" ago get ['keys'][1]
    Decode https://jwt.io/ id_token and show what kid in HEADER
    :return: RSAAlgorithm obj
    """
    header = jwt.get_unverified_header(id_token)
    kid = header['kid']

    response = requests.get(jwks_url)
    if response.ok:
        json_data = response.json()
        for key_data in json_data['keys']:
            if key_data['kid'] == kid:
                key_json = json.dumps(key_data)
                return RSAAlgorithm.from_jwk(key_json)

        # Or used default
        keys_json = json.dumps(json_data['keys'][1])
        return RSAAlgorithm.from_jwk(keys_json)`

That's actually pretty helpful, thank you so much! but that means that the default backend "apple-id" didn't worke for you, right?

@ramonyaskal
Copy link

@MiltonMilton when I doing this the default backend "apple-id" did not yet exist in this lib. You can debug default backend)

@kierandesmond
Copy link

Hi all,

Has this been resolved? Been trying the default apple-id version though it doe's not seem to work. Keep getting model errors. Its working great for Facebook and google logins. If you implemented, which of the solutions above did you use?

Many Thanks

@jfbeltran97
Copy link

jfbeltran97 commented Nov 13, 2020

I'm getting this error all of a sudden (it was working fine previously):
Traceback (most recent call last):
File "/env/lib/python3.6/site-packages/social_core/backends/apple.py", line 111, in decode_id_token
audience=self.setting("CLIENT"), algorithm="RS256",)
File "/env/lib/python3.6/site-packages/jwt/api_jwt.py", line 105, in decode
self._validate_claims(payload, merged_options, **kwargs)
File "/env/lib/python3.6/site-packages/jwt/api_jwt.py", line 141, in _validate_claims
self._validate_aud(payload, audience)
File "/env/lib/python3.6/site-packages/jwt/api_jwt.py", line 190, in _validate_aud
raise InvalidAudienceError('Invalid audience')
jwt.exceptions.InvalidAudienceError: Invalid audience

During handling of the above exception, another exception occurred:
File "/env/lib/python3.6/site-packages/social_core/backends/apple.py", line 146, in do_auth
decoded_data = self.decode_id_token(jwt_string)
File "/env/lib/python3.6/site-packages/social_core/backends/apple.py", line 113, in decode_id_token
raise AuthCanceled("Token validation failed")
social_core.exceptions.AuthCanceled: Authentication process canceled

@adorum
Copy link

adorum commented Nov 16, 2020

@jfbeltran97 I am getting the same error. Have you found a solution? Thanks.

@ramonyaskal
Copy link

You have set the right audience?

@adorum
Copy link

adorum commented Nov 19, 2020

My django API server allows to login with social access token. In case of SIWA I was sending wrong token from iOS to django API server. Now I am sending the corrent JWT identity token and it works as expected. Only issue I am facing is that after I sign in iOS with apple id at first time, at receice all necessarry information about the user (email, first name, last name). But after I send the identify token to API server, apple-id backend of python-social-auth cant get the first name and last name of logged in user. I read that Apple responds with user details only at first login. Any subsequest login response does not contains user details because of security or what...So my django API server can not get the user name when user is sign up from iOS device.

@jfbeltran97
Copy link

@ramonyaskal @adorum I haven't made any changes and as I said, it was working fine previously. I thought maybe it was a problem with the apple servers.

@lhwangweb
Copy link

lhwangweb commented Feb 25, 2021

Hi @jfbeltran97 @adorum

Share my case for your reference:

Perhaps check your SOCIAL_AUTH_APPLE_ID_CLIENT

First, make sure it is the correct Service IDs.

If yes, check your social-auth-core version and your SOCIAL_AUTH_APPLE_ID_CLIENT format, string or list or else?

SOCIAL_AUTH_APPLE_ID_CLIENT is the parameter for audience of JWT decode
(Refer social-auth-core's source code site-packages/social_core/backends/apple.py def decode_id_token() if you are interested it)

In social-auth-core<=3.3.3,
It use SOCIAL_AUTH_APPLE_ID_CLIENT to do audience, and it allow you set string or list

In social-auth-core>=3.4.0,
It use SOCIAL_AUTH_APPLE_ID_AUDIENCE or SOCIAL_AUTH_APPLE_ID_CLIENT.

If SOCIAL_AUTH_APPLE_ID_AUDIENCE existed, package use it to do audience, and allow you set string or list
If SOCIAL_AUTH_APPLE_ID_AUDIENCE empty, it will use [ SOCIAL_AUTH_APPLE_ID_CLIENT , ] to do audience.

Yes, SOCIAL_AUTH_APPLE_ID_CLIENT will be put into a list. In other words, you can't set list for it. It will be converted to a nested list and of course has error 'Invalid audience'.
(Refer social-auth-core's source code site-packages/social_core/backends/apple.py def get_audience() if you are interested it)

In my case, I set SOCIAL_AUTH_APPLE_ID_CLIENT = ["com.aaa", "com.bbb", "com.ccc"] in previous version and fail after 3.4.0.

I use SOCIAL_AUTH_APPLE_ID_AUDIENCE now, and social-auth-core is 4.0.2. It's OK

FYI

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

10 participants