DEV Community

Cover image for Google/Apple token verification with Django
Damian D. Morejon
Damian D. Morejon

Posted on • Edited on

7

Google/Apple token verification with Django

Overview

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

We are going to use google-api-python-client to handle Google token verification and requests to handle token verification with Apple-ID servers.

Step 1: Setup

Install all 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

If you want to separate multiple python versions you can use a virtual environment by running python3 -m venv , this will create venv folder, activate it with source /bin/activate. Check the docs for more details

Next, we will set global variables in our settings.py with the 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'
# .....
view raw settings.py hosted with ❤ by GitHub

Make sure to check how to enable Apple Sign-In and Google Sign-in with Firebase if it’s not done yet.

Step 2: Create views

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')
view raw serializers.py hosted with ❤ by GitHub

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)
view raw views.py hosted with ❤ by GitHub

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)
view raw views.py hosted with ❤ by GitHub

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.

Configure some routes:

urlpatterns = [
# ....
path('auth/g/verify/', GoogleView.as_view()),
path('auth/a/verify/', AppleView.as_view())
# ...
]
view raw urls.py hosted with ❤ by GitHub

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

Image of Datadog

Create and maintain end-to-end frontend tests

Learn best practices on creating frontend tests, testing on-premise apps, integrating tests into your CI/CD pipeline, and using Datadog’s testing tunnel.

Download The Guide

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay