Skip to content

Commit

Permalink
Merge pull request #36 from andela/ft-anable-social-login-161967012
Browse files Browse the repository at this point in the history
#161967012 Enable Social login via Google, Facebook and Twitter
  • Loading branch information
dmithamo1 authored Jan 3, 2019
2 parents d31c462 + 87074c2 commit 485533d
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 16 deletions.
4 changes: 1 addition & 3 deletions authors/apps/articles/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import logging
from django.db import models
from rest_framework.reverse import reverse

from authors.apps.authentication.models import User

from django.db.models.signals import post_save
from django.dispatch import receiver

# Create your models here.
from authors.apps.authentication.models import User
from authors.apps.notifier.utils import Notifier
from authors.apps.profiles.models import Profile

Expand Down
16 changes: 16 additions & 0 deletions authors/apps/authentication/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ def generate_token(user, is_refresh_token=False):
logger.debug("is_refresh_token : %s : %s" % (is_refresh_token, token))
return token

@staticmethod
def generate_social_token(username, is_refresh_token=False):
"""
generate a social token
"""
secret = settings.SECRET_KEY
token = jwt.encode({
'username': username,
'iat': datetime.datetime.utcnow(),
'nbf': datetime.datetime.utcnow() + datetime.timedelta(minutes=-5),
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=5)
}, secret)
# decode the byte type token to
token = token.decode('utf-8')
return token

def authenticate_credentials(self, key):
try:
# decode the payload and get the user
Expand Down
2 changes: 1 addition & 1 deletion authors/apps/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class User(AbstractBaseUser, PermissionsMixin):
# themselves when logging in. Since we need an email address for contacting
# the user anyways, we will also use the email for logging in because it is
# the most common form of login credential at the time of writing.
email = models.EmailField(db_index=True, unique=True)
email = models.EmailField(db_index=True, unique=True, default=None)

# When a user no longer wishes to use our platform, they may try to delete
# there account. That's a problem for us because the data we collect is
Expand Down
25 changes: 17 additions & 8 deletions authors/apps/authentication/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,17 @@ def get_token(self, obj):
:return token:
"""
return JWTAuthentication.generate_token(
user=obj,
is_refresh_token=False)
user=obj,
is_refresh_token=False)

def get_refresh_token(self, obj):
"""
Generate a refresh token
:return refresh token:
"""
return JWTAuthentication.generate_token(
user=obj,
is_refresh_token=True)
user=obj,
is_refresh_token=True)


class LoginSerializer(serializers.ModelSerializer):
Expand All @@ -120,17 +120,17 @@ def get_token(self, obj):
:return token:
"""
return JWTAuthentication.generate_token(
user=obj,
is_refresh_token=True)
user=obj,
is_refresh_token=True)

def get_refresh_token(self, obj):
"""
fetch and return refresh token
:return:
"""
return JWTAuthentication.generate_token(
user=obj,
is_refresh_token=False)
user=obj,
is_refresh_token=False)

def validate(self, data):
# The `validate` method is where we make sure that the current
Expand Down Expand Up @@ -242,3 +242,12 @@ def update(self, instance, validated_data):
instance.save()

return instance


class SocialAuthenticator(serializers.Serializer):
""" Accepts the Oauth input acces token , and access_token_secret"""
provider = serializers.CharField(max_length=255, required=True)
access_token = serializers.CharField(
max_length=4096, required=True, trim_whitespace=True)
access_token_secret = serializers.CharField(
max_length=4096, required=False, trim_whitespace=True)
110 changes: 110 additions & 0 deletions authors/apps/authentication/tests/test_social_oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import os

from rest_framework.test import APITestCase
from django.urls import reverse
from rest_framework import status


class SocialOauthTest(APITestCase):
"""
Test user social login
"""
FACEBOOK_ACCESS_TOKEN = os.getenv('FACEBOOK_ACCESS_TOKEN')
TWITTER_ACCESS_TOKEN = os.getenv('TWITTER_ACCESS_TOKEN')
TWITTER_ACCESS_TOKEN_SECRET = os.getenv('TWITTER_ACCESS_TOKEN_SECRET')
GOOGLE_ACCESS_TOKEN = "Time bound"

def setUp(self):
self.facebook_data = {
"provider": "facebook",
"access_token": self.FACEBOOK_ACCESS_TOKEN
}

self.twitter_data = {
"provider": "twitter",
"access_token": self.TWITTER_ACCESS_TOKEN,
"access_token_secret": self.TWITTER_ACCESS_TOKEN_SECRET
}

def test_facebook_login(self):
""" Test successful user login """
response = self.client.post(reverse('social_auth'),
self.facebook_data,
format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

response.render()
self.assertIn(b"GransonOyombe",
response.content) # TEST FOR USERNAME
self.assertIn(b"facebook", response.content) # TEST FOR THE PROVIDER
self.assertIn(b"social_token", response.content) # TEST FOR THE TOKEN

def test_facebook_login_errors(self):
""" Test wrong access token """
facebook_wrong_token = {
"provider": "facebook",
"access_token": "wrong-{}".format(self.FACEBOOK_ACCESS_TOKEN)
}
response = self.client.post(reverse('social_auth'),
facebook_wrong_token,
format='json')
self.assertIn(b"access_token/token_secret error",
response.content) # wrong credentials passed
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

# Test wrong provider
facebook_wrong_provider = {
"provider": "face",
"access_token": self.FACEBOOK_ACCESS_TOKEN
}
response = self.client.post(reverse('social_auth'),
facebook_wrong_provider,
format='json')
self.assertIn(b"Provider is invalid",
response.content) # wrong credentials passed
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_twitter_login(self):
""" Test successful user login """
response = self.client.post(reverse('social_auth'),
self.twitter_data,
format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

response.render()
self.assertIn(b"GransonO",
response.content) # TEST FOR USERNAME
self.assertIn(b"twitter", response.content) # TEST FOR THE PROVIDER
self.assertIn(b"social_token", response.content) # TEST FOR THE TOKEN

def test_twitter_login_errors(self):
# Test wrong access token
facebook_wrong_token = {
"provider": "facebook",
"access_token": "wrong-{}".format(self.FACEBOOK_ACCESS_TOKEN)
}
response = self.client.post(reverse('social_auth'),
facebook_wrong_token,
format='json')
self.assertIn(b"access_token/token_secret error",
response.content) # wrong credentials passed
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

# Test wrong provider
twitter_wrong_provider = {
"provider": "face",
"access_token": "This is a wrong token",
"access_token_secret": self.TWITTER_ACCESS_TOKEN_SECRET
}
response = self.client.post(reverse('social_auth'),
twitter_wrong_provider,
format='json')
self.assertIn(b"Provider is invalid",
response.content) # wrong credentials passed
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

def test_google_login(self):
pass

def test_google_login_errors(self):
pass
7 changes: 6 additions & 1 deletion authors/apps/authentication/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
ResetPasswordRequestAPIView,
ResetPasswordConfirmAPIView,
ListUsersAPIView,
ActivateAPIView
ActivateAPIView,
SocialAuthenticate
)


Expand Down Expand Up @@ -39,4 +40,8 @@
'users/activate/<uidb64>/<token>',
ActivateAPIView.as_view(),
name='activate'),
path(
'users/social/auth',
SocialAuthenticate.as_view(),
name='social_auth'),
]
94 changes: 91 additions & 3 deletions authors/apps/authentication/views.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import jwt
import random
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.sites.shortcuts import get_current_site
Expand All @@ -17,13 +18,18 @@
from .models import User
from .renderers import UserJSONRenderer
from .serializers import (
LoginSerializer, RegistrationSerializer, UserSerializer
LoginSerializer, RegistrationSerializer, UserSerializer,
SocialAuthenticator
)
from social_django.utils import load_strategy, load_backend
from social.backends.oauth import BaseOAuth1, BaseOAuth2
from social_core.exceptions import MissingBackend
from social.exceptions import AuthAlreadyAssociated


# Instantiate base classes
instance = User()
auth = JWTAuthentication()

User = get_user_model()


Expand Down Expand Up @@ -190,7 +196,7 @@ def get(self, request, uidb64, token, **kwargs):
return Response({'message': 'Activation link has expired'})

if user is not None and account_activation_token.check_token(
user, token):
user, token):
# activate user and login:
user.is_active = True
user.save()
Expand All @@ -199,3 +205,85 @@ def get(self, request, uidb64, token, **kwargs):

else:
return Response({'message': 'Activation link is invalid'})


class SocialAuthenticate(GenericAPIView):
"""processes logins form social platforms"""
permission_classes = (AllowAny,)
serializer_class = SocialAuthenticator
login_serializer = LoginSerializer

def post(self, request, **kwargs):
"""Receives payload data and retrieves user datails"""
payload = request.data
serializer = self.serializer_class(data=payload)
serializer.is_valid(raise_exception=True)
provider = serializer.data.get("provider")

try:
strategy = load_strategy(request)
backend = load_backend(
strategy=strategy, name=provider, redirect_uri=None)
baseOAuth1 = isinstance(backend, BaseOAuth1)
baseOAuth2 = isinstance(backend, BaseOAuth2)
if baseOAuth1:
# Check if access token is passed
if "access_token_secret" in request.data:
token = {
'oauth_token': payload.get('access_token'),
'oauth_token_secret': payload.get(
'access_token_secret'),
}
else:
return Response(
{"error": "Access token secret is required"},
status=status.HTTP_400_BAD_REQUEST
)

elif baseOAuth2:
token = payload.get('access_token')

else:
return Response({'message':
'Could not identify Oauth property used'})

except MissingBackend:
return Response({"error": "The Provider is invalid"},
status=status.HTTP_400_BAD_REQUEST)

try:
response_user = backend.do_auth(token, ajax=True)

except AuthAlreadyAssociated:
# You can't associate a social account with more than user
return Response({"message": "Tha account is in use"},
status=status.HTTP_400_BAD_REQUEST)

except BaseException as what:
return Response({"message": "access_token/token_secret error",
"error": str(what)},
status=status.HTTP_400_BAD_REQUEST)

# login(request, user)
response_data = {
'username': response_user.username,
'email': '{}'.format(response_user),
'provider': provider
}

user = User.objects.get(username=response_data["username"])
user.is_active = True

# If twitter
if provider == 'twitter':
user.email = "{}.{}@twit.auth".format(response_user.username,
random.randint(1, 1000))
user.save()

token = auth.generate_social_token(response_user.username)

result = {
'login_data': response_data,
'social_token': 'Token {}'.format(token)
}
return Response(result, status=status.HTTP_200_OK)
Loading

0 comments on commit 485533d

Please sign in to comment.