Skip to content

Instantly share code, notes, and snippets.

@aamishbaloch
Created October 8, 2019 14:58
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save aamishbaloch/2f0e5d94055e1c29c0585d2f79a8634e to your computer and use it in GitHub Desktop.
Save aamishbaloch/2f0e5d94055e1c29c0585d2f79a8634e to your computer and use it in GitHub Desktop.
Sign In with Apple using Django (Python) Backend

Implementing Sign In with Apple in your Django (Python) backend

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.

How it works

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!

Python Social Auth

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.

Generating the keys

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.

Generating the client secret

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.

Implement a custom backend

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.

What is so important about the ID Token?

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.

Not using Python Social Auth?

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 :)

You can also Learn

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.

@rajaravi1
Copy link

After successful validation of Authorization_code with apple, Django create user account if it doesn't exist. In this implementation, Does your Django app creates the access token and refresh token to share with Clients (or) will the apple access token / refresh token would used by the clients and Django?

@NipunShaji
Copy link

While specifying token issue time(iat) and token expiry time(exp) please don't use timezone.now(). Apple API expects time as seconds, in terms of the number of seconds since Epoch, in UTC.

So to correct that issue use time package instead.
To get iat, use int(time.time())
To get exp, use int(time.time()) + 'the time you need in seconds' . if you need expiry as 10 minutes add 600 to it.

NB: please dont forget to import time module.

@leminhbang
Copy link

Do you give me some way to generate key_id, client_id and client_secret use for auth?

@PABourdais
Copy link

@aj3sh
Copy link

aj3sh commented Oct 8, 2021

Cool it works.

For PyJWT >= 2.2.0 use the following code for encoding and decoding.

client_secret = jwt.encode(
    payload, 
    settings.SOCIAL_AUTH_APPLE_PRIVATE_KEY, 
    algorithm='ES256', 
    headers=headers
)
decoded = jwt.decode(id_token, '', options={"verify_signature": False})

@Johnywhisky
Copy link

Hi there, although I was following your guide I've faced on error this `TypeError: init() missing 1 required positional argument: 'strategy'``
someone let me know what should I do??

@loveJesus
Copy link

While specifying token issue time(iat) and token expiry time(exp) please don't use timezone.now(). Apple API expects time as seconds, in terms of the number of seconds since Epoch, in UTC.

So to correct that issue use time package instead. To get iat, use int(time.time()) To get exp, use int(time.time()) + 'the time you need in seconds' . if you need expiry as 10 minutes add 600 to it.

NB: please dont forget to import time module.

Hallelujah, worked for me with this change, thanks

@EKonetskaia
Copy link

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?

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