Over the past few weeks, I have been working on a project where one of the main requirements is to enable token verification and authenticate or register & authenticate a user with Django.

What we're going to do

📌 use google-api-python-client to handle Google token verification.

📌 use requests to handle token verification with Apple-ID servers.

Create a requirements.txt file with the packages we are going to use:

django-filter
Django==3.1.7
gunicorn==20.0.4
google-api-python-client==2.0.2
requests==2.25.1
cryptography==3.4.6
django-detect==1.0.20

Next, we will set global variables in our settings.py file with our keys and secrets, necessary to verify our tokens.

# ......

FIREBASE_ANDROID_APP_ID = os.getenv('FIREBASE_ANDROID_APP_ID')
FIREBASE_IOS_APP_ID = os.getenv('FIREBASE_IOS_APP_ID')

AUTH_APPLE_KEY_ID = os.getenv('AUTH_APPLE_KEY_ID')
AUTH_APPLE_TEAM_ID = os.getenv('AUTH_APPLE_TEAM_ID')
AUTH_APPLE_PRIVATE_KEY = os.getenv('AUTH_APPLE_PRIVATE_KEY')
AUTH_APPLE_CLIENT_ID = os.getenv('AUTH_APPLE_CLIENT_ID')
AUTH_APPLE_APP_ID = os.getenv('AUTH_APPLE_APP_ID')
ACCESS_TOKEN_URL = 'https://appleid.apple.com/auth/token'

# .....

Here we will create two views, one for manage verification for Google tokens and another one for Apple tokens.

Let's create first a simple serializer for our User model:

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('username', 'email', 'id')

Then the Google view:

class GoogleView(APIView):

    def post(self, request):
        # get id_token from post request
        token = {'id_token': request.data.get('id_token')}
        try:
            # verify google oauth2 token
            idinfo = id_token.verify_token(token['id_token'], requests.Request())
            
            # check audience
            if idinfo['aud'] not in [FIREBASE_ANDROID_APP_ID, FIREBASE_IOS_APP_ID]:
                raise ValueError('Could not verify audience.')

            # check issuer
            if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
                raise ValueError('Wrong issuer.')

            # search for an existent user, if not, register
            if User.objects.filter(email=idinfo['email']).exists():
                user = User.objects.get(email=idinfo['email'])
            else:
                password = User.objects.make_random_password()
                user = User.objects.create_user(email=idinfo['email'], username=idinfo['email'],
                                                first_name=idinfo['given_name'],
                                                last_name=idinfo['family_name'],
                                                password=password)
                name = idinfo['email'].replace('@', '_').replace('.', '_') + '.png'
                
                # get user profile image and save it
                response = requester.get(idinfo['picture'], stream=True)
                if response.status_code != requester.codes.ok:
                    lf = tempfile.NamedTemporaryFile()
                    for block in response.iter_content(1024 * 8):
                        if not block:
                            break
                        lf.write(block)
                    user.image.save(name, files.File(lf))

            # handle JWT token generation for user 
            # authentication in the server
            jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
            jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
            payload = jwt_payload_handler(user)
            token = jwt_encode_handler(payload)
            
            # serialize user data and send it to your frontend 
            # or mobile application
            serializer = UserRegisterSerializer(user)
            return Response({'token': token, 'user': serializer.data})
        except ValueError as err:
            # Handle value exceptions
            content = {'message': err.__str__()}
            return Response(content, 500)

Here we are using the token that comes from a request to be verified using google-api-python-client, then we use the email, given name, and family name to register a user in our server if is not registered yet, save the profile image, and create a jwt token for user authentication in our server.

The Apple view:

import jwt
import requests as requester
from rest_framework_jwt.settings import api_settings


class AppleView(APIView):

    # create the client secret
    def get_key_and_secret(self):
        # jwt header
        headers = {
            'kid': AUTH_APPLE_KEY_ID,
            'alg': 'ES256'
        }
        # jwt payload
        payload = {
            'iss': AUTH_APPLE_TEAM_ID,
            'iat': timezone.now(),
            'exp': timezone.now() + timedelta(days=180),
            'aud': 'https://appleid.apple.com',
            'sub': AUTH_APPLE_CLIENT_ID,
        }
        # sign the jwt to get the client secret
        client_secret = jwt.api_jwt.encode(
            payload,
            AUTH_APPLE_PRIVATE_KEY,
            algorithm="ES256",
            headers=headers
        )
        return AUTH_APPLE_CLIENT_ID, client_secret

    def post(self, request):
        # get the jwt payload and encode to be used to create a
        # user authentication token in our server
        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
        
        # get either the access token or the refresh token to verify a user
        access_token = request.data.get('access_token')
        refresh_token = request.data.get('refresh_token')
        
        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,
        }
        if refresh_token is None:
            data['code'] = access_token
            data['grant_type'] = 'authorization_code'
        else:
            data['refresh_token'] = refresh_token
            data['grant_type'] = 'refresh_token'
        #
        res = requester.post(ACCESS_TOKEN_URL, data=data, headers=headers)
        response_dict = res.json()
        if 'error' not in response_dict:
            id_token = response_dict.get('id_token', None)
            refresh_tk = response_dict.get('refresh_token', None)
            decoded = jwt.decode(id_token, '', verify=False) if id_token else None
            if id_token and decoded:
                try:
                    if User.objects.filter(email=decoded['email']).exists():
                        user = User.objects.get(email=decoded['email'])
                    else:
                        name = decoded['email'].split('@')[0]
                        user = User.objects.create_user(email=decoded['email'], username=decoded['email'],
                                                        first_name=name,
                                                        last_name=name,
                                                        password=User.objects.make_random_password())

                    payload = jwt_payload_handler(user)
                    token = jwt_encode_handler(payload)
                    serializer = UserRegisterSerializer(user)
                    data = {'token': token, 'user': serializer.data}
                    if refresh_tk is not None:
                        data['refresh_token'] = refresh_tk
                    return Response(data)
                except AssertionError as err:
                    # Invalid token
                    content = {'message': err.__str__()}
                    return Response(content, 500)
        else:
            content = {'message': response_dict.__str__()}
            return Response(content, 500)

In our Apple view, we are expecting either an access_token or a refresh_token. First, we need to generate a client secret using the algorithm ES256, then use the access token and the client secret to request an authentication token to Apple ID servers, and decode the response to get user information. You have to handle the user's full name because this is only returned the first time a user signs in. Second, if we receive a refresh token, we use it to verify that the user is still signed in against the Apple ID servers. If we got no errors, proceed to authenticate or register/authenticate a user.

In our urls.py file, configure some routes:

urlpatterns = [
    # ....
    path('auth/google/verify/', GoogleView.as_view()),
    path('auth/apple/verify/', AppleView.as_view())
    # ...
]

And that's it!! We rock! Now you have all set to verify tokens from both Google and Apple and manage your users...