From d003e759febb26e8364aa99fa7dde2dda7b328de Mon Sep 17 00:00:00 2001 From: Chris Li <c58li@uwaterloo.ca> Date: Tue, 7 Feb 2023 13:22:00 -0500 Subject: [PATCH] save intermeidate things. --- core/api/auth.py | 33 ++++++- .../serializers/appleSocialLoginSerializer.py | 98 +++++++++++++++++++ core/urls.py | 5 +- ece651_backend/settings.py | 51 +++++++++- ece651_backend/urls.py | 2 + env.yml | 12 +++ environment.yml | 3 + 7 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 core/serializers/appleSocialLoginSerializer.py create mode 100644 env.yml diff --git a/core/api/auth.py b/core/api/auth.py index 56120f1..bb1eb83 100644 --- a/core/api/auth.py +++ b/core/api/auth.py @@ -1,10 +1,18 @@ from django.shortcuts import render +from django.urls import reverse from rest_framework import generics 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 django.contrib.auth.backends import AllowAllUsersModelBackend +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.serializers.login import LoginSerializer +from core.serializers.appleSocialLoginSerializer import AppleSocialLoginSerializer from core.serializers.register import RegisterSerializer from core.serializers.user import UserSerializer @@ -39,6 +47,29 @@ class LoginAPI(generics.GenericAPIView): }) +class AppleLogin(SocialLoginView): + adapter_class = AppleOAuth2Adapter + client_class = AppleOAuth2Client + + @property + def callback_url(self): + return self.request.build_absolute_uri(reverse('apple_callback')) + + +class GoogleLogin(SocialLoginView): # if you want to use Authorization Code Grant, use this + adapter_class = GoogleOAuth2Adapter + client_class = OAuth2Client + + @property + def callback_url(self): + return self.request.build_absolute_uri(reverse('google_callback')) + + +class TwitterLogin(SocialLoginView): + serializer_class = TwitterLoginSerializer + adapter_class = TwitterOAuthAdapter + + def verify_user_and_activate(request, token): try: auth = AuthToken.objects.filter(digest=token).first() diff --git a/core/serializers/appleSocialLoginSerializer.py b/core/serializers/appleSocialLoginSerializer.py new file mode 100644 index 0000000..664b983 --- /dev/null +++ b/core/serializers/appleSocialLoginSerializer.py @@ -0,0 +1,98 @@ +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/urls.py b/core/urls.py index 6258c45..a60ca09 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, verify_user_and_activate +from core.api.auth import RegisterAPI, LoginAPI, AppleLogin, GoogleLogin, TwitterLogin, verify_user_and_activate from core.api.password import ChangePasswordView from core.api.profile import ProfileViewSet @@ -16,6 +16,9 @@ urlpatterns += [ path('api/auth/activate/<token>', verify_user_and_activate, name='activate'), path('api/auth/login', LoginAPI.as_view(), name='login'), 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'), # 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 a8ddd3e..48b555b 100644 --- a/ece651_backend/settings.py +++ b/ece651_backend/settings.py @@ -36,19 +36,31 @@ 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', ], } @@ -57,6 +69,8 @@ REST_KNOX = { 'AUTO_REFRESH': True, } +REST_USE_JWT = True + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -129,7 +143,7 @@ TIME_ZONE = 'America/Toronto' USE_I18N = True -USE_TZ = True +USE_TZ = False # Static files (CSS, JavaScript, Images) @@ -148,3 +162,38 @@ 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, + } +} diff --git a/ece651_backend/urls.py b/ece651_backend/urls.py index bbc9de7..bae7a3a 100644 --- a/ece651_backend/urls.py +++ b/ece651_backend/urls.py @@ -11,6 +11,8 @@ urlpatterns = router.urls urlpatterns += [ path('', include('core.urls')), + # path('dj-rest-auth/', include('dj_rest_auth.urls')), + # path('dj-rest-auth/registration/', include('dj_rest_auth.registration.urls')), path('admin/', admin.site.urls), path('docs/', include_docs_urls(title='ECE651 Backend API Document', description='This API document includes all ednpoints that has been implemented.')), ] diff --git a/env.yml b/env.yml new file mode 100644 index 0000000..0250500 --- /dev/null +++ b/env.yml @@ -0,0 +1,12 @@ +name: django +channels: + - defaults +dependencies: + - python=3.9 + - django + - pyjwt + - cryptography + - djangorestframework + - django-extensions + - pillow +prefix: /Users/lichenyang/opt/anaconda3/envs/django diff --git a/environment.yml b/environment.yml index fbcd5ab..1974a01 100644 --- a/environment.yml +++ b/environment.yml @@ -38,3 +38,6 @@ dependencies: - pygments - django-filter - django-guardian + - dj-rest-auth + - django-allauth + - djangorestframework-simplejwt -- GitLab