From a27c8e8dd32158b454c011ce019168c3f9439ea2 Mon Sep 17 00:00:00 2001 From: Georgeygigz Date: Thu, 17 Jan 2019 09:11:15 +0300 Subject: [PATCH 01/11] feat(SocialAuthentication):allow user to login using social media - enable user to login with google, facebook and twitter - write test for social auth functionality (Starts #162948840) --- authors/apps/authentication/backends.py | 54 ++++++------ authors/apps/authentication/serializers.py | 6 ++ .../authentication/tests/test_social_auth.py | 87 +++++++++++++++++++ authors/apps/authentication/urls.py | 6 +- authors/apps/authentication/views.py | 65 +++++++++++++- authors/settings.py | 36 ++++++++ requirements.txt | 4 + 7 files changed, 225 insertions(+), 33 deletions(-) create mode 100644 authors/apps/authentication/tests/test_social_auth.py diff --git a/authors/apps/authentication/backends.py b/authors/apps/authentication/backends.py index 65497bdc..54bc9c74 100644 --- a/authors/apps/authentication/backends.py +++ b/authors/apps/authentication/backends.py @@ -1,9 +1,12 @@ +import jwt +from datetime import datetime, timedelta + from django.conf import settings from django.http import HttpResponse from rest_framework.authentication import BaseAuthentication from rest_framework.authentication import get_authorization_header from rest_framework import status, exceptions -import jwt + from .models import User @@ -21,32 +24,27 @@ def authenticate(self, request): raise exceptions.AuthenticationFailed(msg) try: - token = auth[1] - if token == "null": - msg = 'Null token not allowed' - raise exceptions.AuthenticationFailed(msg) - except UnicodeError: - msg = 'Invalid token header. Token string contains invalid characters.' - raise exceptions.AuthenticationFailed(msg) - - return self.authenticate_credentials(token) + payload = jwt.decode( + auth, settings.SECRET_KEY, algorithm='HS256') + except: + raise AuthenticationFailed("Your token is invalid. ") - def authenticate_credentials(self, token): - """Check that the user associated with the token - exists in our database""" - payload = jwt.decode(token, settings.SECRET_KEY) - email = payload['email'] try: - user = User.objects.get( - email=email, - is_active=True - ) - if not user: - msg = "User does not exist" - raise exceptions.AuthenticationFailed(msg) - except jwt.ExpiredSignature or jwt.DecodeError or jwt.InvalidTokenError: - return HttpResponse( - {'Error': "Token is invalid"}, - status=status.HTTP_403_FORBIDDEN) - - return user, token + user = User.objects.get(email=payload["email"]) + except User.DoesNotExist: + raise AuthenticationFailed('no available user') + + if not user.is_active: + raise AuthenticationFailed("Acount is not active.") + return (user, auth) + + def generate_token(email, username): + """Generate token.""" + date = datetime.now() + timedelta(days=20) + payload = { + 'email': email, + 'username': username, + 'exp': int(date.strftime('%s')) + } + token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256') + return token.decode() diff --git a/authors/apps/authentication/serializers.py b/authors/apps/authentication/serializers.py index a50244b3..2bed32e8 100644 --- a/authors/apps/authentication/serializers.py +++ b/authors/apps/authentication/serializers.py @@ -195,3 +195,9 @@ def update(self, instance, validated_data): instance.save() return instance + + +class SocialAuthSerializer(serializers.Serializer): + """Social auth serializers.""" + provider = serializers.CharField(max_length=255, required=True) + access_token = serializers.CharField(max_length=1024, required=True, trim_whitespace=True) \ No newline at end of file diff --git a/authors/apps/authentication/tests/test_social_auth.py b/authors/apps/authentication/tests/test_social_auth.py new file mode 100644 index 00000000..8e17eaa2 --- /dev/null +++ b/authors/apps/authentication/tests/test_social_auth.py @@ -0,0 +1,87 @@ +import json +from django.urls import reverse +from rest_framework.views import status +from rest_framework.test import APITestCase, APIClient + + +class SocialAuthTest(APITestCase): + """Test socil authentixation functionality""" + client = APIClient + + def setUp(self): + self.social_oauth_url = reverse('authentication:social_auth') + self.google_access_token = "ya29.GluUBhPKmgM3AMh2tCVajQJUwGfGhaXR_GQ7fbMkMchru6yNd5LgTr0FKVueVzK23ec2X5wRoG-5o0_M-KH-bPVheQ4CavWNJuG_WOfxBXbB1VaeiO6dwmjaoh9c" + self.facebook_access_token = "EAAFKHchEtfYBAITgia1DN1hAJUKsK5PKN5Oz0l7qq4GnCKNF5IQDdcl79cgLW075knYn3bZBX1WYcki5LZCWOm7oAZB9BgGHrt4mSk7SqvsJTDudsbAxwvIwlhuVbyGTsa5ZCO81xZC4NcrZCcIkCBXMejQYHTMpvxpU1Uj3LcwljUJNTwAFyueYGu0MZAdvHUZD" + self.oauth1_access_token = "2858460258-wAufB7oZWWZXRlMJTfXsx2SD18IFITxmMPIeACs" + self.oauth1_access_token_secret = "IIP38lM21FJ3PF1xhZ7PKQ7bGSYIwXSUfoAH8snkkUXJ8" + + self.invalid_provider = { + "provider": "google-oauth21", + "access_token": self.google_access_token + + } + self.google_provider = { + "provider": "google-oauth2", + "access_token": self.google_access_token + } + self.facebook_provider = { + "provider": "facebook", + "access_token": self.facebook_access_token + } + self.twitter_provider = { + "provider": "twitter", + "access_token": self.oauth1_access_token, + "access_token_secret": self.oauth1_access_token_secret + } + + def test_rejects_invalid_provider_name(self): + """Test invalid provider name.""" + response = self.client.post( + self.social_oauth_url, self.invalid_provider, format='json') + self.assertEqual(response.data['error'], + 'Please enter a valid social provider') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_login_with_google(self): + """Test login with google""" + response = self.client.post( + self.social_oauth_url, self.google_provider, format='json') + self.assertIn('email', response.data) + self.assertIn('token', response.data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_login_with_facebook(self): + """Test login with fcabook.""" + response = self.client.post( + self.social_oauth_url, self.facebook_provider, format='json') + self.assertIn('email', response.data) + self.assertIn('token', response.data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_login_with_twitter(self): + """Test login with twitter.""" + response = self.client.post( + self.social_oauth_url, self.twitter_provider, format='json') + self.assertIn('email', response.data) + self.assertIn('token', response.data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_rejects_login_missing_access_token(self): + """Test missing token.""" + response = self.client.post(self.social_oauth_url, + data={"provider": 'facebook'}, + format='json') + self.assertEqual(json.loads(response.content), + {"errors": {"access_token": + ["This field is required."]}}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_rejects_login_missing_provider_name(self): + """Test missing provider.""" + response = self.client.post(self.social_oauth_url, + data={"access_token": + self.google_access_token}, format='json') + self.assertEqual(json.loads(response.content), {"errors": {"provider": + ["This field is required."]}}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + diff --git a/authors/apps/authentication/urls.py b/authors/apps/authentication/urls.py index 5d4ce272..aacf798b 100644 --- a/authors/apps/authentication/urls.py +++ b/authors/apps/authentication/urls.py @@ -3,7 +3,8 @@ from .views import ( LoginAPIView, RegistrationAPIView, UserRetrieveUpdateAPIView, - ResetPasswordAPIView, UpdatePasswordAPIView, VerifyAPIView,ResendVerifyAPIView + ResetPasswordAPIView, UpdatePasswordAPIView, VerifyAPIView, + ResendVerifyAPIView, SocialAuthenticationView ) app_name = "authentication" @@ -15,6 +16,5 @@ path('users/passwordresetdone/', UpdatePasswordAPIView.as_view(), name="passwordresetdone"), path('users/verify//', VerifyAPIView.as_view(), name="auth-verify"), path('users/resend-verification/', ResendVerifyAPIView.as_view(), name="auth-reverify"), - - + path('login/oauth/', SocialAuthenticationView.as_view(), name="social_auth") ] diff --git a/authors/apps/authentication/views.py b/authors/apps/authentication/views.py index 53f22425..8430f8d3 100644 --- a/authors/apps/authentication/views.py +++ b/authors/apps/authentication/views.py @@ -8,18 +8,26 @@ from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView + +from social_django.utils import load_strategy, load_backend +from social_core.exceptions import MissingBackend from rest_framework.renderers import BrowsableAPIRenderer +from social_core.backends.oauth import BaseOAuth1, BaseOAuth2 + +from authors.apps.core.permissions import IsAuthorOrReadOnly +from authors.apps.authentication.backends import JWTAuthentication from .renderers import UserJSONRenderer from . import models from .serializers import ( LoginSerializer, RegistrationSerializer, UserSerializer, - ResetSerializerEmail, ResetSerializerPassword + ResetSerializerEmail, ResetSerializerPassword, SocialAuthSerialize ) from .models import User from .utils import send_link class RegistrationAPIView(generics.CreateAPIView): + # Allow any user (authenticated or not) to hit this endpoint. permission_classes = (AllowAny,) renderer_classes = (UserJSONRenderer,) @@ -194,4 +202,57 @@ def put(self, request, token, **kwargs): except jwt.ExpiredSignatureError: return Response({"The link expired"}, status=status.HTTP_400_BAD_REQUEST) - \ No newline at end of file + + +class SocialAuthenticationView(generics.CreateAPIView): + """Social authentication.""" + permission_classes = (AllowAny,) + serializer_class = SocialAuthSerializer + render_classes = (UserJSONRenderer,) + + def create(self, request, *args, **kwargs): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + provider = request.data['provider'] + strategy = load_strategy(request) + authenticated_user = request.user if not request.user.is_anonymous else None + + try: + backend = load_backend( + strategy=strategy, + name=provider, + redirect_uri=None + ) + + if isinstance(backend, BaseOAuth1): + if "access_token_secret" in request.data: + token = { + 'oauth_token': request.data['access_token'], + 'oauth_token_secret': + request.data['access_token_secret'] + } + + else: + return Response({'error': + 'Please enter your secret token'}, + status=status.HTTP_400_BAD_REQUEST) + elif isinstance(backend, BaseOAuth2): + token = serializer.data.get('access_token') + + except MissingBackend: + return Response({ + 'error': 'Please enter a valid social provider' + }, status=status.HTTP_400_BAD_REQUEST) + try: + user = backend.do_auth(token, user=authenticated_user) + except BaseException as error: + return Response({"error": str(error)}) + if user: + user.is_active = True + user.save() + serializer = UserSerializer(user) + serialized_details = serializer.data + serialized_details["token"] = JWTAuthentication.generate_token( + serialized_details["email"], serialized_details["username"]) + + return Response(serialized_details, status.HTTP_200_OK) diff --git a/authors/settings.py b/authors/settings.py index 4116953f..f4aa84ac 100644 --- a/authors/settings.py +++ b/authors/settings.py @@ -47,6 +47,9 @@ 'authors.apps.profiles', 'authors.apps.friends', 'authors.apps.articles', + 'oauth2_provider', + 'social_django', + 'rest_framework_social_oauth2', # Enables API to be documented using Swagger 'rest_framework_swagger', @@ -153,7 +156,40 @@ 'authors.apps.authentication.backends.JWTAuthentication', ), } +AUTHENTICATION_BACKENDS = ( + 'social_core.backends.google.GoogleOAuth2', + 'social_core.backends.twitter.TwitterOAuth', + 'social_core.backends.facebook.FacebookOAuth2', + 'django.contrib.auth.backends.ModelBackend', +) + +SOCIAL_AUTH_FACEBOOK_KEY = os.getenv('APP_ID') +SOCIAL_AUTH_FACEBOOK_SECRET = os.getenv('APP_SECRET') +SOCIAL_AUTH_FACEBOOK_SCOPE = ['email'] +SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = { + 'fields': 'id, name, email' +} +SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = os.getenv('OAUTH2_KEY') +SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = os.getenv('OAUTH2_SECRET') +SOCIAL_AUTH_GOOGLE_OAUTH_SCOPE = ['email', 'username', 'password'] + +SOCIAL_AUTH_TWITTER_KEY = os.getenv('TWITTER_KEY') +SOCIAL_AUTH_TWITTER_SECRET = os.getenv('TWITTER_SECRET') +SOCIAL_AUTH_TWITTER_SCOPE = ['email'] + +SOCIAL_AUTH_PIPELINE = ( + 'social_core.pipeline.social_auth.social_details', + 'social_core.pipeline.social_auth.social_uid', + 'social_core.pipeline.social_auth.auth_allowed', + 'social_core.pipeline.social_auth.social_user', + 'social_core.pipeline.user.get_username', + 'social_core.pipeline.social_auth.associate_by_email', + 'social_core.pipeline.user.create_user', + 'social_core.pipeline.social_auth.associate_user', + 'social_core.pipeline.social_auth.load_extra_data', + 'social_core.pipeline.user.user_details', +) django_heroku.settings(locals()) # Set the settings for Swagger documentation diff --git a/requirements.txt b/requirements.txt index b28c9dea..a21d7236 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,3 +21,7 @@ six==1.12.0 pytz==2018.9 cloudinary==1.15.0 Pillow==5.4.1 +social-auth-app-django==3.1.0 +social-auth-core==3.0.0 +django-rest-framework-social-oauth2==1.1.0 +django-oauth-toolkit==1.2.0 From 66388547b6e2358116d48acc1ac8a1a77b82a472 Mon Sep 17 00:00:00 2001 From: Georgeygigz Date: Fri, 18 Jan 2019 03:08:11 +0300 Subject: [PATCH 02/11] feat(social auth):add social login functionality - add twitter social media - handle token generation in backends [Starts #162948840] --- .gitignore | 1 + .travis.yml | 7 ++- authors/apps/authentication/backends.py | 51 ++++++++++--------- .../authentication/migrations/0001_initial.py | 2 +- authors/apps/authentication/models.py | 5 ++ .../authentication/tests/test_social_auth.py | 19 +++---- authors/apps/authentication/views.py | 47 ++++++++++++----- authors/settings.py | 3 ++ requirements.txt | 1 + 9 files changed, 83 insertions(+), 53 deletions(-) diff --git a/.gitignore b/.gitignore index fa709082..66ac3f66 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ var/ *.egg .idea/ + # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. diff --git a/.travis.yml b/.travis.yml index fcb1e6c6..d39a63ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,11 +12,14 @@ services: - postgresql addons: - postgresql: "9.6" + postgresql: '9.4' + apt: + packages: + - postgresql-server-dev-9.4 before-script: - psql -c 'CREATE DATABASE test;' -U postgres - - python manage.py makemigrations + - python manage.py makemigrations - python manage.py migrate script: diff --git a/authors/apps/authentication/backends.py b/authors/apps/authentication/backends.py index 54bc9c74..4b44fe85 100644 --- a/authors/apps/authentication/backends.py +++ b/authors/apps/authentication/backends.py @@ -1,12 +1,10 @@ import jwt -from datetime import datetime, timedelta from django.conf import settings from django.http import HttpResponse from rest_framework.authentication import BaseAuthentication from rest_framework.authentication import get_authorization_header from rest_framework import status, exceptions - from .models import User @@ -24,27 +22,32 @@ def authenticate(self, request): raise exceptions.AuthenticationFailed(msg) try: - payload = jwt.decode( - auth, settings.SECRET_KEY, algorithm='HS256') - except: - raise AuthenticationFailed("Your token is invalid. ") + token = auth[1] + if token == "null": + msg = 'Null token not allowed' + raise exceptions.AuthenticationFailed(msg) + except UnicodeError: + msg = 'Invalid token header. Token string contains invalid characters.' + raise exceptions.AuthenticationFailed(msg) + + return self.authenticate_credentials(token) + def authenticate_credentials(self, token): + """Check that the user associated with the token + exists in our database""" + payload = jwt.decode(token, settings.SECRET_KEY) + email = payload['email'] try: - user = User.objects.get(email=payload["email"]) - except User.DoesNotExist: - raise AuthenticationFailed('no available user') - - if not user.is_active: - raise AuthenticationFailed("Acount is not active.") - return (user, auth) - - def generate_token(email, username): - """Generate token.""" - date = datetime.now() + timedelta(days=20) - payload = { - 'email': email, - 'username': username, - 'exp': int(date.strftime('%s')) - } - token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256') - return token.decode() + user = User.objects.get( + email=email, + is_active=True + ) + if not user: + msg = "User does not exist" + raise exceptions.AuthenticationFailed(msg) + except jwt.ExpiredSignature or jwt.DecodeError or jwt.InvalidTokenError: + return HttpResponse( + {'Error': "Token is invalid"}, + status=status.HTTP_403_FORBIDDEN) + + return user, token \ No newline at end of file diff --git a/authors/apps/authentication/migrations/0001_initial.py b/authors/apps/authentication/migrations/0001_initial.py index 6e37a19a..f5f4ee2b 100644 --- a/authors/apps/authentication/migrations/0001_initial.py +++ b/authors/apps/authentication/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 2.1.4 on 2019-01-08 14:41 +# Generated by Django 2.1.4 on 2019-01-18 07:38 from django.db import migrations, models diff --git a/authors/apps/authentication/models.py b/authors/apps/authentication/models.py index 6c56a450..958eb8c5 100644 --- a/authors/apps/authentication/models.py +++ b/authors/apps/authentication/models.py @@ -1,6 +1,9 @@ import jwt from datetime import datetime, timedelta from django.conf import settings +from django.dispatch import receiver +from django.db.models.signals import post_save +from rest_framework.authtoken.models import Token from django.contrib.auth.models import ( AbstractBaseUser, BaseUserManager, PermissionsMixin ) @@ -130,3 +133,5 @@ def token(self): } token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256').decode('utf-8') return token + + diff --git a/authors/apps/authentication/tests/test_social_auth.py b/authors/apps/authentication/tests/test_social_auth.py index 8e17eaa2..a0799734 100644 --- a/authors/apps/authentication/tests/test_social_auth.py +++ b/authors/apps/authentication/tests/test_social_auth.py @@ -5,16 +5,16 @@ class SocialAuthTest(APITestCase): - """Test socil authentixation functionality""" + """Test social authentication funcionality functionality""" client = APIClient def setUp(self): self.social_oauth_url = reverse('authentication:social_auth') - self.google_access_token = "ya29.GluUBhPKmgM3AMh2tCVajQJUwGfGhaXR_GQ7fbMkMchru6yNd5LgTr0FKVueVzK23ec2X5wRoG-5o0_M-KH-bPVheQ4CavWNJuG_WOfxBXbB1VaeiO6dwmjaoh9c" - self.facebook_access_token = "EAAFKHchEtfYBAITgia1DN1hAJUKsK5PKN5Oz0l7qq4GnCKNF5IQDdcl79cgLW075knYn3bZBX1WYcki5LZCWOm7oAZB9BgGHrt4mSk7SqvsJTDudsbAxwvIwlhuVbyGTsa5ZCO81xZC4NcrZCcIkCBXMejQYHTMpvxpU1Uj3LcwljUJNTwAFyueYGu0MZAdvHUZD" - self.oauth1_access_token = "2858460258-wAufB7oZWWZXRlMJTfXsx2SD18IFITxmMPIeACs" - self.oauth1_access_token_secret = "IIP38lM21FJ3PF1xhZ7PKQ7bGSYIwXSUfoAH8snkkUXJ8" - + self.google_access_token = "ya29.GluWBq4fUvGRqGnt8H_CU4WwSEz8xNyaj8qCj0ooEKPyDMjGJrhVz8B_qgbB7Kstq7ZE5b3PTK4fSkg6xxJbe1NWmQIEhmmmJGGv52DZPpIekcY14vkB3L0ylFVt" + self.facebook_access_token = "EAAFKHchEtfYBAHXZCPx4WOjlrbatpx00Wo4aEBNTqpKxAuAbRyiZBZBLrzcYYx0kSpPVikxFUGyniejxUXhPkGvJLRvKEZAIPgfyl27uWOu4DIs4O3vmE26FpQBBtowOrSSluCIsYIYPZA5YRa0rWJQtmCd7B3paAUZA7DlWKlrrKD3ZCAUHemQTwPPZCkWwkbkZD" + self.oauth1_access_token = "2858460258-5giuATgW1FdkZe8uKPY86mueQqlLoiLPoXaxxDt" + self.oauth1_access_token_secret = "ep3d3fAseeJwKcaGtOXtnZj6pRpuyEtyzOrWXvecbB8V8" + self.invalid_provider = { "provider": "google-oauth21", "access_token": self.google_access_token @@ -46,16 +46,12 @@ def test_login_with_google(self): """Test login with google""" response = self.client.post( self.social_oauth_url, self.google_provider, format='json') - self.assertIn('email', response.data) - self.assertIn('token', response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_login_with_facebook(self): - """Test login with fcabook.""" + """Test login with facebook.""" response = self.client.post( self.social_oauth_url, self.facebook_provider, format='json') - self.assertIn('email', response.data) - self.assertIn('token', response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_login_with_twitter(self): @@ -84,4 +80,3 @@ def test_rejects_login_missing_provider_name(self): self.assertEqual(json.loads(response.content), {"errors": {"provider": ["This field is required."]}}) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - diff --git a/authors/apps/authentication/views.py b/authors/apps/authentication/views.py index 8430f8d3..8b98eb0d 100644 --- a/authors/apps/authentication/views.py +++ b/authors/apps/authentication/views.py @@ -1,26 +1,31 @@ import os import jwt from datetime import datetime, timedelta + from django.conf import settings from django.core.mail import send_mail +<<<<<<< HEAD from django.template.loader import render_to_string +======= +from django.db import IntegrityError +>>>>>>> feat(social auth):add social login functionality from rest_framework import status, generics from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView - from social_django.utils import load_strategy, load_backend -from social_core.exceptions import MissingBackend -from rest_framework.renderers import BrowsableAPIRenderer +from social_core.exceptions import MissingBackend, AuthAlreadyAssociated from social_core.backends.oauth import BaseOAuth1, BaseOAuth2 from authors.apps.core.permissions import IsAuthorOrReadOnly from authors.apps.authentication.backends import JWTAuthentication -from .renderers import UserJSONRenderer from . import models +from .renderers import UserJSONRenderer +from .models import User +from . import serializers from .serializers import ( LoginSerializer, RegistrationSerializer, UserSerializer, - ResetSerializerEmail, ResetSerializerPassword, SocialAuthSerialize + ResetSerializerEmail, ResetSerializerPassword, SocialAuthSerializer ) from .models import User from .utils import send_link @@ -228,31 +233,45 @@ def create(self, request, *args, **kwargs): if "access_token_secret" in request.data: token = { 'oauth_token': request.data['access_token'], - 'oauth_token_secret': + 'oauth_token_secret': request.data['access_token_secret'] } - else: - return Response({'error': + return Response({'error': 'Please enter your secret token'}, status=status.HTTP_400_BAD_REQUEST) elif isinstance(backend, BaseOAuth2): token = serializer.data.get('access_token') - except MissingBackend: return Response({ 'error': 'Please enter a valid social provider' }, status=status.HTTP_400_BAD_REQUEST) + try: user = backend.do_auth(token, user=authenticated_user) - except BaseException as error: - return Response({"error": str(error)}) + except (AuthAlreadyAssociated, IntegrityError): + return Response({ + "errors": "User is authenticated"}, + status=status.HTTP_400_BAD_REQUEST) + except BaseException: + return Response({ + "errors": "The token has expired"}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) if user: user.is_active = True + username = user.username + email = user.email user.save() + + date = datetime.now() + timedelta(days=20) + payload = { + 'email': email, + 'username': username, + 'exp': int(date.strftime('%s')) + } + user_token = jwt.encode( + payload, settings.SECRET_KEY, algorithm='HS256') serializer = UserSerializer(user) serialized_details = serializer.data - serialized_details["token"] = JWTAuthentication.generate_token( - serialized_details["email"], serialized_details["username"]) - + serialized_details["token"] = user_token return Response(serialized_details, status.HTTP_200_OK) diff --git a/authors/settings.py b/authors/settings.py index f4aa84ac..c7de5a28 100644 --- a/authors/settings.py +++ b/authors/settings.py @@ -42,6 +42,7 @@ 'rest_framework', 'taggit', + 'authors.apps.authentication', 'authors.apps.core', 'authors.apps.profiles', @@ -50,6 +51,8 @@ 'oauth2_provider', 'social_django', 'rest_framework_social_oauth2', + + # Enables API to be documented using Swagger 'rest_framework_swagger', diff --git a/requirements.txt b/requirements.txt index a21d7236..c2e8fd0a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,3 +25,4 @@ social-auth-app-django==3.1.0 social-auth-core==3.0.0 django-rest-framework-social-oauth2==1.1.0 django-oauth-toolkit==1.2.0 +coverage==4.5.2 From 10ccf54393e3ceb17b4b439ebd715ba3cbe2f8f1 Mon Sep 17 00:00:00 2001 From: Georgeygigz Date: Sat, 19 Jan 2019 10:31:20 +0300 Subject: [PATCH 03/11] feat(imports):Remove unused imports - remove unused imports [Fixes #162948840] --- authors/apps/authentication/models.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/authors/apps/authentication/models.py b/authors/apps/authentication/models.py index 958eb8c5..6c56a450 100644 --- a/authors/apps/authentication/models.py +++ b/authors/apps/authentication/models.py @@ -1,9 +1,6 @@ import jwt from datetime import datetime, timedelta from django.conf import settings -from django.dispatch import receiver -from django.db.models.signals import post_save -from rest_framework.authtoken.models import Token from django.contrib.auth.models import ( AbstractBaseUser, BaseUserManager, PermissionsMixin ) @@ -133,5 +130,3 @@ def token(self): } token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256').decode('utf-8') return token - - From f820a7baa736ab29b13c11df4148b9e3b5159e3e Mon Sep 17 00:00:00 2001 From: Georgeygigz Date: Sat, 19 Jan 2019 14:21:08 +0300 Subject: [PATCH 04/11] feat(imports):Remove unused imports - remove double imports - use single quotes for uniformity [Fixes #162948840] --- authors/apps/authentication/tests/test_social_auth.py | 4 ++-- authors/apps/authentication/views.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/authors/apps/authentication/tests/test_social_auth.py b/authors/apps/authentication/tests/test_social_auth.py index a0799734..257a01bf 100644 --- a/authors/apps/authentication/tests/test_social_auth.py +++ b/authors/apps/authentication/tests/test_social_auth.py @@ -10,8 +10,8 @@ class SocialAuthTest(APITestCase): def setUp(self): self.social_oauth_url = reverse('authentication:social_auth') - self.google_access_token = "ya29.GluWBq4fUvGRqGnt8H_CU4WwSEz8xNyaj8qCj0ooEKPyDMjGJrhVz8B_qgbB7Kstq7ZE5b3PTK4fSkg6xxJbe1NWmQIEhmmmJGGv52DZPpIekcY14vkB3L0ylFVt" - self.facebook_access_token = "EAAFKHchEtfYBAHXZCPx4WOjlrbatpx00Wo4aEBNTqpKxAuAbRyiZBZBLrzcYYx0kSpPVikxFUGyniejxUXhPkGvJLRvKEZAIPgfyl27uWOu4DIs4O3vmE26FpQBBtowOrSSluCIsYIYPZA5YRa0rWJQtmCd7B3paAUZA7DlWKlrrKD3ZCAUHemQTwPPZCkWwkbkZD" + self.google_access_token = "ya29.GluWBs6tZRl_j9ZYNPelugOza5fcsFnRvC1NrYLhanbqEXpN3pAmTOEMnX-MLvpdHeT1nV6S__-F6FQuOPZ5p_86HPuxk8T5S1kl3z3PDASH-Dx8aJEILEI8G1K4" + self.facebook_access_token = "EAAFKHchEtfYBABWLGvrwJZCs9CcRYnkwJGroQn1OHKEt10mp8rkzpMYPA90m5f0llwODHdGV9kpZATBo8dZBditNlwBlhcdZAyuqdvIOqy6TtnGZAEsGOpULsTct9ZC88ZClPfcemTcS7VirUXX7h3D0THReoEfk5PZB3Ep1rtUTYDycosaIHCOwVdeUXmuIEi4ZD" self.oauth1_access_token = "2858460258-5giuATgW1FdkZe8uKPY86mueQqlLoiLPoXaxxDt" self.oauth1_access_token_secret = "ep3d3fAseeJwKcaGtOXtnZj6pRpuyEtyzOrWXvecbB8V8" diff --git a/authors/apps/authentication/views.py b/authors/apps/authentication/views.py index 8b98eb0d..25444fc0 100644 --- a/authors/apps/authentication/views.py +++ b/authors/apps/authentication/views.py @@ -21,7 +21,6 @@ from authors.apps.authentication.backends import JWTAuthentication from . import models from .renderers import UserJSONRenderer -from .models import User from . import serializers from .serializers import ( LoginSerializer, RegistrationSerializer, UserSerializer, From a4dde30b2201f335dcad354b27e09a22e35c9fe2 Mon Sep 17 00:00:00 2001 From: Georgeygigz Date: Sun, 20 Jan 2019 19:42:54 +0300 Subject: [PATCH 05/11] feat(add line):add blank line at the end of each file - add a blank line at the end of each file [Fixes #162948840] --- authors/apps/authentication/backends.py | 3 ++- authors/apps/authentication/serializers.py | 3 ++- authors/apps/authentication/tests/test_social_auth.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/authors/apps/authentication/backends.py b/authors/apps/authentication/backends.py index 4b44fe85..3064fce2 100644 --- a/authors/apps/authentication/backends.py +++ b/authors/apps/authentication/backends.py @@ -50,4 +50,5 @@ def authenticate_credentials(self, token): {'Error': "Token is invalid"}, status=status.HTTP_403_FORBIDDEN) - return user, token \ No newline at end of file + return user, token + \ No newline at end of file diff --git a/authors/apps/authentication/serializers.py b/authors/apps/authentication/serializers.py index 2bed32e8..cce840cc 100644 --- a/authors/apps/authentication/serializers.py +++ b/authors/apps/authentication/serializers.py @@ -200,4 +200,5 @@ def update(self, instance, validated_data): class SocialAuthSerializer(serializers.Serializer): """Social auth serializers.""" provider = serializers.CharField(max_length=255, required=True) - access_token = serializers.CharField(max_length=1024, required=True, trim_whitespace=True) \ No newline at end of file + access_token = serializers.CharField(max_length=1024, required=True, trim_whitespace=True) + \ No newline at end of file diff --git a/authors/apps/authentication/tests/test_social_auth.py b/authors/apps/authentication/tests/test_social_auth.py index 257a01bf..21838073 100644 --- a/authors/apps/authentication/tests/test_social_auth.py +++ b/authors/apps/authentication/tests/test_social_auth.py @@ -10,8 +10,8 @@ class SocialAuthTest(APITestCase): def setUp(self): self.social_oauth_url = reverse('authentication:social_auth') - self.google_access_token = "ya29.GluWBs6tZRl_j9ZYNPelugOza5fcsFnRvC1NrYLhanbqEXpN3pAmTOEMnX-MLvpdHeT1nV6S__-F6FQuOPZ5p_86HPuxk8T5S1kl3z3PDASH-Dx8aJEILEI8G1K4" - self.facebook_access_token = "EAAFKHchEtfYBABWLGvrwJZCs9CcRYnkwJGroQn1OHKEt10mp8rkzpMYPA90m5f0llwODHdGV9kpZATBo8dZBditNlwBlhcdZAyuqdvIOqy6TtnGZAEsGOpULsTct9ZC88ZClPfcemTcS7VirUXX7h3D0THReoEfk5PZB3Ep1rtUTYDycosaIHCOwVdeUXmuIEi4ZD" + self.google_access_token = "ya29.GluXBun21tX5j8lWx7SLmEiZW3M2YMgGDF9CvK4QE9BQPzSXflkvrez_40azqmt0ft0zo5Yed7mg5mrfWpr9FVhN6xcsk6G11DBxClpMtDBt75lgrhrj2MZC1Vo4" + self.facebook_access_token = "EAAFKHchEtfYBAGJLNcPRLgzjbY2jAIyv7fisZAIXWxB9ZB2iO3BRWfVk0wK9E6fnJtOVBWZBXoLsMwG6uwt33s1Vs5haoECVgXy5dXEWehSPUYsr2R2IhObbAT9l0mUASAhViL7PEN4fbPlqbdPsDadzVZAdrEMZC6CREkU8twZCtdxdBZBXH6cwOuWb0Gp9isZD" self.oauth1_access_token = "2858460258-5giuATgW1FdkZe8uKPY86mueQqlLoiLPoXaxxDt" self.oauth1_access_token_secret = "ep3d3fAseeJwKcaGtOXtnZj6pRpuyEtyzOrWXvecbB8V8" From 08c1617fd4b22693706097eb3a30e3d9d1dfe98b Mon Sep 17 00:00:00 2001 From: Georgeygigz Date: Mon, 21 Jan 2019 12:33:32 +0300 Subject: [PATCH 06/11] feat(Error and Token):Handle errors and add token to variable environments -Add a descriptive message when user is already authenticated using another account -Add provider token to virtual environment [Fixes #162948842] --- authors/apps/authentication/tests/test_social_auth.py | 9 +++++---- authors/apps/authentication/views.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/authors/apps/authentication/tests/test_social_auth.py b/authors/apps/authentication/tests/test_social_auth.py index 21838073..89eb6d9e 100644 --- a/authors/apps/authentication/tests/test_social_auth.py +++ b/authors/apps/authentication/tests/test_social_auth.py @@ -1,4 +1,5 @@ import json +import os from django.urls import reverse from rest_framework.views import status from rest_framework.test import APITestCase, APIClient @@ -10,10 +11,10 @@ class SocialAuthTest(APITestCase): def setUp(self): self.social_oauth_url = reverse('authentication:social_auth') - self.google_access_token = "ya29.GluXBun21tX5j8lWx7SLmEiZW3M2YMgGDF9CvK4QE9BQPzSXflkvrez_40azqmt0ft0zo5Yed7mg5mrfWpr9FVhN6xcsk6G11DBxClpMtDBt75lgrhrj2MZC1Vo4" - self.facebook_access_token = "EAAFKHchEtfYBAGJLNcPRLgzjbY2jAIyv7fisZAIXWxB9ZB2iO3BRWfVk0wK9E6fnJtOVBWZBXoLsMwG6uwt33s1Vs5haoECVgXy5dXEWehSPUYsr2R2IhObbAT9l0mUASAhViL7PEN4fbPlqbdPsDadzVZAdrEMZC6CREkU8twZCtdxdBZBXH6cwOuWb0Gp9isZD" - self.oauth1_access_token = "2858460258-5giuATgW1FdkZe8uKPY86mueQqlLoiLPoXaxxDt" - self.oauth1_access_token_secret = "ep3d3fAseeJwKcaGtOXtnZj6pRpuyEtyzOrWXvecbB8V8" + self.google_access_token = os.getenv('GOOGLE_TOKEN') + self.facebook_access_token = os.getenv('FACEBOOK_TOKEN') + self.oauth1_access_token = os.getenv('TWITTER_TOKEN') + self.oauth1_access_token_secret = os.getenv('TWITTER_SECRET_TOKEN') self.invalid_provider = { "provider": "google-oauth21", diff --git a/authors/apps/authentication/views.py b/authors/apps/authentication/views.py index 25444fc0..3483d64c 100644 --- a/authors/apps/authentication/views.py +++ b/authors/apps/authentication/views.py @@ -250,7 +250,7 @@ def create(self, request, *args, **kwargs): user = backend.do_auth(token, user=authenticated_user) except (AuthAlreadyAssociated, IntegrityError): return Response({ - "errors": "User is authenticated"}, + "errors": "You are already logged in with another account"}, status=status.HTTP_400_BAD_REQUEST) except BaseException: return Response({ From 8a6157bba45b934d5199c27bf4cfe20d3c27d9e7 Mon Sep 17 00:00:00 2001 From: Georgeygigz Date: Mon, 21 Jan 2019 16:44:57 +0300 Subject: [PATCH 07/11] feat(Invalid Token):Return a message when token is invalid -Custom message for invalid token [Fixes #162948840] --- authors/apps/authentication/views.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/authors/apps/authentication/views.py b/authors/apps/authentication/views.py index 3483d64c..4c2da26f 100644 --- a/authors/apps/authentication/views.py +++ b/authors/apps/authentication/views.py @@ -1,24 +1,20 @@ import os import jwt from datetime import datetime, timedelta +from social_django.utils import load_strategy, load_backend +from social_core.exceptions import MissingBackend, AuthAlreadyAssociated +from social_core.backends.oauth import BaseOAuth1, BaseOAuth2 from django.conf import settings from django.core.mail import send_mail -<<<<<<< HEAD from django.template.loader import render_to_string -======= from django.db import IntegrityError ->>>>>>> feat(social auth):add social login functionality from rest_framework import status, generics from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from social_django.utils import load_strategy, load_backend -from social_core.exceptions import MissingBackend, AuthAlreadyAssociated -from social_core.backends.oauth import BaseOAuth1, BaseOAuth2 -from authors.apps.core.permissions import IsAuthorOrReadOnly -from authors.apps.authentication.backends import JWTAuthentication + from . import models from .renderers import UserJSONRenderer from . import serializers @@ -254,13 +250,12 @@ def create(self, request, *args, **kwargs): status=status.HTTP_400_BAD_REQUEST) except BaseException: return Response({ - "errors": "The token has expired"}, + "errors": "Invalid token"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) if user: user.is_active = True username = user.username email = user.email - user.save() date = datetime.now() + timedelta(days=20) payload = { From d33e6e25e8527ee48609180e986977849d228192 Mon Sep 17 00:00:00 2001 From: Georgeygigz Date: Thu, 24 Jan 2019 21:54:06 +0300 Subject: [PATCH 08/11] feat(unused imports):Remove unused imports - Remove imports that are not used [Fixes #162948840] --- authors/apps/authentication/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/authors/apps/authentication/views.py b/authors/apps/authentication/views.py index 4c2da26f..66cc714d 100644 --- a/authors/apps/authentication/views.py +++ b/authors/apps/authentication/views.py @@ -17,7 +17,6 @@ from . import models from .renderers import UserJSONRenderer -from . import serializers from .serializers import ( LoginSerializer, RegistrationSerializer, UserSerializer, ResetSerializerEmail, ResetSerializerPassword, SocialAuthSerializer From 5148e7fd8a754666da213e7407686825f80658d4 Mon Sep 17 00:00:00 2001 From: Georgeygigz Date: Thu, 24 Jan 2019 23:10:03 +0300 Subject: [PATCH 09/11] feat(Check data in response):Check user data returned when user is authenticated -Remove unnecesary words -Assert for data in response.data -Format the code inorder for key-value pairs to be in the same line [Fixes #162948840] --- .../apps/authentication/tests/test_social_auth.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/authors/apps/authentication/tests/test_social_auth.py b/authors/apps/authentication/tests/test_social_auth.py index 89eb6d9e..c3f8e3af 100644 --- a/authors/apps/authentication/tests/test_social_auth.py +++ b/authors/apps/authentication/tests/test_social_auth.py @@ -6,7 +6,7 @@ class SocialAuthTest(APITestCase): - """Test social authentication funcionality functionality""" + """Test social authentication funcionality.""" client = APIClient def setUp(self): @@ -47,12 +47,16 @@ def test_login_with_google(self): """Test login with google""" response = self.client.post( self.social_oauth_url, self.google_provider, format='json') + self.assertIn('email', response.data) + self.assertIn('token', response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_login_with_facebook(self): """Test login with facebook.""" response = self.client.post( self.social_oauth_url, self.facebook_provider, format='json') + self.assertIn('email', response.data) + self.assertIn('token', response.data) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_login_with_twitter(self): @@ -75,9 +79,9 @@ def test_rejects_login_missing_access_token(self): def test_rejects_login_missing_provider_name(self): """Test missing provider.""" - response = self.client.post(self.social_oauth_url, - data={"access_token": - self.google_access_token}, format='json') + response = self.client.post( + self.social_oauth_url, + data={"access_token": self.google_access_token}, format='json') self.assertEqual(json.loads(response.content), {"errors": {"provider": ["This field is required."]}}) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) From e796a69d8ffe633b2bcbfed577df047805960b78 Mon Sep 17 00:00:00 2001 From: Georgeygigz Date: Thu, 24 Jan 2019 23:41:28 +0300 Subject: [PATCH 10/11] feat(updated travis):upgrade postgress version -upgrade postgres version [Fixes #162948840] --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index d39a63ee..1950b0e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,10 +12,7 @@ services: - postgresql addons: - postgresql: '9.4' - apt: - packages: - - postgresql-server-dev-9.4 + postgresql: '9.6' before-script: - psql -c 'CREATE DATABASE test;' -U postgres From ec5024f4b304f8518f1c029032194623ae313819 Mon Sep 17 00:00:00 2001 From: Georgeygigz Date: Fri, 25 Jan 2019 10:33:27 +0300 Subject: [PATCH 11/11] feat(Facebook config keys):add facebook prefix to facebook keys -Add facebook prefix to app_id and secret key [Fixes #162948845] --- .env.sample | 7 +++++++ authors/settings.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.env.sample b/.env.sample index 9fb8020f..89ed0995 100644 --- a/.env.sample +++ b/.env.sample @@ -10,3 +10,10 @@ export PASSWORD_RESET_URL="127.0.0.1:8000/api" export DEFAULT_FROM_EMAIL="senders email" export SECRET_KEY="enter your secret key" export EMAIL_VERIFICATION_URL="http://127.0.0.1:8000/" + +export FACEBOOK_APP_ID="your_facebook_app_id" +export FACEBOOK_APP_SECRET="your_facebook_app_secret" +export OAUTH2_KEY="your_google_app_secret" +export OAUTH2_SECRET="your_google_secret" +export TWITTER_KEY="your_twitter_key" +export TWITTER_SECRET="your_twitter_secret" diff --git a/authors/settings.py b/authors/settings.py index c7de5a28..9f8d225a 100644 --- a/authors/settings.py +++ b/authors/settings.py @@ -166,8 +166,8 @@ 'django.contrib.auth.backends.ModelBackend', ) -SOCIAL_AUTH_FACEBOOK_KEY = os.getenv('APP_ID') -SOCIAL_AUTH_FACEBOOK_SECRET = os.getenv('APP_SECRET') +SOCIAL_AUTH_FACEBOOK_KEY = os.getenv('FACEBOOK_APP_ID') +SOCIAL_AUTH_FACEBOOK_SECRET = os.getenv('FACEBOOK_APP_SECRET') SOCIAL_AUTH_FACEBOOK_SCOPE = ['email'] SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = { 'fields': 'id, name, email'