Apple announced a new feature, "Sign In with Apple" enabling users to sign in to apps using their Apple ID. This new feature is meant to be a secure and privacy-friendly way for users to create an account in apps. Most iOS and Mac users already have an Apple ID, and this new feature lets them use that Apple ID to sign in to other apps and websites.
Apple is taking a firm stance to protect user's privacy, rather than letting applications see the user's real email address, they will provide the app with a fake or random email address unique to each app. Don't you worry! Developers will still be able to send emails to these proxy addresses, it just means developers won't be able to use the email addresses in any other way. This feature will also allow users to disable email forwarding per application.
Apple adopted the existing standards OAuth 2.0 and OpenID Connect to use as the foundation for their new API. If you're familiar with these technologies, you can easily start Sign in with Apple right away!
In this article, we'll be using Python Social Auth as it provides with OAuth 2.0 and OpenID support. Adding a new custom backend will do a lot of help and there will be less code, which we usually prefers. We just have to override some functionalities and then it'll be good to go.
The first thing is to know what you need from the apple's account. Then you'll generate the keys needed from you apple's account.
- key_id
- team_id
- client_id
- client_secret
- redirect_uri
The first three keys are straight forward, you will get them from the apple's account. If you are only concerned with mobile clients, you can just give the redirect_uri as https://example.com/redirect.
Rather than static client secrets, Apple requires that you derive a client secret yourself from your private key every time. They use the ES256 JWT algorithm to generate that secret. There are already some libraries that do this for you. Here in this example we are using PyJwt for this.
pip install pyjwt
import jwt
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")
This is also described in Apple's documentation Creating the Client Secret.
Now that you have all the things ready, you can start with the custom backend. Python social auth implements OAuth 2.0 standards but apple has some differences in their flow. So in order to complete apple sign in you have to extend BaseOAuth2 and customise or override some functions.
get_key_and_secret. override this as you have to generate the client secret the way mentioned above get_user_details. override just to give the email or other user information back to the Python Social Auth framework do_auth. override do_auth method as you need to verify the code or access token given by mobile client from apple and get the id_token from which other details can be extracted.
As the response of the validate token call, apple return id_token which contains several things but two things are very important. Email and Sub, where sub is the unique user_id and email is the email id of the user, fake or real.
You can decode the token by using JWT like:
decoded = jwt.decode(id_token, '', verify=False)
This is described in Apple's documentation Generate and validate tokens.
We have created AppleOAuth2 class as a custom backend doing sign in with apple using Python Social Auth.
import jwt
import requests
from datetime import timedelta
from django.conf import settings
from django.utils import timezone
from social_core.backends.oauth import BaseOAuth2
from social_core.utils import handle_http_errors
class AppleOAuth2(BaseOAuth2):
"""apple authentication backend"""
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):
"""
Finish the auth process once the access_token was retrieved
Get the email from ID token received from apple
"""
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',
'redirect_uri': 'https://example-app.com/redirect'
}
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, '', verify=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_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
Important thing to know is, user email and name are returned only the first time you make the request. So test that again and again, you can remove your app from your user from the apple's account.
If you are not using python social auth, you can do the manual creation of the user after the validation and decoding of id_token you got from apple. In case the uid already exists in you data, then that's the same user, you just have to login. In our case python social auth is doing this already :)
We used environment variables of our AWS instances to save all the keys, but then I came across a scenario that AWS environment variables has a character limit up to 256. The long Apple private key cannot be fit into these environment variables. We figured out that AWS has a service AWS Secret Manager that we can use to store the long private keys.
Hi everyone
I've implemented 'Sign In with Apple' from this source taking into account the comments of NipunShaji and aj3sh. But it doesn't works because of Apple doesn't send full data (I recieve decoded = {'iss': 'https://appleid.apple.com', 'aud': '...', 'exp': 1664463442, 'iat': 1664377042, 'sub': '.....', 'at_hash': '....', 'auth_time': 1664377030, 'nonce_supported': True}. But without email I'll not be able to register m user). What I'm missing?