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' | |
# ..... |
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') |
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.
Configure some routes:
urlpatterns = [ | |
# .... | |
path('auth/g/verify/', GoogleView.as_view()), | |
path('auth/a/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…
Top comments (0)