diff --git a/README.md b/README.md index b45a011..ed67e04 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ This README aims to document functionality of backend as well as required steps - [First Steps](#first-steps) - [Load location data](#load-location-data) - [Load taxonomy data](#load-taxonomy-data) +- [Social Auth](#social-auth) - [Company Endpoints](#company-endpoints) - [Product Endpoints](#product-endpoints) - [Core Endpoints](#core-endpoints) @@ -61,6 +62,26 @@ This data serves as initial Tags To load initial set of tags: `python manage.py addtaxonomy` + +## Social Auth + +Use your credentials from social media network to create acount and log in. + +Backend must be registered with every service that we want to use it with. + +During registration, provide a `Callback URL`, where the app can receive requests. +You are given `client key` and `client secret` tokens, used to validate login requests. + +### Facebook + +- Go to `developers.facebook.com/` +- click on `My Apps` +- On `Add a New App`, click on `Website` +- Click on `Create App ID` +- On the next screen click on `Skip Quick Start` +- grab the `App ID` and `App Secret` +- in `Settings / Basic`, click on the button `+ Add Platform` and add a website, and other info + ## Company Endpoints ### CompanyViewSet diff --git a/back_latienda/settings/base.py b/back_latienda/settings/base.py index 4996fc6..290c4a5 100644 --- a/back_latienda/settings/base.py +++ b/back_latienda/settings/base.py @@ -57,6 +57,9 @@ INSTALLED_APPS = [ 'storages', 'mapwidgets', 'django_admin_listfilter_dropdown', + 'oauth2_provider', + 'social_django', + 'rest_framework_social_oauth2', # local apps 'core', @@ -77,6 +80,8 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + + 'social_django.middleware.SocialAuthExceptionMiddleware', ] ROOT_URLCONF = 'back_latienda.urls' @@ -92,6 +97,9 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + + 'social_django.context_processors.backends', + 'social_django.context_processors.login_redirect', ], }, }, @@ -123,6 +131,15 @@ AUTH_PASSWORD_VALIDATORS = [ AUTH_USER_MODEL = 'core.CustomUser' +AUTHENTICATION_BACKENDS = ( + 'rest_framework_social_oauth2.backends.DjangoOAuth2', + 'django.contrib.auth.backends.ModelBackend', +) + +DRFSO2_PROPRIETARY_BACKEND_NAME = os.getenv('DRFSO2_PROPRIETARY_BACKEND_NAME') # E.g. Facebook +DRFSO2_URL_NAMESPACE = os.getenv('DRFSO2_URL_NAMESPACE') # namespace for reversing URLs + + # DRF Options REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': [ @@ -130,6 +147,9 @@ REST_FRAMEWORK = { ], 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', + + 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', + 'rest_framework_social_oauth2.authentication.SocialAuthentication', ], 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', diff --git a/back_latienda/urls.py b/back_latienda/urls.py index b18a320..809426e 100644 --- a/back_latienda/urls.py +++ b/back_latienda/urls.py @@ -31,6 +31,7 @@ admin.site.site_header = 'LaTiendaCOOP Administration' urlpatterns = [ path('admin/', admin.site.urls), path('activate///',core_views.activate_user, name='activate_user'), + path('api/v1/social-auth/', include('rest_framework_social_oauth2.urls')), path('api/v1/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('api/v1/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('api/v1/token/verify/', TokenVerifyView.as_view(), name='token_verify'), diff --git a/core/tests.py b/core/tests.py index 6144a64..a4285c8 100644 --- a/core/tests.py +++ b/core/tests.py @@ -722,3 +722,77 @@ class AdminStatsTest(APITestCase): for name in expected_entries: self.assertTrue(name in payload) + +''' +class SocialLoginTest(APITestCase): + + def setUp(self): + self.endpoint = '/api/v1/admin_stats/' + self.factory = factories.CustomUserFactory + self.model = models.CustomUser + # create user + self.email = "user@mail.com" + self.password = ''.join(random.choices(string.ascii_uppercase, k = 10)) + self.user = self.factory(email=self.email, is_active=True) + self.user.set_password(self.password) + self.user.save() + # data + self.callback_uri = 'http://127.0.0.1:8000/' + + def test_user_can_login_facebook(self): + # get auth page + url = f'https://facebook.com/auth? \ + response_type=code& \ + client_id={settings.FACEBOOK_ID}& \ + redirect_uri={self.callback_uri}& \ + scope=profile& \ + scope=email' + + response = self.client.get(url) + # make assertions + self.assertEquals(response.status_code, 200) + + # authenticate to auth page if not logged in + url2 = response.url + creds = { + 'email': 'sam@mail.com', + 'password': 'supersecret' + } + response = self.client.post(url2, data=creds) + # assertions + self.assertEquals(response.status_code, 200) + # redirection url + redirect = response.url + auth_code = redirect.split('=')[-1] + + # authenticate previous query is valid + url = f'https://facebook.com/token/? + grant_type=authorization_code&\ + code={auth_code}&\ + redirect_uri={self.callback_uri}&\ + client_id={settings.FACEBOOK_ID}&\ + client_secret=CLIENT_SECRET' + + response = self.client.get(url) + # assertions + self.assertEquals(response.status_code, 200) + # redirection url + redirect = response.url + auth_code = redirect.split('=')[-1] + + def test_user_can_login_google(self): + url = f'https://google.com/auth? \ + response_type=code& \ + client_id={settings.GOOGLE_CLIENT_ID}& \ + redirect_uri=CALLBACK_URI& \ + scope=profile& \ + scope=email' + + response = self.client.get(url) + + # assertions + self.assertEquals(response.status_code, 200) + + def test_bad_login(self): + pass +''' diff --git a/example.env b/example.env index 87e699b..ea51289 100644 --- a/example.env +++ b/example.env @@ -16,4 +16,13 @@ WC_SECRET = '' # GOOGLE MAPS GOOGLE_MAP_API_KEY = '' # USER ACTIVATION REDIRECTION -ACTIVATION_REDIRECT = '' \ No newline at end of file +ACTIVATION_REDIRECT = '' +# SOCIAL LOGIN +SOCIAL_AUTH_GITHUB_KEY = '' +SOCIAL_AUTH_GITHUB_SECRET = '' +SOCIAL_AUTH_FACEBOOK_KEY = '' +SOCIAL_AUTH_FACEBOOK_SECRET = '' +DRFSO2_PROPRIETARY_BACKEND_NAME = '' # E.g. Facebook +DRFSO2_URL_NAMESPACE = '' # namespace for reversing URLs +GOOGLE_CLIENT_ID='' +FACEBOOK_ID='' \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index df15689..153a02a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,7 @@ django-autocomplete-light==3.8.2 # manually install `pip install --default-timeout=100 future` to avoid wcapi to timeout django-map-widgets==0.3.0 django-admin-list-filter-dropdown==1.0.3 +django-rest-framework-social-oauth2==1.1.0 # required for production django-anymail[amazon_ses]==8.2 boto3==1.17.11