Requirements: Python 3, Django 2
The authentication part of the new API is built on top of the existing standards OAuth 2.0 and OpenID Connect. If you're familiar with these technologies, you can easily start implementing Sign in with Apple.
In the diagram you can see the whole authentication flow covering all steps.
Steps 1-3 begin with how to Implement Sign In with Apple on iOS.
We'll be using Python Social Auth as it provides OAuth 2.0 and OpenID support. We'll be adding a new custom backend.
Before you start diving into the code, you'll need to generate a key on Apple's Developer Portal in order for Apple to associate and verify your requests.
Once the iOS app calls the backend providing an authorizationCode
, we can build our authentication request. It consists of a few pieces:
- Key ID (The ID of the key you've generated on Apple's Developer Portal)
- Apple Developer Team ID
- Client ID (The iOS app's bundle ID, e.g.
com.yourcompany.yourapp
) - Client Secret
Apple requires you to derive a client secret yourself from your private key every time. They use the ES256 JWT algorithm to generate that secret. This is also described in Apple's documentation Creating the Client Secret.
We have used PyJWT to generate the client secret.
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")
Now that you can make authenticated requests, you can start with the custom backend. Python Social Auth implements the OAuth 2.0 standard but Apple has some differences in their flow. In order to complete Sign In with Apple 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 aboveget_user_details
override just to give the email address or other user information back to the Python Social Auth frameworkdo_auth
override this 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.
With the response of the validate token call Apple returns an id_token
that contains bunch of information. Two things are very important: sub
(subject) is the unique user id and email
is the email address of the user, fake or real.
You can decode the token by using JWT:
decoded = jwt.decode(id_token, '', verify=False)
You can also refer to Apple's documentation Generate and Validate Tokens that explains this.
We have created the 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',
}
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
A very important thing to know is, that the email address and the name are returned only the first time you make the request. So, if you need to test this over and over again, you need to remove your app from the authorized apps of your AppleID account everytime.
If you are not using Python Social Auth, you can manually create the user after the validation of the user and decoding of the id_token
you got from Apple. In case the uid already exists, then that's the same user, you just have to login. In our case Python Social Auth is doing this already :)
If you haven't started with the iOS app yet, you can start to Implement Sign In with Apple on iOS.