Last active
February 10, 2021 11:54
-
-
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]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| -----BEGIN PRIVATE KEY----- | |
| MIGTAgohoMGByqGS.... | |
| .....etc. | |
| -----END PRIVATE KEY----- |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 = '...' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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