Skip to content

Instantly share code, notes, and snippets.

@nadyshalaby
Last active February 10, 2021 11:54
Show Gist options
  • Save nadyshalaby/f7d3c216e30ec2f0a2feb76fa50aa70a to your computer and use it in GitHub Desktop.
Save nadyshalaby/f7d3c216e30ec2f0a2feb76fa50aa70a to your computer and use it in GitHub Desktop.
Django Social Auth AppleID OAuth2 Backend for authenticating users across Flutter APP using package (sign_in_with_apple)[https://pub.dev/packages/sign_in_with_apple]
import logging
import os
import time
from datetime import datetime, timedelta
from urllib import parse
import json
import jwt
from jwt.algorithms import RSAAlgorithm
import requests
from django.conf import settings
from django.contrib.auth import login
from django.http import HttpResponseRedirect
from django.http.response import HttpResponse, JsonResponse
from rest_framework import permissions
from rest_framework.decorators import (api_view, permission_classes,
renderer_classes)
from rest_framework.renderers import JSONRenderer
from social_core.backends.oauth import BaseOAuth2
from social_core.exceptions import AuthFailed
from social_core.utils import handle_http_errors
from social_django.utils import psa
logger = logging.getLogger('apple-auth')
class AppleOAuth2(BaseOAuth2):
"""apple authentication backend"""
name = 'apple'
ACCESS_TOKEN_URL = 'https://appleid.apple.com/auth/token'
SCOPE_SEPARATOR = ','
ID_KEY = 'uid'
JWK_URL = 'https://appleid.apple.com/auth/keys'
@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,
'redirect_uri': settings.SOCIAL_AUTH_APPLE_REDIRECT_URI,
'code': access_token,
'grant_type': 'authorization_code',
}
res = requests.post(AppleOAuth2.ACCESS_TOKEN_URL, data=data, headers=headers)
response_dict = res.json()
if 'error' in response_dict:
logger.error('[DO-AUTH] ' + response_dict['error'])
raise AuthFailed(self, response_dict['error'])
id_token = response_dict.get('id_token', None)
if id_token:
try:
kid = jwt.get_unverified_header(id_token).get('kid')
public_key = RSAAlgorithm.from_jwk(self.get_apple_jwk(kid))
decoded = jwt.decode(
id_token,
key=public_key,
audience=self.get_audience(),
algorithms=['RS256'],
)
except:
raise AuthFailed(self, 'Token validation failed')
response_data.update({'email': decoded['email']}) if 'email' in decoded else None
response_data.update({'uid': decoded['sub']}) if 'sub' in decoded else None
else:
logger.error('[DO-AUTH] Missing id_token parameter')
raise AuthFailed(self, 'Missing id_token parameter')
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,
}
logger.error('[GET-USER-DETAILS] Headers: ' + str(details))
return details
def get_key_and_secret(self):
headers = {
'kid': settings.SOCIAL_AUTH_APPLE_KEY_ID
}
logger.error('[GET-KEY-AND-SECRET] Headers: ' + str(headers))
now = int(time.time())
payload = {
'iss': settings.SOCIAL_AUTH_APPLE_TEAM_ID,
'iat': now,
'exp': now + 3600,
'aud': 'https://appleid.apple.com',
'sub': settings.CLIENT_ID,
}
logger.error('[GET-KEY-AND-SECRET] Payload: ' + str(payload))
with open(os.path.join(settings.BASE_DIR, 'Authentication', 'AuthKey.p8'), "r") as f:
private_key = f.read()
client_secret = jwt.encode(
payload,
key=private_key,
algorithm='ES256',
headers=headers
)
logger.error('[GET-KEY-AND-SECRET] Client Secret: ' + str(client_secret))
return settings.CLIENT_ID, client_secret
def get_apple_jwk(self, kid=None):
"""
Return requested Apple public key or all available.
"""
keys = self.get_json(url=self.JWK_URL).get('keys')
if not isinstance(keys, list) or not keys:
raise AuthFailed(self, 'Invalid jwk response')
if kid:
return json.dumps([key for key in keys if key['kid'] == kid][0])
else:
return (json.dumps(key) for key in keys)
def get_audience(self):
client_id = settings.CLIENT_ID
return self.setting('AUDIENCE', default=[client_id])
@api_view(['POST', 'GET'])
@permission_classes((permissions.AllowAny,))
def sign_in_with_apple_callback(request):
try:
params = {**request.query_params.dict(), **request.data.dict()}
logger.error('[SIGN-IN-WITH-APPLE-CALLBACK] Intent Params: ' + str(params))
HttpResponseRedirect.allowed_schemes.append('intent')
url = 'intent://callback?%s#Intent;package=%s;scheme=signinwithapple;end' % (
parse.urlencode(params), settings.APP_ID)
logger.error('[SIGN-IN-WITH-APPLE-CALLBACK] Intent URL: ' + url)
return HttpResponseRedirect(url)
# return HttpResponse('koncoasn')
except Exception as e:
logger.error('[SIGN-IN-WITH-APPLE-CALLBACK] ' + str(e))
@psa('social:complete')
@renderer_classes((JSONRenderer))
def sign_in_with_apple(request, backend):
# This view expects an access_token GET parameter, if it's needed,
# request.backend and request.strategy will be loaded with the current
# backend and strategy.
token = request.GET.get('code')
logger.error('[SIGN-IN-WITH-APPLE] Token: ' + str(token))
try:
user = request.backend.do_auth(token)
logger.error('[SIGN-IN-WITH-APPLE] User: ' + str(user))
if user:
login(request, user)
return JsonResponse({'user': str(user)})
else:
return JsonResponse({'error': 'Failed'})
except Exception as e:
logger.error('[SIGN-IN-WITH-APPLE] ' + str(e))
return JsonResponse(str(e))
-----BEGIN PRIVATE KEY-----
MIGTAgohoMGByqGS....
.....etc.
-----END PRIVATE KEY-----
# Authentication backends
AUTHENTICATION_BACKENDS = (
# ... ,
# AppleAuth2 Backend.
'Authentication.apple.AppleOAuth2',
# ...
)
APP_ID = 'bundle-id'
CLIENT_ID = 'bundle-id'
SOCIAL_AUTH_APPLE_REDIRECT_URI = 'https://api.example.com/callbacks/sign_in_with_apple/'
SOCIAL_AUTH_APPLE_KEY_ID = '...'
SOCIAL_AUTH_APPLE_TEAM_ID = '...'
url(r'^sign_in_with/(?P<backend>[^/]+)/$', sign_in_with_apple),
url(r'^callbacks/sign_in_with_apple', sign_in_with_apple_callback, name='apple-callback'),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment