Open Allan-Nava opened 5 years ago
+1 to the request
+1
Please don't spam subscribers with +1 replies and use emoji response to the OP, thanks.
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
.
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 ofsocial-auth-core
together with the existing release ofsocial-auth-app-django
.
perfect!
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
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)`
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?
@MiltonMilton when I doing this the default backend "apple-id" did not yet exist in this lib. You can debug default backend)
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
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
@jfbeltran97 I am getting the same error. Have you found a solution? Thanks.
You have set the right audience?
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.
@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.
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
When is possible to implement the social login with apple sign in?