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.
📌 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...