diff --git a/core/api/auth.py b/core/api/auth.py index bb1eb83b0f18491249ef86d2299ba2c9ba45cc1c..45b85359dbd93379429d9711e66724f95e537857 100644 --- a/core/api/auth.py +++ b/core/api/auth.py @@ -1,20 +1,20 @@ +import json +import jwt +from jwt.algorithms import RSAAlgorithm +import requests + +from django.contrib.auth.models import User from django.shortcuts import render from django.urls import reverse -from rest_framework import generics +from rest_framework import generics, status, serializers from rest_framework.response import Response -from dj_rest_auth.registration.views import SocialConnectView, SocialLoginView -from dj_rest_auth.social_serializers import TwitterLoginSerializer from knox.models import AuthToken -from allauth.socialaccount.providers.apple.client import AppleOAuth2Client -from allauth.socialaccount.providers.apple.views import AppleOAuth2Adapter -from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter -from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter -from allauth.socialaccount.providers.oauth2.client import OAuth2Client +from core.models.social_account import SocialAccount from core.serializers.login import LoginSerializer -from core.serializers.appleSocialLoginSerializer import AppleSocialLoginSerializer from core.serializers.register import RegisterSerializer from core.serializers.user import UserSerializer +from core.serializers.socialAuthSerializer import AppleUserInputSerializer, FacebookUserInputSerializer, GoogleUserInputSerializer class RegisterAPI(generics.GenericAPIView): @@ -47,27 +47,123 @@ class LoginAPI(generics.GenericAPIView): }) -class AppleLogin(SocialLoginView): - adapter_class = AppleOAuth2Adapter - client_class = AppleOAuth2Client +class AppleLogin(generics.GenericAPIView): + serializer_class = AppleUserInputSerializer + APPLE_PUBLIC_KEY_URL = "https://appleid.apple.com/auth/keys" + APPLE_APP_ID = "com.hamsterwhat.ios" + + def _decode_apple_user_token(self, apple_user_token): + key_payload = requests.get(self.APPLE_PUBLIC_KEY_URL).json() + + for public_key in key_payload["keys"]: + public_key = RSAAlgorithm.from_jwk(json.dumps(public_key)) + try: + token = jwt.decode(apple_user_token, public_key, audience=[self.APPLE_APP_ID, 'host.exp.Exponent'], algorithms=['RS256']) + except jwt.exceptions.ExpiredSignatureError as e: + serializers.ValidationError({"id_token": "That token has expired."}) + except jwt.exceptions.InvalidAudienceError as e: + serializers.ValidationError({"id_token": "That token's audience did not match."}) + except Exception as e: + continue + + if token is None: + serializers.ValidationError({"id_token": "That token is invalid."}) + return token - @property - def callback_url(self): - return self.request.build_absolute_uri(reverse('apple_callback')) + def post(self, request): + print(request.data) + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + id_token = serializer.validated_data.get('id_token') + data_from_id_token = self._decode_apple_user_token(id_token) + print(data_from_id_token) + + identity = 'apple_' + data_from_id_token.get('sub') + if SocialAccount.objects.filter(identity=identity).exists(): + user = SocialAccount.objects.filter(identity=identity).first().user + else: + user, created = User.objects.get_or_create( + username=identity, + password=User.objects.make_random_password(), + email=data_from_id_token.get('email', None), + ) + social_account = SocialAccount(identity=identity, user=user) + social_account.save() + token = AuthToken.objects.create(user) + return Response({ + "user": UserSerializer(user, context=self.get_serializer_context()).data, + "token": token[1], + }) -class GoogleLogin(SocialLoginView): # if you want to use Authorization Code Grant, use this - adapter_class = GoogleOAuth2Adapter - client_class = OAuth2Client +class GoogleLogin(generics.GenericAPIView): + serializer_class = GoogleUserInputSerializer + GOOGLE_API = "https://www.googleapis.com/userinfo/v2/me" - @property - def callback_url(self): - return self.request.build_absolute_uri(reverse('google_callback')) + def post(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + access_token = serializer.validated_data.get('access_token') + req = requests.get(self.GOOGLE_API, params={'access_token': access_token}) + data_from_api = req.json() + + identity = 'google_' + data_from_api.get('id') + if SocialAccount.objects.filter(identity=identity).exists(): + user = SocialAccount.objects.filter(identity=identity).first().user + else: + user, created = User.objects.get_or_create( + username=identity, + password=User.objects.make_random_password(), + email=data_from_api.get('email', None), + ) + if created: + user.first_name = data_from_api.get('given_name', user.first_name) + user.last_name = data_from_api.get('family_name', user.last_name) + user.save() + user.profile.photo = data_from_api.get('picture', user.profile.photo) + user.profile.save() + social_account = SocialAccount(identity=identity, user=user) + social_account.save() + token = AuthToken.objects.create(user) + return Response({ + "user": UserSerializer(user, context=self.get_serializer_context()).data, + "token": token[1], + }) -class TwitterLogin(SocialLoginView): - serializer_class = TwitterLoginSerializer - adapter_class = TwitterOAuthAdapter +class FacebookLogin(generics.GenericAPIView): + serializer_class = FacebookUserInputSerializer + FACEBOOK_API = "https://graph.facebook.com/me" + + def post(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + access_token = serializer.validated_data.get('access_token') + req = requests.get(self.FACEBOOK_API, params={'fields': 'id,name,email,first_name,last_name,picture', 'access_token': access_token}) + data_from_api = req.json() + + identity = 'fb_' + data_from_api.get('id') + if SocialAccount.objects.filter(identity=identity).exists(): + user = SocialAccount.objects.filter(identity=identity).first().user + else: + user, created = User.objects.get_or_create( + username=identity, + password=User.objects.make_random_password(), + email=data_from_api.get('email', None), + ) + if created: + user.first_name = data_from_api.get('first_name', user.first_name) + user.last_name = data_from_api.get('last_name', user.last_name) + user.save() + user.profile.photo = data_from_api.get('picture', {}).get('data', {}).get('url', user.profile.photo) + user.profile.save() + social_account = SocialAccount(identity=identity, user=user) + social_account.save() + token = AuthToken.objects.create(user) + return Response({ + "user": UserSerializer(user, context=self.get_serializer_context()).data, + "token": token[1], + }) def verify_user_and_activate(request, token): diff --git a/core/models/social_account.py b/core/models/social_account.py new file mode 100644 index 0000000000000000000000000000000000000000..7cc326de88fa9be9b4052cc2d2589f5855aa237f --- /dev/null +++ b/core/models/social_account.py @@ -0,0 +1,12 @@ +from django.contrib.auth.models import User +from django.db import models + +from .utils import UUIDModel + + +class SocialAccount(UUIDModel): + identity = models.CharField(max_length=100, unique=True) + user = models.ForeignKey(User, on_delete=models.CASCADE) + + def __str__(self) -> str: + return self.identity diff --git a/core/serializers/appleSocialLoginSerializer.py b/core/serializers/appleSocialLoginSerializer.py deleted file mode 100644 index 664b98304d1d409433f80d47e98ed80802d44e38..0000000000000000000000000000000000000000 --- a/core/serializers/appleSocialLoginSerializer.py +++ /dev/null @@ -1,98 +0,0 @@ -from dj_rest_auth.registration.serializers import SocialLoginSerializer -from django.contrib.auth import get_user_model -from django.utils.translation import gettext_lazy as _ -from rest_framework import serializers -from requests.exceptions import HTTPError - -try: - from allauth.account import app_settings as allauth_settings - from allauth.socialaccount.helpers import complete_social_login -except ImportError: - raise ImportError('allauth needs to be added to INSTALLED_APPS.') - - -class AppleSocialLoginSerializer(SocialLoginSerializer): - def validate(self, attrs): - view = self.context.get('view') - request = self._get_request() - - if not view: - raise serializers.ValidationError( - _('View is not defined, pass it as a context variable') - ) - - adapter_class = getattr(view, 'adapter_class', None) - if not adapter_class: - raise serializers.ValidationError(_('Define adapter_class in view')) - - adapter = adapter_class(request) - app = adapter.get_provider().get_app(request) - - # More info on code vs access_token - # http://stackoverflow.com/questions/8666316/facebook-oauth-2-0-code-and-token - - # Case 1: We received the access_token - if attrs.get('access_token'): - access_token = attrs.get('access_token') - token = {'access_token': access_token} - - # Case 2: We received the authorization code - elif attrs.get('code'): - self.callback_url = getattr(view, 'callback_url', None) - self.client_class = getattr(view, 'client_class', None) - - if not self.callback_url: - raise serializers.ValidationError( - _('Define callback_url in view') - ) - if not self.client_class: - raise serializers.ValidationError( - _('Define client_class in view') - ) - - code = attrs.get('code') - - provider = adapter.get_provider() - scope = provider.get_scope(request) - client = self.client_class( - request, - app.client_id, - app.secret, - adapter.access_token_method, - adapter.access_token_url, - self.callback_url, - scope, - key=app.key, - cert=app.cert, - ) - token = client.get_access_token(code) - access_token = token['access_token'] - - else: - raise serializers.ValidationError( - _('Incorrect input. access_token or code is required.')) - - social_token = adapter.parse_token(token) # The important change is here. - social_token.app = app - - try: - login = self.get_social_login(adapter, app, social_token, access_token) - complete_social_login(request, login) - except HTTPError: - raise serializers.ValidationError(_('Incorrect value')) - - if not login.is_existing: - # We have an account already signed up in a different flow - # with the same email address: raise an exception. - # This needs to be handled in the frontend. We can not just - # link up the accounts due to security constraints - if allauth_settings.UNIQUE_EMAIL: - # Do we have an account already with this email address? - if get_user_model().objects.filter(email=login.user.email).exists(): - raise serializers.ValidationError(_('E-mail already registered using different signup method.')) - - login.lookup() - login.save(request, connect=True) - - attrs['user'] = login.account.user - return attrs diff --git a/core/serializers/socialAuthSerializer.py b/core/serializers/socialAuthSerializer.py new file mode 100644 index 0000000000000000000000000000000000000000..9d52196d5f3e47a033769c77d4ba27369cf82238 --- /dev/null +++ b/core/serializers/socialAuthSerializer.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + + +class AppleUserInputSerializer(serializers.Serializer): + id_token = serializers.CharField(required=True, allow_blank=False) + + +class GoogleUserInputSerializer(serializers.Serializer): + access_token = serializers.CharField(required=True, allow_blank=False) + + +class FacebookUserInputSerializer(serializers.Serializer): + access_token = serializers.CharField(required=True, allow_blank=False) diff --git a/core/urls.py b/core/urls.py index a60ca0939ddd16ac2fe9d5ab1e24ed0384feaa78..f49ef0e1965930f18c4ad6ae2bcc45defa8b506d 100644 --- a/core/urls.py +++ b/core/urls.py @@ -2,7 +2,7 @@ from django.urls import path, include from knox import views as knox_views from rest_framework import routers -from core.api.auth import RegisterAPI, LoginAPI, AppleLogin, GoogleLogin, TwitterLogin, verify_user_and_activate +from core.api.auth import RegisterAPI, LoginAPI, AppleLogin, GoogleLogin, FacebookLogin, verify_user_and_activate from core.api.password import ChangePasswordView from core.api.profile import ProfileViewSet @@ -18,7 +18,7 @@ urlpatterns += [ path('api/auth/logout', knox_views.LogoutView.as_view(), name='logout'), path('api/auth/apple', AppleLogin.as_view(), name='apple_login'), path('api/auth/google', GoogleLogin.as_view(), name='google_login'), - path('api/auth/twitter', TwitterLogin.as_view(), name='twitter_login'), + path('api/auth/facebook', FacebookLogin.as_view(), name='facebook_login'), # passwd path('api/change-password', ChangePasswordView.as_view(), name='change-password'), path('api/password_reset/', include('django_rest_passwordreset.urls', namespace='password_reset')), diff --git a/ece651_backend/settings.py b/ece651_backend/settings.py index 48b555b58449b89218916f0c5c3fb476e7e9cde6..6a0882d1caf9cc48639fce43ca0a7b177b4a1035 100644 --- a/ece651_backend/settings.py +++ b/ece651_backend/settings.py @@ -36,41 +36,27 @@ INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', - 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', 'django_extensions', 'rest_framework', 'django_rest_passwordreset', 'knox', - 'dj_rest_auth', - 'dj_rest_auth.registration', - 'allauth', - 'allauth.account', - 'allauth.socialaccount', - 'allauth.socialaccount.providers.apple', - 'allauth.socialaccount.providers.google', - 'allauth.socialaccount.providers.twitter', 'core', ] -SITE_ID = 1 - REST_FRAMEWORK = { 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', 'DEFAULT_AUTHENTICATION_CLASSES': [ 'knox.auth.TokenAuthentication', - 'dj_rest_auth.jwt_auth.JWTCookieAuthentication', ], } REST_KNOX = { - 'TOKEN_TTL': timedelta(days=3), + 'TOKEN_TTL': timedelta(days=7), 'AUTO_REFRESH': True, } -REST_USE_JWT = True - MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -162,38 +148,3 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # Media Settings MEDIA_URL = 'media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') - -SOCIALACCOUNT_PROVIDERS = { - "apple": { - "APP": { - # Your service identifier. - "client_id": "your.service.id", - - # The Key ID (visible in the "View Key Details" page). - "secret": "KEYID", - - # Member ID/App ID Prefix -- you can find it below your name - # at the top right corner of the page, or it’s your App ID - # Prefix in your App ID. - "key": "MEMAPPIDPREFIX", - - # The certificate you downloaded when generating the key. - "certificate_key": """-----BEGIN PRIVATE KEY----- -s3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr -3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3cr3ts3 -c3ts3cr3t ------END PRIVATE KEY----- -""" - } - }, - 'google': { - 'SCOPE': [ - 'profile', - 'email', - ], - 'AUTH_PARAMS': { - 'access_type': 'online', - }, - 'OAUTH_PKCE_ENABLED': True, - } -}